Follow what the live server does with tenants and domains
A run on the production server with throwaway tenants, a role, lists and a domain, all removed, found three things the source reading had not: - Something in a tenant has to be on a domain in that tenant (a list in a tenant on an unassigned domain is invalidForeignKey), while something in no tenant may be on a tenant's domain. The account panel's tenant choice offered every tenant; it offers only the domain's now, and a new account starts in the tenant of the domain it is made on. The domain list reads memberTenantId for it. - A domain created in a tenant puts its DKIM keys there too, and they keep the tenant from being deleted. They are counted with the rest, so Delete is not offered while any remain. - Stalwart lets a domain leave a tenant while the tenant still has accounts on it, stranding them. The panel asks first and refuses while any are there. The refusal to delete a tenant that holds anything was confirmed, as were tenant create, quota pointers, logo and rename. The mock follows the domain rule, filters DKIM keys by tenant, and KNOWN-ISSUES records the run. The non-Enterprise notice is now just "Tenants are a Stalwart Enterprise feature." Two sentences were reworded and one plural added, in all nine catalogues, and the old sentences are gone.
This commit is contained in:
+8
-4
@@ -1278,14 +1278,18 @@ under Access:
|
|||||||
- **The tenant's role** is the most anyone inside it can be allowed: their own
|
- **The tenant's role** is the most anyone inside it can be allowed: their own
|
||||||
roles are cut down to it.
|
roles are cut down to it.
|
||||||
- **What it holds** is counted, each against its limit. Stalwart keeps no list
|
- **What it holds** is counted, each against its limit. Stalwart keeps no list
|
||||||
on the tenant; each account, group, domain, list and role names its tenant,
|
on the tenant; each account, group, domain, list, role and DKIM key names its
|
||||||
so the counts are queries for those.
|
tenant, so the counts are queries for those. A domain created in a tenant
|
||||||
|
brings its keys with it.
|
||||||
- **Domains** are added to a tenant, or taken out, from its panel. Only a domain
|
- **Domains** are added to a tenant, or taken out, from its panel. Only a domain
|
||||||
in no tenant can be added, and the accounts already on it stay where they
|
in no tenant can be added, and the accounts already on it stay where they
|
||||||
are.
|
are. A domain comes out only once none of the tenant's accounts are on it —
|
||||||
|
Stalwart would allow it, and strand them.
|
||||||
- **An account's tenant** is chosen on the account's own panel, which is how a
|
- **An account's tenant** is chosen on the account's own panel, which is how a
|
||||||
tenant gets its first administrator: an Administrator inside a tenant
|
tenant gets its first administrator: an Administrator inside a tenant
|
||||||
administers that tenant.
|
administers that tenant. Stalwart puts something in a tenant only on a domain
|
||||||
|
in that tenant, so the choice is between no tenant and the domain's own, and
|
||||||
|
a new account starts in its domain's tenant.
|
||||||
- **Delete** is offered once the tenant holds nothing.
|
- **Delete** is offered once the tenant holds nothing.
|
||||||
|
|
||||||
Only an administrator outside every tenant can put anything into one; Stalwart
|
Only an administrator outside every tenant can put anything into one; Stalwart
|
||||||
|
|||||||
+9
-1
@@ -86,7 +86,15 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
|
|||||||
|
|
||||||
**The picker is stricter than the server for a few permissions.** `GET /api/account` never lists some permissions an administrator holds — `sysLogCreate` among them, which was granted without complaint — so their *Allow* is locked for everyone. That errs towards refusing and can be revisited if it gets in anyone's way. Still from source only: that a denial anywhere in a role's tree wins (`permissions.rs` unions enabled and disabled across the tree, then subtracts). **`GET /api/schema` has not been fetched through ihasmail's server on production** — the route sends the session's Basic credential, which reaches every other endpoint, and the source serves the schema to any signed-in account; until it is seen working there the picker's fallback is a notice that the list could not be loaded.
|
**The picker is stricter than the server for a few permissions.** `GET /api/account` never lists some permissions an administrator holds — `sysLogCreate` among them, which was granted without complaint — so their *Allow* is locked for everyone. That errs towards refusing and can be revisited if it gets in anyone's way. Still from source only: that a denial anywhere in a role's tree wins (`permissions.rs` unions enabled and disabled across the tree, then subtracts). **`GET /api/schema` has not been fetched through ihasmail's server on production** — the route sends the session's Basic credential, which reaches every other endpoint, and the source serves the schema to any signed-in account; until it is seen working there the picker's fallback is a notice that the list could not be loaded.
|
||||||
|
|
||||||
- **Tenants were built from the 0.16.22 source, its schema and the mock; nothing about them has been written on a live server yet.** Production is Enterprise with no tenants. Read from source: `x:Tenant` is `name`, `logo`, `roles`, `permissions`, `quotas` (a map of `TenantStorageQuota` names) and computed `usedDiskQuota`; membership is `memberTenantId` on accounts, groups, domains, lists, roles and DKIM keys; only a caller outside every tenant may set it (`set.rs` passes `can_set_tenant` only when the token has no tenant, and anyone else gets "Cannot modify memberTenantId property"); a tenant administrator's queries are scoped to the tenant. Assumed, not confirmed: that a tenant still named by anything is refused as `objectIsLinked` — the panel offers the delete only once its counts are all zero, so this matters only if a count cannot be read.
|
- **Tenants were built from the 0.16.22 source, its schema and the mock, then tried on the live server (2026-09-15)** with throwaway `ihasmail-tenant-test` tenants, a throwaway role, two throwaway lists and a throwaway domain, `ihasmail-tenant-test.ttlhost.com`, all removed. The live run changed the design twice:
|
||||||
|
|
||||||
|
- **A tenant is created and edited as built**: `name`, `logo`, `roles`, `permissions`, `quotas`; `quotas/<name>` pointers, a logo and a rename in one update; an unknown quota name is `invalidPatch`.
|
||||||
|
- **Something in a tenant has to be on a domain in that tenant.** A list in the tenant on a domain in none was refused, `invalidForeignKey` with `objectId` `{"object": "Domain", …}`; the same list on a domain created in the tenant was accepted — and so was a list in *no* tenant on that domain. **So an account's tenant choice offers only its domain's tenant**, and a new account starts in the tenant of the domain it is made on.
|
||||||
|
- **A domain created in a tenant puts its DKIM keys in the tenant too**, and they stay there. They count against `maxDkimKeys` and keep the tenant from being deleted, so they are counted with everything else.
|
||||||
|
- **Stalwart lets a domain leave a tenant while the tenant still has things on it**, leaving them in a tenant on a domain outside it. **The panel refuses to take a domain out while any of the tenant's accounts are on it.** Mailing lists cannot be filtered by domain, so a list is not checked.
|
||||||
|
- **A tenant still holding anything is kept**: `objectIsLinked`, `objectId` `{"object": "Tenant", …}`, `linkedObjects` naming a role, a list and DKIM keys. A role set to `memberTenantId: null` left it, after which the tenant was deleted.
|
||||||
|
|
||||||
|
Still from source only: that only a caller outside every tenant may set `memberTenantId` (`set.rs` passes `can_set_tenant` only when the token has no tenant), and that a tenant administrator's queries are scoped to the tenant. On a server that does not report Enterprise the Tenants page is only its notice.
|
||||||
|
|
||||||
- **The permission labels in eight languages are machine translations awaiting native review.** 661 labels and 59 headings per language, written against each catalogue's existing terms. The translators flagged the terms they were least sure of, which are the place to start: *principal* (JMAP/DAV), *throttles*, *listeners*, *lookups*, *milters*, *masked emails*, *samples* (spam training), *schedules* (MTA delivery), *email submission*, and the MTA stage settings. Several of Stalwart's own English labels are identical for different permissions (ARF, DMARC and TLS reports are all "Get reports"), and the translations inherit that; the heading above tells them apart.
|
- **The permission labels in eight languages are machine translations awaiting native review.** 661 labels and 59 headings per language, written against each catalogue's existing terms. The translators flagged the terms they were least sure of, which are the place to start: *principal* (JMAP/DAV), *throttles*, *listeners*, *lookups*, *milters*, *masked emails*, *samples* (spam training), *schedules* (MTA delivery), *email submission*, and the MTA stage settings. Several of Stalwart's own English labels are identical for different permissions (ARF, DMARC and TLS reports are all "Get reports"), and the translations inherit that; the heading above tells them apart.
|
||||||
|
|
||||||
|
|||||||
@@ -286,3 +286,17 @@ test("a tenant administrator cannot move anything into a tenant", () => {
|
|||||||
assert.equal(r.notUpdated?.d4?.type, "invalidPatch");
|
assert.equal(r.notUpdated?.d4?.type, "invalidPatch");
|
||||||
assert.match(r.notUpdated!.d4!.description, /memberTenantId/);
|
assert.match(r.notUpdated!.d4!.description, /memberTenantId/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("something in a tenant has to be on a domain in it, and something in none may be anywhere", () => {
|
||||||
|
const dir = make("admin");
|
||||||
|
const outside = dir.handlers["x:MailingList/set"]!({ create: { n: { name: "stray", domainId: "d1", memberTenantId: "t1" } } }) as { notCreated?: Record<string, { type: string; objectId: { object: string } }> };
|
||||||
|
assert.equal(outside.notCreated?.n?.type, "invalidForeignKey");
|
||||||
|
assert.equal(outside.notCreated!.n!.objectId.object, "Domain");
|
||||||
|
const inside = dir.handlers["x:MailingList/set"]!({ create: { n: { name: "team", domainId: "d3", memberTenantId: "t1" } } }) as { created?: Record<string, { id: string }> };
|
||||||
|
assert.ok(inside.created?.n?.id);
|
||||||
|
const none = dir.handlers["x:MailingList/set"]!({ create: { n: { name: "open", domainId: "d3" } } }) as { created?: Record<string, { id: string }> };
|
||||||
|
assert.ok(none.created?.n?.id);
|
||||||
|
const [someone] = (dir.handlers["x:Account/query"]!({ filter: { "@type": "User", domainId: "d1" } }) as { ids: string[] }).ids;
|
||||||
|
const move = dir.handlers["x:Account/set"]!({ update: { [someone!]: { memberTenantId: "t1" } } }) as { notUpdated?: Record<string, { type: string }> };
|
||||||
|
assert.equal(move.notUpdated?.[someone!]?.type, "invalidForeignKey");
|
||||||
|
});
|
||||||
|
|||||||
@@ -231,6 +231,17 @@ export function createDirectory(opts: Options) {
|
|||||||
{ id: "t1", name: "Acme Corp", logo: null, roles: { "@type": "Default" }, permissions: { "@type": "Inherit" }, quotas: { maxAccounts: 25, maxDomains: 2, maxDiskQuota: 50 * GIB }, createdAt: "2026-07-01T09:00:00Z" },
|
{ id: "t1", name: "Acme Corp", logo: null, roles: { "@type": "Default" }, permissions: { "@type": "Inherit" }, quotas: { maxAccounts: 25, maxDomains: 2, maxDiskQuota: 50 * GIB }, createdAt: "2026-07-01T09:00:00Z" },
|
||||||
];
|
];
|
||||||
const tenantUsage = (id: string) => accounts.filter((x) => x.memberTenantId === id).reduce((n, x) => n + Number(x.usedDiskQuota ?? 0), 0);
|
const tenantUsage = (id: string) => accounts.filter((x) => x.memberTenantId === id).reduce((n, x) => n + Number(x.usedDiskQuota ?? 0), 0);
|
||||||
|
/**
|
||||||
|
* Something in a tenant has to be on a domain in that tenant; something in no
|
||||||
|
* tenant may be on anyone's domain. Both as the live server answered
|
||||||
|
* (2026-09-15), including the shape of the refusal.
|
||||||
|
*/
|
||||||
|
const domainTenantRefused = (o: Obj): Obj | null => {
|
||||||
|
const tenant = o.memberTenantId ?? null;
|
||||||
|
const domain = domains.find((d) => d.id === o.domainId);
|
||||||
|
if (!tenant || !domain || (domain.memberTenantId ?? null) === tenant) return null;
|
||||||
|
return { type: "invalidForeignKey", objectId: { object: "Domain", id: domain.id } };
|
||||||
|
};
|
||||||
/** Only an administrator outside every tenant may put things in one; Stalwart refuses anyone else. */
|
/** Only an administrator outside every tenant may put things in one; Stalwart refuses anyone else. */
|
||||||
const tenantRefused = (patch: Obj): Obj | null =>
|
const tenantRefused = (patch: Obj): Obj | null =>
|
||||||
"memberTenantId" in patch && opts.role !== "admin" ? setError("invalidPatch", "Cannot modify memberTenantId property", ["memberTenantId"]) : null;
|
"memberTenantId" in patch && opts.role !== "admin" ? setError("invalidPatch", "Cannot modify memberTenantId property", ["memberTenantId"]) : null;
|
||||||
@@ -355,7 +366,7 @@ export function createDirectory(opts: Options) {
|
|||||||
const refused = grantRefused(o.roles);
|
const refused = grantRefused(o.roles);
|
||||||
if (refused) { notCreated[cid] = setError("forbidden", refused); continue; }
|
if (refused) { notCreated[cid] = setError("forbidden", refused); continue; }
|
||||||
if (o.memberTenantId) {
|
if (o.memberTenantId) {
|
||||||
const refusedTenant = tenantRefused(o);
|
const refusedTenant = tenantRefused(o) ?? domainTenantRefused(o);
|
||||||
if (refusedTenant) { notCreated[cid] = refusedTenant; continue; }
|
if (refusedTenant) { notCreated[cid] = refusedTenant; continue; }
|
||||||
}
|
}
|
||||||
const password = Object.values((o.credentials as Obj) ?? {})[0] as Obj | undefined;
|
const password = Object.values((o.credentials as Obj) ?? {})[0] as Obj | undefined;
|
||||||
@@ -389,6 +400,7 @@ export function createDirectory(opts: Options) {
|
|||||||
if (target["@type"] === "Group") failure = setError("invalidProperties", "Groups cannot be members of other groups.", ["memberGroupIds"]);
|
if (target["@type"] === "Group") failure = setError("invalidProperties", "Groups cannot be members of other groups.", ["memberGroupIds"]);
|
||||||
else if (Object.keys((next.memberGroupIds as Obj) ?? {}).some((g) => accounts.find((x) => x.id === g)?.["@type"] !== "Group")) failure = setError("invalidForeignKey", "Group does not exist.", ["memberGroupIds"]);
|
else if (Object.keys((next.memberGroupIds as Obj) ?? {}).some((g) => accounts.find((x) => x.id === g)?.["@type"] !== "Group")) failure = setError("invalidForeignKey", "Group does not exist.", ["memberGroupIds"]);
|
||||||
}
|
}
|
||||||
|
if (!failure && "memberTenantId" in patch) failure = domainTenantRefused(next);
|
||||||
if (!failure && ("roles" in patch || "permissions" in patch)) {
|
if (!failure && ("roles" in patch || "permissions" in patch)) {
|
||||||
const refused = grantRefused(next.roles);
|
const refused = grantRefused(next.roles);
|
||||||
if (refused) failure = setError("forbidden", refused);
|
if (refused) failure = setError("forbidden", refused);
|
||||||
@@ -477,7 +489,8 @@ export function createDirectory(opts: Options) {
|
|||||||
return { accountId: opts.accountId, oldState: "1", newState: "2", created, updated, destroyed, ...(Object.keys(notCreated).length ? { notCreated } : {}), ...(Object.keys(notUpdated).length ? { notUpdated } : {}), ...(Object.keys(notDestroyed).length ? { notDestroyed } : {}) };
|
return { accountId: opts.accountId, oldState: "1", newState: "2", created, updated, destroyed, ...(Object.keys(notCreated).length ? { notCreated } : {}), ...(Object.keys(notUpdated).length ? { notUpdated } : {}), ...(Object.keys(notDestroyed).length ? { notDestroyed } : {}) };
|
||||||
},
|
},
|
||||||
"x:DkimSignature/get": get(dkimKeys, "sysDkimSignatureGet"),
|
"x:DkimSignature/get": get(dkimKeys, "sysDkimSignatureGet"),
|
||||||
"x:DkimSignature/query": query(() => dkimKeys, "sysDkimSignatureQuery", ["domainId", "memberTenantId"], (o, f) => f.domainId === undefined || o.domainId === f.domainId),
|
"x:DkimSignature/query": query(() => dkimKeys, "sysDkimSignatureQuery", ["domainId", "memberTenantId"], (o, f) =>
|
||||||
|
(f.domainId === undefined || o.domainId === f.domainId) && (f.memberTenantId === undefined || (o.memberTenantId ?? null) === f.memberTenantId)),
|
||||||
"x:DkimSignature/set": (a) => {
|
"x:DkimSignature/set": (a) => {
|
||||||
const destroyed: string[] = [];
|
const destroyed: string[] = [];
|
||||||
for (const id of (a.destroy as string[]) ?? []) {
|
for (const id of (a.destroy as string[]) ?? []) {
|
||||||
@@ -526,10 +539,10 @@ export function createDirectory(opts: Options) {
|
|||||||
for (const [cid, raw] of Object.entries((a.create as Obj) ?? {})) {
|
for (const [cid, raw] of Object.entries((a.create as Obj) ?? {})) {
|
||||||
demand("sysMailingListCreate");
|
demand("sysMailingListCreate");
|
||||||
const o: Obj = { recipients: {}, aliases: {}, description: null, ...(raw as Obj) };
|
const o: Obj = { recipients: {}, aliases: {}, description: null, ...(raw as Obj) };
|
||||||
const failure = check(o);
|
const failure = check(o) ?? (o.memberTenantId ? (tenantRefused(o) ?? domainTenantRefused(o)) : null);
|
||||||
if (failure) { notCreated[cid] = failure; continue; }
|
if (failure) { notCreated[cid] = failure; continue; }
|
||||||
const id = `l${counter++}`;
|
const id = `l${counter++}`;
|
||||||
lists.push({ ...o, id, memberTenantId: null });
|
lists.push({ memberTenantId: null, ...o, id });
|
||||||
created[cid] = { id, emailAddress: `${o.name}@${domainName(o.domainId)}` };
|
created[cid] = { id, emailAddress: `${o.name}@${domainName(o.domainId)}` };
|
||||||
}
|
}
|
||||||
for (const [id, raw] of Object.entries((a.update as Obj) ?? {})) {
|
for (const [id, raw] of Object.entries((a.update as Obj) ?? {})) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe("what a tenant holds", () => {
|
|||||||
if (method === "x:Role/query") throw new Error("forbidden");
|
if (method === "x:Role/query") throw new Error("forbidden");
|
||||||
return { total: method === "x:Account/query" && f["@type"] === "Group" ? 2 : 1 };
|
return { total: method === "x:Account/query" && f["@type"] === "Group" ? 2 : 1 };
|
||||||
});
|
});
|
||||||
expect(await countTenantMembers("t1")).toEqual({ accounts: 1, groups: 2, lists: 1, domains: 1 });
|
expect(await countTenantMembers("t1")).toEqual({ accounts: 1, groups: 2, lists: 1, domains: 1, dkimKeys: 1 });
|
||||||
expect(call).toHaveBeenCalledWith("x:Account/query", { filter: { "@type": "User", memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
expect(call).toHaveBeenCalledWith("x:Account/query", { filter: { "@type": "User", memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
||||||
expect(call).toHaveBeenCalledWith("x:Domain/query", { filter: { memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
expect(call).toHaveBeenCalledWith("x:Domain/query", { filter: { memberTenantId: "t1" }, limit: 0, calculateTotal: true });
|
||||||
call.mockRestore();
|
call.mockRestore();
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export interface DirectoryAccount {
|
|||||||
export interface DirectoryDomain {
|
export interface DirectoryDomain {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** The tenant the domain is in: an account can be in a tenant only on one of its domains. */
|
||||||
|
memberTenantId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACCOUNT_PROPERTIES = [
|
const ACCOUNT_PROPERTIES = [
|
||||||
@@ -121,7 +123,7 @@ async function all<T>(object: "Domain" | "Role", properties: string[]): Promise<
|
|||||||
return res.list;
|
return res.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listDomains = () => all<DirectoryDomain>("Domain", ["name"]);
|
export const listDomains = () => all<DirectoryDomain>("Domain", ["name", "memberTenantId"]);
|
||||||
export const listRoles = () => all<RoleDef>("Role", ["description", "enabledPermissions", "roleIds"]);
|
export const listRoles = () => all<RoleDef>("Role", ["description", "enabledPermissions", "roleIds"]);
|
||||||
|
|
||||||
export async function listGroups(): Promise<DirectoryAccount[]> {
|
export async function listGroups(): Promise<DirectoryAccount[]> {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export const TENANT_MEMBERS = [
|
|||||||
{ key: "lists", method: "x:MailingList/query", filter: {}, quota: "maxMailingLists" },
|
{ key: "lists", method: "x:MailingList/query", filter: {}, quota: "maxMailingLists" },
|
||||||
{ key: "domains", method: "x:Domain/query", filter: {}, quota: "maxDomains" },
|
{ key: "domains", method: "x:Domain/query", filter: {}, quota: "maxDomains" },
|
||||||
{ key: "roles", method: "x:Role/query", filter: {}, quota: "maxRoles" },
|
{ key: "roles", method: "x:Role/query", filter: {}, quota: "maxRoles" },
|
||||||
|
// A domain's keys join the tenant it was created in, and keep it there.
|
||||||
|
{ key: "dkimKeys", method: "x:DkimSignature/query", filter: {}, quota: "maxDkimKeys" },
|
||||||
] as const;
|
] as const;
|
||||||
export type TenantMemberKind = (typeof TENANT_MEMBERS)[number]["key"];
|
export type TenantMemberKind = (typeof TENANT_MEMBERS)[number]["key"];
|
||||||
|
|
||||||
@@ -118,6 +120,18 @@ export async function tenantDomains(tenantId: string): Promise<{ inTenant: Array
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many of a tenant's accounts and groups are on a domain.
|
||||||
|
*
|
||||||
|
* Stalwart lets a domain leave a tenant while the tenant still has accounts on
|
||||||
|
* it (live, 2026-09-15), leaving them in a tenant on a domain outside it --
|
||||||
|
* which it refuses to create. The panel asks this before it offers the move.
|
||||||
|
*/
|
||||||
|
export async function tenantAccountsOnDomain(tenantId: string, domainId: string): Promise<number> {
|
||||||
|
const res = await client.call<{ total?: number }>("x:Account/query", { filter: { domainId, memberTenantId: tenantId }, limit: 0, calculateTotal: true });
|
||||||
|
return res.total ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** Put a domain in a tenant, or take it out with null. */
|
/** Put a domain in a tenant, or take it out with null. */
|
||||||
export async function setDomainTenant(domainId: string, tenantId: string | null): Promise<void> {
|
export async function setDomainTenant(domainId: string, tenantId: string | null): Promise<void> {
|
||||||
const res = await client.call<SetResponse>("x:Domain/set", { update: { [domainId]: { memberTenantId: tenantId } } });
|
const res = await client.call<SetResponse>("x:Domain/set", { update: { [domainId]: { memberTenantId: tenantId } } });
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "Der Server hat nicht mitgeteilt, ob die Rolle angelegt wurde.",
|
"The server did not say whether the role was created.": "Der Server hat nicht mitgeteilt, ob die Rolle angelegt wurde.",
|
||||||
"No tenant": "Kein Mandant",
|
"No tenant": "Kein Mandant",
|
||||||
"You can't move your own account into a tenant.": "Sie können Ihr eigenes Konto nicht in einen Mandanten verschieben.",
|
"You can't move your own account into a tenant.": "Sie können Ihr eigenes Konto nicht in einen Mandanten verschieben.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Ein Konto in einem Mandanten ist durch dessen Rolle begrenzt und zählt zu dessen Limits, und Administrator bedeutet Administrator dieses Mandanten.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Ein Konto kann in dem Mandanten sein, in dem seine Domain ist. In einem Mandanten ist es durch dessen Rolle begrenzt und zählt zu dessen Limits, und Administrator bedeutet Administrator dieses Mandanten.",
|
||||||
"Tenants": "Mandanten",
|
"Tenants": "Mandanten",
|
||||||
"Storage in GB": "Speicher in GB",
|
"Storage in GB": "Speicher in GB",
|
||||||
"Default tenant roles": "Standardrollen für Mandanten",
|
"Default tenant roles": "Standardrollen für Mandanten",
|
||||||
@@ -284,13 +284,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "{domain} aus dem Mandanten entfernen",
|
"Take {domain} out of the tenant": "{domain} aus dem Mandanten entfernen",
|
||||||
"No domains in this tenant yet": "Noch keine Domains in diesem Mandanten",
|
"No domains in this tenant yet": "Noch keine Domains in diesem Mandanten",
|
||||||
"Domain to add": "Hinzuzufügende Domain",
|
"Domain to add": "Hinzuzufügende Domain",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Nur Domains ohne Mandanten können hinzugefügt werden. Die Konten auf einer Domain bleiben, wo sie sind; verschieben Sie jedes in seinem eigenen Bereich.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Nur Domains ohne Mandanten können hinzugefügt werden, und die Konten darauf bleiben, wo sie sind. Eine Domain kann erst entfernt werden, wenn keines der Konten dieses Mandanten mehr darauf ist.",
|
||||||
"An empty tenant can be deleted.": "Ein leerer Mandant kann gelöscht werden.",
|
"An empty tenant can be deleted.": "Ein leerer Mandant kann gelöscht werden.",
|
||||||
"Delete tenant…": "Mandant löschen…",
|
"Delete tenant…": "Mandant löschen…",
|
||||||
"Still holds {things}. Move them out first.": "Enthält noch {things}. Verschieben Sie diese zuerst.",
|
"Still holds {things}. Move them out first.": "Enthält noch {things}. Verschieben Sie diese zuerst.",
|
||||||
"Delete tenant": "Mandant löschen",
|
"Delete tenant": "Mandant löschen",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Getrennte Organisationen auf einem Server, jede mit eigenen Personen, Domains und Limits.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Getrennte Organisationen auf einem Server, jede mit eigenen Personen, Domains und Limits.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Mandanten sind eine Funktion von Stalwart Enterprise. Dieser Server meldet kein Enterprise, daher hat jeder in einem Mandanten nur die Berechtigungen eines normalen Benutzers.",
|
"Tenants are a Stalwart Enterprise feature.": "Mandanten sind eine Funktion von Stalwart Enterprise.",
|
||||||
"Search tenants": "Mandanten durchsuchen",
|
"Search tenants": "Mandanten durchsuchen",
|
||||||
"No tenants match": "Keine passenden Mandanten",
|
"No tenants match": "Keine passenden Mandanten",
|
||||||
"No tenants yet": "Noch keine Mandanten",
|
"No tenants yet": "Noch keine Mandanten",
|
||||||
@@ -1716,6 +1716,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Gewährt {n} Berechtigung", other: "Gewährt {n} Berechtigungen" },
|
"Grants {n} permissions": { one: "Gewährt {n} Berechtigung", other: "Gewährt {n} Berechtigungen" },
|
||||||
"{n} roles": { one: "{n} Rolle", other: "{n} Rollen" },
|
"{n} roles": { one: "{n} Rolle", other: "{n} Rollen" },
|
||||||
"{n} tenants": { one: "{n} Mandant", other: "{n} Mandanten" },
|
"{n} tenants": { one: "{n} Mandant", other: "{n} Mandanten" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} Konto dieses Mandanten ist noch auf {domain}. Verschieben oder löschen Sie es, bevor Sie die Domain entfernen.", other: "{n} Konten dieses Mandanten sind noch auf {domain}. Verschieben oder löschen Sie sie, bevor Sie die Domain entfernen." },
|
||||||
"{n} DKIM keys": { one: "{n} DKIM-Schlüssel", other: "{n} DKIM-Schlüssel" },
|
"{n} DKIM keys": { one: "{n} DKIM-Schlüssel", other: "{n} DKIM-Schlüssel" },
|
||||||
"{n} other items": { one: "{n} weiteres Objekt", other: "{n} weitere Objekte" },
|
"{n} other items": { one: "{n} weiteres Objekt", other: "{n} weitere Objekte" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "El servidor no indicó si el rol se creó.",
|
"The server did not say whether the role was created.": "El servidor no indicó si el rol se creó.",
|
||||||
"No tenant": "Sin inquilino",
|
"No tenant": "Sin inquilino",
|
||||||
"You can't move your own account into a tenant.": "No puede mover su propia cuenta a un inquilino.",
|
"You can't move your own account into a tenant.": "No puede mover su propia cuenta a un inquilino.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Una cuenta de un inquilino está limitada por el rol del inquilino y cuenta para sus límites, y Administrador significa administrador de ese inquilino.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Una cuenta puede estar en el inquilino en el que está su dominio. En un inquilino está limitada por el rol del inquilino y cuenta para sus límites, y Administrador significa administrador de ese inquilino.",
|
||||||
"Tenants": "Inquilinos",
|
"Tenants": "Inquilinos",
|
||||||
"Storage in GB": "Almacenamiento en GB",
|
"Storage in GB": "Almacenamiento en GB",
|
||||||
"Default tenant roles": "Roles de inquilino predeterminados",
|
"Default tenant roles": "Roles de inquilino predeterminados",
|
||||||
@@ -276,13 +276,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "Quitar {domain} del inquilino",
|
"Take {domain} out of the tenant": "Quitar {domain} del inquilino",
|
||||||
"No domains in this tenant yet": "Aún no hay dominios en este inquilino",
|
"No domains in this tenant yet": "Aún no hay dominios en este inquilino",
|
||||||
"Domain to add": "Dominio para añadir",
|
"Domain to add": "Dominio para añadir",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Solo se pueden añadir dominios que no estén en ningún inquilino. Las cuentas que ya están en un dominio se quedan donde están; mueva cada una desde su propio panel.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Solo se pueden añadir dominios que no estén en ningún inquilino, y las cuentas que ya están en uno se quedan donde están. Un dominio solo se puede quitar cuando ninguna cuenta de este inquilino esté en él.",
|
||||||
"An empty tenant can be deleted.": "Un inquilino vacío se puede eliminar.",
|
"An empty tenant can be deleted.": "Un inquilino vacío se puede eliminar.",
|
||||||
"Delete tenant…": "Eliminar inquilino…",
|
"Delete tenant…": "Eliminar inquilino…",
|
||||||
"Still holds {things}. Move them out first.": "Aún tiene {things}. Muévalos primero.",
|
"Still holds {things}. Move them out first.": "Aún tiene {things}. Muévalos primero.",
|
||||||
"Delete tenant": "Eliminar inquilino",
|
"Delete tenant": "Eliminar inquilino",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Organizaciones separadas en un mismo servidor, cada una con sus propias personas, dominios y límites.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Organizaciones separadas en un mismo servidor, cada una con sus propias personas, dominios y límites.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Los inquilinos son una función de Stalwart Enterprise. Este servidor no indica Enterprise, así que cualquiera dentro de un inquilino solo tiene los permisos de un usuario normal.",
|
"Tenants are a Stalwart Enterprise feature.": "Los inquilinos son una función de Stalwart Enterprise.",
|
||||||
"Search tenants": "Buscar inquilinos",
|
"Search tenants": "Buscar inquilinos",
|
||||||
"No tenants match": "Ningún inquilino coincide",
|
"No tenants match": "Ningún inquilino coincide",
|
||||||
"No tenants yet": "Aún no hay inquilinos",
|
"No tenants yet": "Aún no hay inquilinos",
|
||||||
@@ -1689,6 +1689,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Concede {n} permiso", other: "Concede {n} permisos" },
|
"Grants {n} permissions": { one: "Concede {n} permiso", other: "Concede {n} permisos" },
|
||||||
"{n} roles": { one: "{n} rol", other: "{n} roles" },
|
"{n} roles": { one: "{n} rol", other: "{n} roles" },
|
||||||
"{n} tenants": { one: "{n} inquilino", other: "{n} inquilinos" },
|
"{n} tenants": { one: "{n} inquilino", other: "{n} inquilinos" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} cuenta de este inquilino sigue en {domain}. Muévala o elimínela antes de quitar el dominio.", other: "{n} cuentas de este inquilino siguen en {domain}. Muévalas o elimínelas antes de quitar el dominio." },
|
||||||
"{n} DKIM keys": { one: "{n} clave DKIM", other: "{n} claves DKIM" },
|
"{n} DKIM keys": { one: "{n} clave DKIM", other: "{n} claves DKIM" },
|
||||||
"{n} other items": { one: "{n} elemento más", other: "{n} elementos más" },
|
"{n} other items": { one: "{n} elemento más", other: "{n} elementos más" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "Le serveur n’a pas indiqué si le rôle a été créé.",
|
"The server did not say whether the role was created.": "Le serveur n’a pas indiqué si le rôle a été créé.",
|
||||||
"No tenant": "Aucun locataire",
|
"No tenant": "Aucun locataire",
|
||||||
"You can't move your own account into a tenant.": "Vous ne pouvez pas déplacer votre propre compte dans un locataire.",
|
"You can't move your own account into a tenant.": "Vous ne pouvez pas déplacer votre propre compte dans un locataire.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Un compte dans un locataire est limité par le rôle du locataire et compte dans ses limites, et Administrateur signifie administrateur de ce locataire.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Un compte peut être dans le locataire où se trouve son domaine. Dans un locataire, il est limité par le rôle du locataire et compte dans ses limites, et Administrateur signifie administrateur de ce locataire.",
|
||||||
"Tenants": "Locataires",
|
"Tenants": "Locataires",
|
||||||
"Storage in GB": "Stockage en Go",
|
"Storage in GB": "Stockage en Go",
|
||||||
"Default tenant roles": "Rôles de locataire par défaut",
|
"Default tenant roles": "Rôles de locataire par défaut",
|
||||||
@@ -281,13 +281,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "Retirer {domain} du locataire",
|
"Take {domain} out of the tenant": "Retirer {domain} du locataire",
|
||||||
"No domains in this tenant yet": "Aucun domaine dans ce locataire pour l’instant",
|
"No domains in this tenant yet": "Aucun domaine dans ce locataire pour l’instant",
|
||||||
"Domain to add": "Domaine à ajouter",
|
"Domain to add": "Domaine à ajouter",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Seuls les domaines hors de tout locataire peuvent être ajoutés. Les comptes déjà sur un domaine restent où ils sont ; déplacez chacun depuis son propre panneau.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Seuls les domaines hors de tout locataire peuvent être ajoutés, et les comptes déjà dessus restent où ils sont. Un domaine ne peut être retiré qu’une fois qu’aucun compte de ce locataire n’y est plus.",
|
||||||
"An empty tenant can be deleted.": "Un locataire vide peut être supprimé.",
|
"An empty tenant can be deleted.": "Un locataire vide peut être supprimé.",
|
||||||
"Delete tenant…": "Supprimer le locataire…",
|
"Delete tenant…": "Supprimer le locataire…",
|
||||||
"Still holds {things}. Move them out first.": "Contient encore {things}. Déplacez-les d’abord.",
|
"Still holds {things}. Move them out first.": "Contient encore {things}. Déplacez-les d’abord.",
|
||||||
"Delete tenant": "Supprimer le locataire",
|
"Delete tenant": "Supprimer le locataire",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Des organisations distinctes sur un même serveur, chacune avec ses personnes, ses domaines et ses limites.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Des organisations distinctes sur un même serveur, chacune avec ses personnes, ses domaines et ses limites.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Les locataires sont une fonctionnalité de Stalwart Enterprise. Ce serveur ne se déclare pas Enterprise : toute personne dans un locataire n’a donc que les autorisations d’un utilisateur ordinaire.",
|
"Tenants are a Stalwart Enterprise feature.": "Les locataires sont une fonctionnalité de Stalwart Enterprise.",
|
||||||
"Search tenants": "Rechercher des locataires",
|
"Search tenants": "Rechercher des locataires",
|
||||||
"No tenants match": "Aucun locataire ne correspond",
|
"No tenants match": "Aucun locataire ne correspond",
|
||||||
"No tenants yet": "Aucun locataire pour l’instant",
|
"No tenants yet": "Aucun locataire pour l’instant",
|
||||||
@@ -1694,6 +1694,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Accorde {n} autorisation", other: "Accorde {n} autorisations" },
|
"Grants {n} permissions": { one: "Accorde {n} autorisation", other: "Accorde {n} autorisations" },
|
||||||
"{n} roles": { one: "{n} rôle", other: "{n} rôles" },
|
"{n} roles": { one: "{n} rôle", other: "{n} rôles" },
|
||||||
"{n} tenants": { one: "{n} locataire", other: "{n} locataires" },
|
"{n} tenants": { one: "{n} locataire", other: "{n} locataires" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} compte de ce locataire est encore sur {domain}. Déplacez-le ou supprimez-le avant de retirer le domaine.", other: "{n} comptes de ce locataire sont encore sur {domain}. Déplacez-les ou supprimez-les avant de retirer le domaine." },
|
||||||
"{n} DKIM keys": { one: "{n} clé DKIM", other: "{n} clés DKIM" },
|
"{n} DKIM keys": { one: "{n} clé DKIM", other: "{n} clés DKIM" },
|
||||||
"{n} other items": { one: "{n} autre élément", other: "{n} autres éléments" },
|
"{n} other items": { one: "{n} autre élément", other: "{n} autres éléments" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "ロールが作成されたかどうか、サーバーから返答がありませんでした。",
|
"The server did not say whether the role was created.": "ロールが作成されたかどうか、サーバーから返答がありませんでした。",
|
||||||
"No tenant": "テナントなし",
|
"No tenant": "テナントなし",
|
||||||
"You can't move your own account into a tenant.": "自分のアカウントをテナントに移すことはできません。",
|
"You can't move your own account into a tenant.": "自分のアカウントをテナントに移すことはできません。",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "テナント内のアカウントは、テナントのロールによって制限され、テナントの上限に数えられます。また、管理者はそのテナントの管理者を意味します。",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "アカウントは、そのドメインが属するテナントにのみ入れられます。テナント内のアカウントは、テナントのロールによって制限され、テナントの上限に数えられます。また、管理者はそのテナントの管理者を意味します。",
|
||||||
"Tenants": "テナント",
|
"Tenants": "テナント",
|
||||||
"Storage in GB": "ストレージ (GB)",
|
"Storage in GB": "ストレージ (GB)",
|
||||||
"Default tenant roles": "テナントの既定ロール",
|
"Default tenant roles": "テナントの既定ロール",
|
||||||
@@ -275,13 +275,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "{domain} をテナントから外す",
|
"Take {domain} out of the tenant": "{domain} をテナントから外す",
|
||||||
"No domains in this tenant yet": "このテナントにはまだドメインがありません",
|
"No domains in this tenant yet": "このテナントにはまだドメインがありません",
|
||||||
"Domain to add": "追加するドメイン",
|
"Domain to add": "追加するドメイン",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "追加できるのは、どのテナントにも属していないドメインだけです。ドメイン上の既存のアカウントはそのまま残るため、それぞれのパネルから移動してください。",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "追加できるのは、どのテナントにも属していないドメインだけで、既存のアカウントはそのまま残ります。ドメインを外せるのは、このテナントのアカウントがそのドメインに 1 つも残っていないときだけです。",
|
||||||
"An empty tenant can be deleted.": "空のテナントは削除できます。",
|
"An empty tenant can be deleted.": "空のテナントは削除できます。",
|
||||||
"Delete tenant…": "テナントを削除…",
|
"Delete tenant…": "テナントを削除…",
|
||||||
"Still holds {things}. Move them out first.": "まだ {things} が含まれています。先に移動してください。",
|
"Still holds {things}. Move them out first.": "まだ {things} が含まれています。先に移動してください。",
|
||||||
"Delete tenant": "テナントを削除",
|
"Delete tenant": "テナントを削除",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "1 台のサーバー上の別々の組織で、それぞれに利用者、ドメイン、上限があります。",
|
"Separate organisations on one server, each with its own people, domains and limits.": "1 台のサーバー上の別々の組織で、それぞれに利用者、ドメイン、上限があります。",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "テナントは Stalwart Enterprise の機能です。このサーバーは Enterprise と報告していないため、テナント内の利用者は通常のユーザーの権限しか持ちません。",
|
"Tenants are a Stalwart Enterprise feature.": "テナントは Stalwart Enterprise の機能です。",
|
||||||
"Search tenants": "テナントを検索",
|
"Search tenants": "テナントを検索",
|
||||||
"No tenants match": "一致するテナントはありません",
|
"No tenants match": "一致するテナントはありません",
|
||||||
"No tenants yet": "まだテナントがありません",
|
"No tenants yet": "まだテナントがありません",
|
||||||
@@ -1697,6 +1697,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { other: "{n} 件の権限を付与" },
|
"Grants {n} permissions": { other: "{n} 件の権限を付与" },
|
||||||
"{n} roles": { other: "{n} 件のロール" },
|
"{n} roles": { other: "{n} 件のロール" },
|
||||||
"{n} tenants": { other: "{n} 件のテナント" },
|
"{n} tenants": { other: "{n} 件のテナント" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { other: "このテナントのアカウントがまだ {n} 件 {domain} にあります。ドメインを外す前に、移動するか削除してください。" },
|
||||||
"{n} DKIM keys": { other: "{n} 個の DKIM 鍵" },
|
"{n} DKIM keys": { other: "{n} 個の DKIM 鍵" },
|
||||||
"{n} other items": { other: "その他 {n} 件" },
|
"{n} other items": { other: "その他 {n} 件" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "De server heeft niet gemeld of de rol is aangemaakt.",
|
"The server did not say whether the role was created.": "De server heeft niet gemeld of de rol is aangemaakt.",
|
||||||
"No tenant": "Geen tenant",
|
"No tenant": "Geen tenant",
|
||||||
"You can't move your own account into a tenant.": "U kunt uw eigen account niet naar een tenant verplaatsen.",
|
"You can't move your own account into a tenant.": "U kunt uw eigen account niet naar een tenant verplaatsen.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Een account in een tenant wordt beperkt door de rol van de tenant en telt mee voor de limieten, en Beheerder betekent beheerder van die tenant.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Een account kan in de tenant zitten waarin zijn domein zit. In een tenant wordt het beperkt door de rol van de tenant en telt het mee voor de limieten, en Beheerder betekent beheerder van die tenant.",
|
||||||
"Tenants": "Tenants",
|
"Tenants": "Tenants",
|
||||||
"Storage in GB": "Opslag in GB",
|
"Storage in GB": "Opslag in GB",
|
||||||
"Default tenant roles": "Standaardrollen voor tenants",
|
"Default tenant roles": "Standaardrollen voor tenants",
|
||||||
@@ -272,13 +272,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "{domain} uit de tenant halen",
|
"Take {domain} out of the tenant": "{domain} uit de tenant halen",
|
||||||
"No domains in this tenant yet": "Nog geen domeinen in deze tenant",
|
"No domains in this tenant yet": "Nog geen domeinen in deze tenant",
|
||||||
"Domain to add": "Toe te voegen domein",
|
"Domain to add": "Toe te voegen domein",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Alleen domeinen die in geen enkele tenant zitten kunnen worden toegevoegd. De accounts op een domein blijven waar ze zijn; verplaats elk vanuit het eigen paneel.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Alleen domeinen die in geen enkele tenant zitten kunnen worden toegevoegd, en de accounts die er al op staan blijven waar ze zijn. Een domein kan er pas uit als geen enkel account van deze tenant er nog op staat.",
|
||||||
"An empty tenant can be deleted.": "Een lege tenant kan worden verwijderd.",
|
"An empty tenant can be deleted.": "Een lege tenant kan worden verwijderd.",
|
||||||
"Delete tenant…": "Tenant verwijderen…",
|
"Delete tenant…": "Tenant verwijderen…",
|
||||||
"Still holds {things}. Move them out first.": "Bevat nog {things}. Verplaats die eerst.",
|
"Still holds {things}. Move them out first.": "Bevat nog {things}. Verplaats die eerst.",
|
||||||
"Delete tenant": "Tenant verwijderen",
|
"Delete tenant": "Tenant verwijderen",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Afzonderlijke organisaties op één server, elk met eigen mensen, domeinen en limieten.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Afzonderlijke organisaties op één server, elk met eigen mensen, domeinen en limieten.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Tenants zijn een functie van Stalwart Enterprise. Deze server meldt geen Enterprise, dus iedereen in een tenant heeft alleen de rechten van een gewone gebruiker.",
|
"Tenants are a Stalwart Enterprise feature.": "Tenants zijn een functie van Stalwart Enterprise.",
|
||||||
"Search tenants": "Tenants zoeken",
|
"Search tenants": "Tenants zoeken",
|
||||||
"No tenants match": "Geen tenants gevonden",
|
"No tenants match": "Geen tenants gevonden",
|
||||||
"No tenants yet": "Nog geen tenants",
|
"No tenants yet": "Nog geen tenants",
|
||||||
@@ -1685,6 +1685,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Kent {n} recht toe", other: "Kent {n} rechten toe" },
|
"Grants {n} permissions": { one: "Kent {n} recht toe", other: "Kent {n} rechten toe" },
|
||||||
"{n} roles": { one: "{n} rol", other: "{n} rollen" },
|
"{n} roles": { one: "{n} rol", other: "{n} rollen" },
|
||||||
"{n} tenants": { one: "{n} tenant", other: "{n} tenants" },
|
"{n} tenants": { one: "{n} tenant", other: "{n} tenants" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} account van deze tenant staat nog op {domain}. Verplaats of verwijder het voordat u het domein eruit haalt.", other: "{n} accounts van deze tenant staan nog op {domain}. Verplaats of verwijder ze voordat u het domein eruit haalt." },
|
||||||
"{n} DKIM keys": { one: "{n} DKIM-sleutel", other: "{n} DKIM-sleutels" },
|
"{n} DKIM keys": { one: "{n} DKIM-sleutel", other: "{n} DKIM-sleutels" },
|
||||||
"{n} other items": { one: "{n} ander item", other: "{n} andere items" },
|
"{n} other items": { one: "{n} ander item", other: "{n} andere items" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "O servidor não informou se a função foi criada.",
|
"The server did not say whether the role was created.": "O servidor não informou se a função foi criada.",
|
||||||
"No tenant": "Nenhum locatário",
|
"No tenant": "Nenhum locatário",
|
||||||
"You can't move your own account into a tenant.": "Você não pode mover sua própria conta para um locatário.",
|
"You can't move your own account into a tenant.": "Você não pode mover sua própria conta para um locatário.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Uma conta em um locatário é limitada pela função do locatário e conta para os limites dele, e Administrador significa administrador desse locatário.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Uma conta pode estar no locatário em que está o seu domínio. Num locatário, ela é limitada pela função do locatário e conta para os limites dele, e Administrador significa administrador desse locatário.",
|
||||||
"Tenants": "Locatários",
|
"Tenants": "Locatários",
|
||||||
"Storage in GB": "Armazenamento em GB",
|
"Storage in GB": "Armazenamento em GB",
|
||||||
"Default tenant roles": "Funções padrão de locatário",
|
"Default tenant roles": "Funções padrão de locatário",
|
||||||
@@ -279,13 +279,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "Retirar {domain} do locatário",
|
"Take {domain} out of the tenant": "Retirar {domain} do locatário",
|
||||||
"No domains in this tenant yet": "Nenhum domínio neste locatário ainda",
|
"No domains in this tenant yet": "Nenhum domínio neste locatário ainda",
|
||||||
"Domain to add": "Domínio a adicionar",
|
"Domain to add": "Domínio a adicionar",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Só podem ser adicionados domínios que não estejam em nenhum locatário. As contas já existentes num domínio ficam onde estão; mova cada uma pelo próprio painel.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Só podem ser adicionados domínios que não estejam em nenhum locatário, e as contas já existentes num deles ficam onde estão. Um domínio só pode ser retirado quando nenhuma conta deste locatário estiver nele.",
|
||||||
"An empty tenant can be deleted.": "Um locatário vazio pode ser excluído.",
|
"An empty tenant can be deleted.": "Um locatário vazio pode ser excluído.",
|
||||||
"Delete tenant…": "Excluir locatário…",
|
"Delete tenant…": "Excluir locatário…",
|
||||||
"Still holds {things}. Move them out first.": "Ainda tem {things}. Mova-os primeiro.",
|
"Still holds {things}. Move them out first.": "Ainda tem {things}. Mova-os primeiro.",
|
||||||
"Delete tenant": "Excluir locatário",
|
"Delete tenant": "Excluir locatário",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Organizações separadas em um mesmo servidor, cada uma com suas próprias pessoas, domínios e limites.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Organizações separadas em um mesmo servidor, cada uma com suas próprias pessoas, domínios e limites.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Locatários são um recurso do Stalwart Enterprise. Este servidor não informa ser Enterprise, então qualquer pessoa num locatário tem só as permissões de um usuário comum.",
|
"Tenants are a Stalwart Enterprise feature.": "Locatários são um recurso do Stalwart Enterprise.",
|
||||||
"Search tenants": "Pesquisar locatários",
|
"Search tenants": "Pesquisar locatários",
|
||||||
"No tenants match": "Nenhum locatário corresponde",
|
"No tenants match": "Nenhum locatário corresponde",
|
||||||
"No tenants yet": "Nenhum locatário ainda",
|
"No tenants yet": "Nenhum locatário ainda",
|
||||||
@@ -1692,6 +1692,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Concede {n} permissão", other: "Concede {n} permissões" },
|
"Grants {n} permissions": { one: "Concede {n} permissão", other: "Concede {n} permissões" },
|
||||||
"{n} roles": { one: "{n} função", other: "{n} funções" },
|
"{n} roles": { one: "{n} função", other: "{n} funções" },
|
||||||
"{n} tenants": { one: "{n} locatário", other: "{n} locatários" },
|
"{n} tenants": { one: "{n} locatário", other: "{n} locatários" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} conta deste locatário ainda está em {domain}. Mova-a ou exclua-a antes de retirar o domínio.", other: "{n} contas deste locatário ainda estão em {domain}. Mova-as ou exclua-as antes de retirar o domínio." },
|
||||||
"{n} DKIM keys": { one: "{n} chave DKIM", other: "{n} chaves DKIM" },
|
"{n} DKIM keys": { one: "{n} chave DKIM", other: "{n} chaves DKIM" },
|
||||||
"{n} other items": { one: "{n} outro item", other: "{n} outros itens" },
|
"{n} other items": { one: "{n} outro item", other: "{n} outros itens" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "Сервер не сообщил, создана ли роль.",
|
"The server did not say whether the role was created.": "Сервер не сообщил, создана ли роль.",
|
||||||
"No tenant": "Без арендатора",
|
"No tenant": "Без арендатора",
|
||||||
"You can't move your own account into a tenant.": "Нельзя переместить собственную учётную запись в арендатора.",
|
"You can't move your own account into a tenant.": "Нельзя переместить собственную учётную запись в арендатора.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Учётная запись арендатора ограничена ролью арендатора и учитывается в его лимитах, а «Администратор» означает администратора этого арендатора.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Учётная запись может быть в том арендаторе, в котором её домен. В арендаторе она ограничена его ролью и учитывается в его лимитах, а «Администратор» означает администратора этого арендатора.",
|
||||||
"Tenants": "Арендаторы",
|
"Tenants": "Арендаторы",
|
||||||
"Storage in GB": "Хранилище, ГБ",
|
"Storage in GB": "Хранилище, ГБ",
|
||||||
"Default tenant roles": "Роли арендатора по умолчанию",
|
"Default tenant roles": "Роли арендатора по умолчанию",
|
||||||
@@ -278,13 +278,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "Убрать {domain} из арендатора",
|
"Take {domain} out of the tenant": "Убрать {domain} из арендатора",
|
||||||
"No domains in this tenant yet": "У этого арендатора пока нет доменов",
|
"No domains in this tenant yet": "У этого арендатора пока нет доменов",
|
||||||
"Domain to add": "Домен для добавления",
|
"Domain to add": "Домен для добавления",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Добавить можно только домены, не принадлежащие ни одному арендатору. Учётные записи на домене остаются на месте; переносите каждую из её собственной панели.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Добавить можно только домены, не принадлежащие ни одному арендатору, а учётные записи на них остаются на месте. Домен можно убрать, только когда на нём не осталось учётных записей этого арендатора.",
|
||||||
"An empty tenant can be deleted.": "Пустого арендатора можно удалить.",
|
"An empty tenant can be deleted.": "Пустого арендатора можно удалить.",
|
||||||
"Delete tenant…": "Удалить арендатора…",
|
"Delete tenant…": "Удалить арендатора…",
|
||||||
"Still holds {things}. Move them out first.": "Ещё содержит: {things}. Сначала перенесите их.",
|
"Still holds {things}. Move them out first.": "Ещё содержит: {things}. Сначала перенесите их.",
|
||||||
"Delete tenant": "Удалить арендатора",
|
"Delete tenant": "Удалить арендатора",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Отдельные организации на одном сервере, у каждой свои люди, домены и лимиты.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Отдельные организации на одном сервере, у каждой свои люди, домены и лимиты.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Арендаторы — функция Stalwart Enterprise. Этот сервер не сообщает о редакции Enterprise, поэтому у любого в арендаторе только разрешения обычного пользователя.",
|
"Tenants are a Stalwart Enterprise feature.": "Арендаторы — функция Stalwart Enterprise.",
|
||||||
"Search tenants": "Поиск арендаторов",
|
"Search tenants": "Поиск арендаторов",
|
||||||
"No tenants match": "Нет подходящих арендаторов",
|
"No tenants match": "Нет подходящих арендаторов",
|
||||||
"No tenants yet": "Арендаторов пока нет",
|
"No tenants yet": "Арендаторов пока нет",
|
||||||
@@ -1691,6 +1691,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Даёт {n} разрешение", few: "Даёт {n} разрешения", many: "Даёт {n} разрешений", other: "Даёт {n} разрешения" },
|
"Grants {n} permissions": { one: "Даёт {n} разрешение", few: "Даёт {n} разрешения", many: "Даёт {n} разрешений", other: "Даёт {n} разрешения" },
|
||||||
"{n} roles": { one: "{n} роль", few: "{n} роли", many: "{n} ролей", other: "{n} роли" },
|
"{n} roles": { one: "{n} роль", few: "{n} роли", many: "{n} ролей", other: "{n} роли" },
|
||||||
"{n} tenants": { one: "{n} арендатор", few: "{n} арендатора", many: "{n} арендаторов", other: "{n} арендатора" },
|
"{n} tenants": { one: "{n} арендатор", few: "{n} арендатора", many: "{n} арендаторов", other: "{n} арендатора" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} учётная запись этого арендатора ещё на {domain}. Перенесите или удалите её, прежде чем убирать домен.", few: "{n} учётные записи этого арендатора ещё на {domain}. Перенесите или удалите их, прежде чем убирать домен.", many: "{n} учётных записей этого арендатора ещё на {domain}. Перенесите или удалите их, прежде чем убирать домен.", other: "{n} учётной записи этого арендатора ещё на {domain}. Перенесите или удалите их, прежде чем убирать домен." },
|
||||||
"{n} DKIM keys": { one: "{n} ключ DKIM", few: "{n} ключа DKIM", many: "{n} ключей DKIM", other: "{n} ключа DKIM" },
|
"{n} DKIM keys": { one: "{n} ключ DKIM", few: "{n} ключа DKIM", many: "{n} ключей DKIM", other: "{n} ключа DKIM" },
|
||||||
"{n} other items": { one: "{n} другой объект", few: "{n} других объекта", many: "{n} других объектов", other: "{n} другого объекта" },
|
"{n} other items": { one: "{n} другой объект", few: "{n} других объекта", many: "{n} других объектов", other: "{n} другого объекта" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "Сервер не повідомив, чи створено роль.",
|
"The server did not say whether the role was created.": "Сервер не повідомив, чи створено роль.",
|
||||||
"No tenant": "Без орендаря",
|
"No tenant": "Без орендаря",
|
||||||
"You can't move your own account into a tenant.": "Не можна перемістити власний обліковий запис до орендаря.",
|
"You can't move your own account into a tenant.": "Не можна перемістити власний обліковий запис до орендаря.",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Обліковий запис орендаря обмежений роллю орендаря й зараховується до його лімітів, а «Адміністратор» означає адміністратора цього орендаря.",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "Обліковий запис може бути в тому орендарі, у якому його домен. В орендарі він обмежений роллю орендаря й зараховується до його лімітів, а «Адміністратор» означає адміністратора цього орендаря.",
|
||||||
"Tenants": "Орендарі",
|
"Tenants": "Орендарі",
|
||||||
"Storage in GB": "Сховище, ГБ",
|
"Storage in GB": "Сховище, ГБ",
|
||||||
"Default tenant roles": "Ролі орендаря за замовчуванням",
|
"Default tenant roles": "Ролі орендаря за замовчуванням",
|
||||||
@@ -272,13 +272,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "Прибрати {domain} з орендаря",
|
"Take {domain} out of the tenant": "Прибрати {domain} з орендаря",
|
||||||
"No domains in this tenant yet": "У цього орендаря поки немає доменів",
|
"No domains in this tenant yet": "У цього орендаря поки немає доменів",
|
||||||
"Domain to add": "Домен для додавання",
|
"Domain to add": "Домен для додавання",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "Додати можна лише домени, що не належать жодному орендарю. Облікові записи на домені лишаються на місці; переносьте кожен з його власної панелі.",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "Додати можна лише домени, що не належать жодному орендарю, а облікові записи на них лишаються на місці. Домен можна прибрати, лише коли на ньому не лишилося облікових записів цього орендаря.",
|
||||||
"An empty tenant can be deleted.": "Порожнього орендаря можна видалити.",
|
"An empty tenant can be deleted.": "Порожнього орендаря можна видалити.",
|
||||||
"Delete tenant…": "Видалити орендаря…",
|
"Delete tenant…": "Видалити орендаря…",
|
||||||
"Still holds {things}. Move them out first.": "Ще містить: {things}. Спершу перенесіть їх.",
|
"Still holds {things}. Move them out first.": "Ще містить: {things}. Спершу перенесіть їх.",
|
||||||
"Delete tenant": "Видалити орендаря",
|
"Delete tenant": "Видалити орендаря",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "Окремі організації на одному сервері, кожна зі своїми людьми, доменами й лімітами.",
|
"Separate organisations on one server, each with its own people, domains and limits.": "Окремі організації на одному сервері, кожна зі своїми людьми, доменами й лімітами.",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "Орендарі — функція Stalwart Enterprise. Цей сервер не повідомляє про редакцію Enterprise, тож будь-хто в орендарі має лише дозволи звичайного користувача.",
|
"Tenants are a Stalwart Enterprise feature.": "Орендарі — функція Stalwart Enterprise.",
|
||||||
"Search tenants": "Пошук орендарів",
|
"Search tenants": "Пошук орендарів",
|
||||||
"No tenants match": "Немає відповідних орендарів",
|
"No tenants match": "Немає відповідних орендарів",
|
||||||
"No tenants yet": "Орендарів поки немає",
|
"No tenants yet": "Орендарів поки немає",
|
||||||
@@ -1685,6 +1685,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { one: "Надає {n} дозвіл", few: "Надає {n} дозволи", many: "Надає {n} дозволів", other: "Надає {n} дозволу" },
|
"Grants {n} permissions": { one: "Надає {n} дозвіл", few: "Надає {n} дозволи", many: "Надає {n} дозволів", other: "Надає {n} дозволу" },
|
||||||
"{n} roles": { one: "{n} роль", few: "{n} ролі", many: "{n} ролей", other: "{n} ролі" },
|
"{n} roles": { one: "{n} роль", few: "{n} ролі", many: "{n} ролей", other: "{n} ролі" },
|
||||||
"{n} tenants": { one: "{n} орендар", few: "{n} орендарі", many: "{n} орендарів", other: "{n} орендаря" },
|
"{n} tenants": { one: "{n} орендар", few: "{n} орендарі", many: "{n} орендарів", other: "{n} орендаря" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { one: "{n} обліковий запис цього орендаря ще на {domain}. Перенесіть або видаліть його, перш ніж прибирати домен.", few: "{n} облікові записи цього орендаря ще на {domain}. Перенесіть або видаліть їх, перш ніж прибирати домен.", many: "{n} облікових записів цього орендаря ще на {domain}. Перенесіть або видаліть їх, перш ніж прибирати домен.", other: "{n} облікового запису цього орендаря ще на {domain}. Перенесіть або видаліть їх, перш ніж прибирати домен." },
|
||||||
"{n} DKIM keys": { one: "{n} ключ DKIM", few: "{n} ключі DKIM", many: "{n} ключів DKIM", other: "{n} ключа DKIM" },
|
"{n} DKIM keys": { one: "{n} ключ DKIM", few: "{n} ключі DKIM", many: "{n} ключів DKIM", other: "{n} ключа DKIM" },
|
||||||
"{n} other items": { one: "{n} інший об'єкт", few: "{n} інші об'єкти", many: "{n} інших об'єктів", other: "{n} іншого об'єкта" },
|
"{n} other items": { one: "{n} інший об'єкт", few: "{n} інші об'єкти", many: "{n} інших об'єктів", other: "{n} іншого об'єкта" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const catalog: Catalog = {
|
|||||||
"The server did not say whether the role was created.": "服务器未说明角色是否已创建。",
|
"The server did not say whether the role was created.": "服务器未说明角色是否已创建。",
|
||||||
"No tenant": "无租户",
|
"No tenant": "无租户",
|
||||||
"You can't move your own account into a tenant.": "您不能将自己的账户移入租户。",
|
"You can't move your own account into a tenant.": "您不能将自己的账户移入租户。",
|
||||||
"An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "租户中的账户受租户角色限制,并计入租户的限额;管理员指该租户的管理员。",
|
"An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.": "账户只能位于其域名所属的租户中。在租户中,账户受租户角色限制,并计入租户的限额;管理员指该租户的管理员。",
|
||||||
"Tenants": "租户",
|
"Tenants": "租户",
|
||||||
"Storage in GB": "存储 (GB)",
|
"Storage in GB": "存储 (GB)",
|
||||||
"Default tenant roles": "默认租户角色",
|
"Default tenant roles": "默认租户角色",
|
||||||
@@ -274,13 +274,13 @@ export const catalog: Catalog = {
|
|||||||
"Take {domain} out of the tenant": "将 {domain} 移出租户",
|
"Take {domain} out of the tenant": "将 {domain} 移出租户",
|
||||||
"No domains in this tenant yet": "此租户中还没有域名",
|
"No domains in this tenant yet": "此租户中还没有域名",
|
||||||
"Domain to add": "要添加的域名",
|
"Domain to add": "要添加的域名",
|
||||||
"Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.": "只能添加不属于任何租户的域名。域名上已有的账户保持不变;请在各自的面板中移动。",
|
"Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.": "只能添加不属于任何租户的域名,其上已有的账户保持不变。只有当此租户在该域名上没有任何账户时,才能将域名移出。",
|
||||||
"An empty tenant can be deleted.": "空租户可以删除。",
|
"An empty tenant can be deleted.": "空租户可以删除。",
|
||||||
"Delete tenant…": "删除租户…",
|
"Delete tenant…": "删除租户…",
|
||||||
"Still holds {things}. Move them out first.": "仍包含 {things}。请先将其移出。",
|
"Still holds {things}. Move them out first.": "仍包含 {things}。请先将其移出。",
|
||||||
"Delete tenant": "删除租户",
|
"Delete tenant": "删除租户",
|
||||||
"Separate organisations on one server, each with its own people, domains and limits.": "同一服务器上相互独立的组织,各有自己的成员、域名和限额。",
|
"Separate organisations on one server, each with its own people, domains and limits.": "同一服务器上相互独立的组织,各有自己的成员、域名和限额。",
|
||||||
"Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.": "租户是 Stalwart Enterprise 的功能。此服务器未报告为 Enterprise,因此租户中的任何人都只有普通用户的权限。",
|
"Tenants are a Stalwart Enterprise feature.": "租户是 Stalwart Enterprise 的功能。",
|
||||||
"Search tenants": "搜索租户",
|
"Search tenants": "搜索租户",
|
||||||
"No tenants match": "没有匹配的租户",
|
"No tenants match": "没有匹配的租户",
|
||||||
"No tenants yet": "还没有租户",
|
"No tenants yet": "还没有租户",
|
||||||
@@ -1696,6 +1696,7 @@ export const catalog: Catalog = {
|
|||||||
"Grants {n} permissions": { other: "授予 {n} 项权限" },
|
"Grants {n} permissions": { other: "授予 {n} 项权限" },
|
||||||
"{n} roles": { other: "{n} 个角色" },
|
"{n} roles": { other: "{n} 个角色" },
|
||||||
"{n} tenants": { other: "{n} 个租户" },
|
"{n} tenants": { other: "{n} 个租户" },
|
||||||
|
"{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.": { other: "此租户仍有 {n} 个账户位于 {domain}。移出该域名前,请先移动或删除这些账户。" },
|
||||||
"{n} DKIM keys": { other: "{n} 个 DKIM 密钥" },
|
"{n} DKIM keys": { other: "{n} 个 DKIM 密钥" },
|
||||||
"{n} other items": { other: "其他 {n} 项" },
|
"{n} other items": { other: "其他 {n} 项" },
|
||||||
// ── Administration ────────────────────────────────────────────────
|
// ── Administration ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDe
|
|||||||
if (!domainId && ctx.domains[0]) setDomainId(ctx.domains[0].id);
|
if (!domainId && ctx.domains[0]) setDomainId(ctx.domains[0].id);
|
||||||
}, [ctx.domains, domainId]);
|
}, [ctx.domains, domainId]);
|
||||||
|
|
||||||
|
// A new account starts in the tenant of the domain it is being made on.
|
||||||
|
useEffect(() => {
|
||||||
|
if (creating) setTenantId(ctx.domains.find((d) => d.id === domainId)?.memberTenantId ?? "");
|
||||||
|
}, [creating, domainId, ctx.domains]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape" && !document.querySelector(".dialog-backdrop")) onClose();
|
if (e.key === "Escape" && !document.querySelector(".dialog-backdrop")) onClose();
|
||||||
@@ -102,6 +107,13 @@ export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDe
|
|||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
const domainName = (id: string) => ctx.domains.find((d) => d.id === id)?.name ?? "";
|
const domainName = (id: string) => ctx.domains.find((d) => d.id === id)?.name ?? "";
|
||||||
|
/*
|
||||||
|
* Stalwart refuses an account in a tenant on a domain outside it (live,
|
||||||
|
* 2026-09-15: invalidForeignKey naming the domain), and allows one in no
|
||||||
|
* tenant on a tenant's domain. So the only tenant to offer is the domain's.
|
||||||
|
*/
|
||||||
|
const domainTenant = ctx.domains.find((d) => d.id === (account?.domainId ?? domainId))?.memberTenantId ?? null;
|
||||||
|
const tenantName = (id: string) => ctx.tenants?.find((x) => x.id === id)?.name ?? id;
|
||||||
const address = account?.emailAddress ?? `${name}@${domainName(domainId)}`;
|
const address = account?.emailAddress ?? `${name}@${domainName(domainId)}`;
|
||||||
|
|
||||||
const roleOptions = useMemo(() => {
|
const roleOptions = useMemo(() => {
|
||||||
@@ -247,15 +259,19 @@ export function AccountSheet({ account, ctx, onClose, onChanged, onCreated, onDe
|
|||||||
{self ? t("You can't change your own role.") : t("Only roles whose permissions you hold yourself are offered. On an account inside a tenant, Administrator means administrator of that tenant.")}
|
{self ? t("You can't change your own role.") : t("Only roles whose permissions you hold yourself are offered. On an account inside a tenant, Administrator means administrator of that tenant.")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{ctx.tenants && (ctx.tenants.length > 0 || tenantId) && (
|
{ctx.tenants && (domainTenant || tenantId) && (
|
||||||
<>
|
<>
|
||||||
<h3>{t("Tenant")}</h3>
|
<h3>{t("Tenant")}</h3>
|
||||||
<select className="input admin-wide" aria-label={t("Tenant")} value={tenantId} disabled={!editable || self} onChange={(e) => setTenantId(e.target.value)}>
|
<select className="input admin-wide" aria-label={t("Tenant")} value={tenantId} disabled={!editable || self} onChange={(e) => setTenantId(e.target.value)}>
|
||||||
<option value="">{t("No tenant")}</option>
|
<option value="">{t("No tenant")}</option>
|
||||||
{ctx.tenants.map((x) => <option key={x.id} value={x.id}>{x.name}</option>)}
|
{domainTenant && <option value={domainTenant}>{tenantName(domainTenant)}</option>}
|
||||||
{tenantId && !ctx.tenants.some((x) => x.id === tenantId) && <option value={tenantId}>{tenantId}</option>}
|
{tenantId && tenantId !== domainTenant && <option value={tenantId}>{tenantName(tenantId)}</option>}
|
||||||
</select>
|
</select>
|
||||||
<p className="hint">{self ? t("You can't move your own account into a tenant.") : t("An account in a tenant is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.")}</p>
|
<p className="hint">
|
||||||
|
{self
|
||||||
|
? t("You can't move your own account into a tenant.")
|
||||||
|
: t("An account can be in the tenant its domain is in. In a tenant it is limited by the tenant's role and counts towards its limits, and Administrator means administrator of that tenant.")}
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
drawableLogo,
|
drawableLogo,
|
||||||
quotasPatch,
|
quotasPatch,
|
||||||
setDomainTenant,
|
setDomainTenant,
|
||||||
|
tenantAccountsOnDomain,
|
||||||
tenantDomains,
|
tenantDomains,
|
||||||
updateTenant,
|
updateTenant,
|
||||||
TENANT_MEMBERS,
|
TENANT_MEMBERS,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
} from "@/lib/adminTenants";
|
} from "@/lib/adminTenants";
|
||||||
import { formatSize } from "@/lib/format";
|
import { formatSize } from "@/lib/format";
|
||||||
import { proxiedImageUrl } from "@/lib/html";
|
import { proxiedImageUrl } from "@/lib/html";
|
||||||
import { t } from "@/lib/i18n";
|
import { plural, t } from "@/lib/i18n";
|
||||||
import { Dialog } from "@/ui/dialog";
|
import { Dialog } from "@/ui/dialog";
|
||||||
import { Spinner } from "@/ui/misc";
|
import { Spinner } from "@/ui/misc";
|
||||||
import { toast } from "@/ui/toast";
|
import { toast } from "@/ui/toast";
|
||||||
@@ -281,6 +282,16 @@ function TenantDomains({ tenant, canChange, onChanged }: { tenant: DirectoryTena
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
|
if (!into) {
|
||||||
|
const stranded = await tenantAccountsOnDomain(tenant.id, domain.id);
|
||||||
|
if (stranded > 0) {
|
||||||
|
setError(plural(stranded, {
|
||||||
|
one: "{n} account in this tenant is still on {domain}. Move it or delete it before taking the domain out.",
|
||||||
|
other: "{n} accounts in this tenant are still on {domain}. Move them or delete them before taking the domain out.",
|
||||||
|
}, { domain: domain.name }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
await setDomainTenant(domain.id, into ? tenant.id : null);
|
await setDomainTenant(domain.id, into ? tenant.id : null);
|
||||||
toast.success(into ? t("Added {domain} to {tenant}", { domain: domain.name, tenant: tenant.name }) : t("Took {domain} out of {tenant}", { domain: domain.name, tenant: tenant.name }));
|
toast.success(into ? t("Added {domain} to {tenant}", { domain: domain.name, tenant: tenant.name }) : t("Took {domain} out of {tenant}", { domain: domain.name, tenant: tenant.name }));
|
||||||
setRevision((n) => n + 1);
|
setRevision((n) => n + 1);
|
||||||
@@ -323,7 +334,7 @@ function TenantDomains({ tenant, canChange, onChanged }: { tenant: DirectoryTena
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && <p className="admin-notice error" role="alert">{error}</p>}
|
{error && <p className="admin-notice error" role="alert">{error}</p>}
|
||||||
<p className="hint">{t("Only domains in no tenant can be added. The accounts already on a domain stay where they are; move each from its own panel.")}</p>
|
<p className="hint">{t("Only domains in no tenant can be added, and the accounts already on one stay where they are. A domain comes out only once none of this tenant's accounts are on it.")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
|
|||||||
<p className="lead">{t("Separate organisations on one server, each with its own people, domains and limits.")}</p>
|
<p className="lead">{t("Separate organisations on one server, each with its own people, domains and limits.")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="admin-notice warn">{t("Tenants are a Stalwart Enterprise feature. This server does not report Enterprise, so anyone inside a tenant has only an ordinary user's permissions.")}</p>
|
<p className="admin-notice warn">{t("Tenants are a Stalwart Enterprise feature.")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,3 +92,45 @@ describe("the account sheet", () => {
|
|||||||
expect(button(host, "Delete account")?.disabled).toBe(true);
|
expect(button(host, "Delete account")?.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account in a tenant has to be on a domain in that tenant -- the live
|
||||||
|
* server refuses anything else -- so the only tenant offered is the domain's.
|
||||||
|
*/
|
||||||
|
describe("an account's tenant", () => {
|
||||||
|
let host: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
const tenantCtx: DirectoryContext = {
|
||||||
|
...ctx,
|
||||||
|
domains: [{ id: "d1", name: "example.com", memberTenantId: null }, { id: "d3", name: "acme.example", memberTenantId: "t1" }],
|
||||||
|
tenants: [{ id: "t1", name: "Acme Corp" }, { id: "t2", name: "Globex" }],
|
||||||
|
};
|
||||||
|
const render = async (a: DirectoryAccount) => {
|
||||||
|
const { hook } = memoryLocation({ path: `/admin/accounts/${a.id}` });
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<Router hook={hook}><AccountSheet account={a} ctx={tenantCtx} onClose={() => {}} onChanged={() => {}} onCreated={() => {}} onDeleted={() => {}} /></Router>);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
host = document.createElement("div");
|
||||||
|
document.body.appendChild(host);
|
||||||
|
root = createRoot(host);
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
await act(async () => root.unmount());
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers only the tenant its domain is in", async () => {
|
||||||
|
signIn([...HELPDESK, "sysTenantGet", "sysTenantQuery"]);
|
||||||
|
await render(account({ domainId: "d3", memberTenantId: "t1", emailAddress: "[email protected]" }));
|
||||||
|
const options = [...host.querySelectorAll<HTMLOptionElement>('select[aria-label="Tenant"] option')].map((o) => o.textContent);
|
||||||
|
expect(options).toEqual(["No tenant", "Acme Corp"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers no choice at all on a domain in no tenant", async () => {
|
||||||
|
signIn([...HELPDESK, "sysTenantGet", "sysTenantQuery"]);
|
||||||
|
await render(account({ domainId: "d1" }));
|
||||||
|
expect(host.querySelector('select[aria-label="Tenant"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import type { DirectoryTenant } from "@/lib/adminTenants";
|
|||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
const api = vi.hoisted(() => ({
|
const api = vi.hoisted(() => ({
|
||||||
counts: { accounts: 1, groups: 0, lists: 0, domains: 1, roles: 0 } as Record<string, number>,
|
counts: { accounts: 1, groups: 0, lists: 0, domains: 1, roles: 0, dkimKeys: 2 } as Record<string, number>,
|
||||||
|
onDomain: 0,
|
||||||
updateTenant: vi.fn(async () => {}),
|
updateTenant: vi.fn(async () => {}),
|
||||||
setDomainTenant: vi.fn(async () => {}),
|
setDomainTenant: vi.fn(async () => {}),
|
||||||
}));
|
}));
|
||||||
@@ -18,6 +19,7 @@ vi.mock("@/lib/adminTenants", async (original) => ({
|
|||||||
tenantDomains: vi.fn(async () => ({ inTenant: [{ id: "d3", name: "old-brand.example" }], unassigned: [{ id: "d4", name: "spare.example" }] })),
|
tenantDomains: vi.fn(async () => ({ inTenant: [{ id: "d3", name: "old-brand.example" }], unassigned: [{ id: "d4", name: "spare.example" }] })),
|
||||||
updateTenant: api.updateTenant,
|
updateTenant: api.updateTenant,
|
||||||
setDomainTenant: api.setDomainTenant,
|
setDomainTenant: api.setDomainTenant,
|
||||||
|
tenantAccountsOnDomain: vi.fn(async () => api.onDomain),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { TenantSheet } = await import("../TenantSheet");
|
const { TenantSheet } = await import("../TenantSheet");
|
||||||
@@ -49,7 +51,8 @@ describe("the tenant sheet", () => {
|
|||||||
root = createRoot(host);
|
root = createRoot(host);
|
||||||
api.updateTenant.mockClear();
|
api.updateTenant.mockClear();
|
||||||
api.setDomainTenant.mockClear();
|
api.setDomainTenant.mockClear();
|
||||||
api.counts = { accounts: 1, groups: 0, lists: 0, domains: 1, roles: 0 };
|
api.counts = { accounts: 1, groups: 0, lists: 0, domains: 1, roles: 0, dkimKeys: 2 };
|
||||||
|
api.onDomain = 0;
|
||||||
});
|
});
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await act(async () => root.unmount());
|
await act(async () => root.unmount());
|
||||||
@@ -64,7 +67,7 @@ describe("the tenant sheet", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("offers the delete once it is empty", async () => {
|
it("offers the delete once it is empty", async () => {
|
||||||
api.counts = { accounts: 0, groups: 0, lists: 0, domains: 0, roles: 0 };
|
api.counts = { accounts: 0, groups: 0, lists: 0, domains: 0, roles: 0, dkimKeys: 0 };
|
||||||
signIn(ALL);
|
signIn(ALL);
|
||||||
await render();
|
await render();
|
||||||
expect(button(host, "Delete tenant…")?.disabled).toBe(false);
|
expect(button(host, "Delete tenant…")?.disabled).toBe(false);
|
||||||
@@ -92,3 +95,30 @@ describe("the tenant sheet", () => {
|
|||||||
expect(button(host, "Take old-brand.example out of the tenant")).toBeUndefined();
|
expect(button(host, "Take old-brand.example out of the tenant")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("taking a domain out of a tenant", () => {
|
||||||
|
let host: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
beforeEach(() => {
|
||||||
|
host = document.createElement("div");
|
||||||
|
document.body.appendChild(host);
|
||||||
|
root = createRoot(host);
|
||||||
|
api.setDomainTenant.mockClear();
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
await act(async () => root.unmount());
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is refused while the tenant still has accounts on it, which Stalwart would strand", async () => {
|
||||||
|
api.onDomain = 2;
|
||||||
|
signIn(ALL);
|
||||||
|
await act(async () => {
|
||||||
|
root.render(<TenantSheet tenant={tenant} roles={new Map()} onClose={() => {}} onChanged={() => {}} onCreated={() => {}} onDeleted={() => {}} />);
|
||||||
|
});
|
||||||
|
await act(async () => {});
|
||||||
|
await act(async () => button(host, "Take old-brand.example out of the tenant")!.click());
|
||||||
|
expect(api.setDomainTenant).not.toHaveBeenCalled();
|
||||||
|
expect(host.querySelector(".admin-notice.error")?.textContent).toContain("2 accounts in this tenant are still on old-brand.example");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user