From 6ba89696ee240ed9dd6ea64e309399d70e40ac9e Mon Sep 17 00:00:00 2001
From: John Coffey
Date: Tue, 15 Sep 2026 11:35:33 -0700
Subject: [PATCH 1/2] Spell license the US way
---
FEATURES.md | 2 +-
NOTICE | 24 +++++++++----------
README.md | 2 +-
server/src/config.ts | 2 +-
web/src/lib/adminDirectory.ts | 2 +-
web/src/locales/de.ts | 4 ++--
web/src/locales/es.ts | 4 ++--
web/src/locales/fr.ts | 4 ++--
web/src/locales/ja.ts | 4 ++--
web/src/locales/nl.ts | 4 ++--
web/src/locales/pt-BR.ts | 4 ++--
web/src/locales/ru.ts | 4 ++--
web/src/locales/uk.ts | 4 ++--
web/src/locales/zh-Hans.ts | 4 ++--
web/src/views/admin/TenantsAdmin.tsx | 2 +-
web/src/views/settings/AppearanceSettings.tsx | 2 +-
16 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/FEATURES.md b/FEATURES.md
index c250e5a..bd4c5c2 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -1067,7 +1067,7 @@ Every one has both halves, so the top-bar toggle only ever changes the side and
never the colours. Accent colours still sit on top of any of them.
The ten borrowed palettes are the work of their own projects and are used
-under the MIT licence — see [NOTICE](NOTICE). Only the published colour values
+under the MIT license — see [NOTICE](NOTICE). Only the published colour values
are used, taken from each project's own repository; the values as fetched are
recorded in `.palette-sources/palettes-upstream.md`.
diff --git a/NOTICE b/NOTICE
index 6edaa5e..1cb2153 100644
--- a/NOTICE
+++ b/NOTICE
@@ -6,7 +6,7 @@ other people that ships inside it and the terms it comes under.
## Colour palettes
Ten of the palettes offered in Settings › Appearance are the work of their own
-projects and are used under the MIT licence. Only the published colour values
+projects and are used under the MIT license. Only the published colour values
are used — no code, and nothing from anyone else's reimplementation of them.
The values as fetched from each project are recorded in
`.palette-sources/palettes-upstream.md`, and the shades between them are
@@ -16,66 +16,66 @@ not meet the contrast ihasmail claims.
### Dracula and Alucard
Copyright (c) 2016 Dracula Theme — https://github.com/dracula/dracula-theme
-Licensed under the MIT licence. "Dracula" is the dark variant and "Alucard" the
+Licensed under the MIT license. "Dracula" is the dark variant and "Alucard" the
light one; both are published in that repository's own "Color Palette (OSS)"
section.
### Gruvbox
Copyright (c) 2018 Pavel Pertsev — https://github.com/morhetz/gruvbox
-Licensed under the MIT licence.
+Licensed under the MIT license.
### Rosé Pine
Copyright (c) 2021 Rosé Pine — https://github.com/rose-pine/rose-pine-theme
-Licensed under the MIT licence. The light variant is "Dawn".
+Licensed under the MIT license. The light variant is "Dawn".
### Tokyo Night
Copyright (c) 2019 enkia — https://github.com/enkia/tokyo-night-vscode-theme
-Licensed under the MIT licence. The light variant is "Day".
+Licensed under the MIT license. The light variant is "Day".
### Catppuccin
Copyright (c) 2021 Catppuccin — https://github.com/catppuccin/palette
-Licensed under the MIT licence. "Mocha" is the dark variant and "Latte" the
+Licensed under the MIT license. "Mocha" is the dark variant and "Latte" the
light one; both are published in that repository's palette.json.
### Solarized
Copyright (c) 2011 Ethan Schoonover — https://github.com/altercation/solarized
-Licensed under the MIT licence. Light and dark are both original to it, and
+Licensed under the MIT license. Light and dark are both original to it, and
share one set of accent values by design.
### Ayu
Copyright (c) Konstantin Pschera — https://github.com/ayu-theme/ayu-colors
-Licensed under the MIT licence. The two signature accent colours come from the
+Licensed under the MIT license. The two signature accent colours come from the
same author's ayu-theme/vscode-ayu, also MIT.
### Kanagawa
Copyright (c) 2021 Tommaso Laurenzi — https://github.com/rebelot/kanagawa.nvim
-Licensed under the MIT licence. "Wave" is the dark variant and "Lotus" the
+Licensed under the MIT license. "Wave" is the dark variant and "Lotus" the
light one. The theme takes its name from Hokusai's print.
### Everforest
Copyright (c) 2019 Sainnhe Park — https://github.com/sainnhe/everforest
-Licensed under the MIT licence. The medium-contrast variant of each mode is
+Licensed under the MIT license. The medium-contrast variant of each mode is
the one used here.
### Primer
Copyright (c) GitHub, Inc. — https://github.com/primer/primitives
-Licensed under the MIT licence, which covers the colour values. "GitHub" and
+Licensed under the MIT license, which covers the colour values. "GitHub" and
the Invertocat logo are trademarks of GitHub, Inc.; this palette is named
"Primer" after the design system and is neither affiliated with nor endorsed
by GitHub.
---
-The MIT licence, under which all ten are used:
+The MIT license, under which all ten are used:
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
diff --git a/README.md b/README.md
index 11af239..0518088 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/server/src/config.ts b/server/src/config.ts
index 1bd9281..5823c5a 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -311,7 +311,7 @@ export const config = {
* Say that an Enterprise-only section is Enterprise-only even on an
* Enterprise server. Off, as a real installation wants it; the public demo
* turns it on, because it reports Enterprise to show those sections and
- * should not suggest they come without the licence.
+ * should not suggest they come without the license.
*/
showEnterpriseNotices: bool("SHOW_ENTERPRISE_NOTICES", false),
appSecret,
diff --git a/web/src/lib/adminDirectory.ts b/web/src/lib/adminDirectory.ts
index a11a311..e952ac0 100644
--- a/web/src/lib/adminDirectory.ts
+++ b/web/src/lib/adminDirectory.ts
@@ -272,7 +272,7 @@ export function describeDirectoryError(err: unknown, object: DirectoryObject = "
return object === "role" ? t("You can't give a role permissions your own role doesn't have.") : t("You can't give an account permissions your own role doesn't have.");
}
if (/external directory/i.test(description)) return t("This account signs in through an external directory, so its password can't be set here.");
- if (/licen[cs]ed account limit/i.test(description)) return t("The server's licence allows no more accounts.");
+ if (/licen[cs]ed account limit/i.test(description)) return t("The server's license allows no more accounts.");
return t("The mail server refused this. Your role may not allow it.");
case "primaryKeyViolation":
return object === "domain"
diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts
index 60fccfd..c03c31c 100644
--- a/web/src/locales/de.ts
+++ b/web/src/locales/de.ts
@@ -122,7 +122,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "Der Mailserver konnte die Anfrage nicht ausführen ({code}).",
"You can't give an account permissions your own role doesn't have.": "Sie können einem Konto keine Berechtigungen geben, die Ihre eigene Rolle nicht hat.",
"This account signs in through an external directory, so its password can't be set here.": "Dieses Konto meldet sich über ein externes Verzeichnis an, daher kann sein Passwort hier nicht festgelegt werden.",
- "The server's licence allows no more accounts.": "Die Lizenz des Servers erlaubt keine weiteren Konten.",
+ "The server's license allows no more accounts.": "Die Lizenz des Servers erlaubt keine weiteren Konten.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Dieser Domainname wird auf diesem Server bereits verwendet – als Domain oder als weiterer Name einer anderen Domain.",
"Your organisation has reached the number of domains it is allowed.": "Ihre Organisation hat die Anzahl der erlaubten Domains erreicht.",
"That is more than the mail server accepts in one change.": "Das ist mehr, als der Mailserver in einer Änderung annimmt.",
@@ -1482,7 +1482,7 @@ export const catalog: Catalog = {
"Counts people rather than headers, so one address in To and nine in Cc is a message to ten. Catches a reply-all onto a long thread.": "Zählt Personen statt Kopfzeilen: eine Adresse in An und neun in Cc ergeben eine Nachricht an zehn. Erfasst ein Allen-Antworten auf einen langen Thread.",
"Date received": "Empfangsdatum",
"Date sent": "Sendedatum",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Paletten, die nach einem anderen Projekt benannt sind, stammen von diesem Projekt und werden unter dessen eigener Lizenz verwendet; die Abstufungen zwischen den veröffentlichten Farben sind abgeleitet, und jede davon wird auf Kontrast geprüft. Die Akzentfarbe unten gilt weiterhin über jeder von ihnen.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Paletten, die nach einem anderen Projekt benannt sind, stammen von diesem Projekt und werden unter dessen eigener Lizenz verwendet; die Abstufungen zwischen den veröffentlichten Farben sind abgeleitet, und jede davon wird auf Kontrast geprüft. Die Akzentfarbe unten gilt weiterhin über jeder von ihnen.",
"Earlier": "Früher",
"Every folder": "Jeder Ordner",
"Everyone addressed will receive this.": "Alle Adressierten erhalten dies.",
diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts
index 0015cca..2aa4f1b 100644
--- a/web/src/locales/es.ts
+++ b/web/src/locales/es.ts
@@ -114,7 +114,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "El servidor de correo no pudo completar la solicitud ({code}).",
"You can't give an account permissions your own role doesn't have.": "No puede dar a una cuenta permisos que su propio rol no tiene.",
"This account signs in through an external directory, so its password can't be set here.": "Esta cuenta inicia sesión mediante un directorio externo, así que su contraseña no se puede establecer aquí.",
- "The server's licence allows no more accounts.": "La licencia del servidor no permite más cuentas.",
+ "The server's license allows no more accounts.": "La licencia del servidor no permite más cuentas.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Ese nombre de dominio ya está en uso en este servidor, como dominio o como otro nombre de otro dominio.",
"Your organisation has reached the number of domains it is allowed.": "Su organización ha alcanzado el número de dominios permitido.",
"That is more than the mail server accepts in one change.": "Eso supera lo que el servidor de correo acepta en un solo cambio.",
@@ -1438,7 +1438,7 @@ export const catalog: Catalog = {
"Date received": "Fecha de recepción",
"Date sent": "Fecha de envío",
"Day view": "Vista de día",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Las paletas que llevan el nombre de otro proyecto son obra de ese proyecto y se usan bajo su propia licencia; los tonos intermedios entre sus colores publicados son derivados, y cada uno se comprueba para el contraste. El color de acento de abajo sigue aplicándose sobre cualquiera de ellas.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Las paletas que llevan el nombre de otro proyecto son obra de ese proyecto y se usan bajo su propia licencia; los tonos intermedios entre sus colores publicados son derivados, y cada uno se comprueba para el contraste. El color de acento de abajo sigue aplicándose sobre cualquiera de ellas.",
"Earlier": "Antes",
"Every folder": "Todas las carpetas",
"Everyone addressed will receive this.": "Todos los destinatarios lo recibirán.",
diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts
index 0dfc0b6..4dd50d7 100644
--- a/web/src/locales/fr.ts
+++ b/web/src/locales/fr.ts
@@ -119,7 +119,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "Le serveur de messagerie n’a pas pu traiter la demande ({code}).",
"You can't give an account permissions your own role doesn't have.": "Vous ne pouvez pas donner à un compte des autorisations que votre propre rôle n’a pas.",
"This account signs in through an external directory, so its password can't be set here.": "Ce compte se connecte via un annuaire externe, son mot de passe ne peut donc pas être défini ici.",
- "The server's licence allows no more accounts.": "La licence du serveur ne permet pas de comptes supplémentaires.",
+ "The server's license allows no more accounts.": "La license du serveur ne permet pas de comptes supplémentaires.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Ce nom de domaine est déjà utilisé sur ce serveur, comme domaine ou comme autre nom d’un autre domaine.",
"Your organisation has reached the number of domains it is allowed.": "Votre organisation a atteint le nombre de domaines autorisé.",
"That is more than the mail server accepts in one change.": "C’est plus que ce que le serveur de messagerie accepte en une seule modification.",
@@ -1443,7 +1443,7 @@ export const catalog: Catalog = {
"Date received": "Date de réception",
"Date sent": "Date d'envoi",
"Day view": "Vue jour",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Les palettes portant le nom d'un autre projet sont l'œuvre de ce projet et sont utilisées sous sa propre licence ; les nuances intermédiaires entre leurs couleurs publiées sont dérivées, et chacune est vérifiée pour le contraste. La couleur d'accentuation ci-dessous s'applique toujours par-dessus n'importe laquelle d'entre elles.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Les palettes portant le nom d'un autre projet sont l'œuvre de ce projet et sont utilisées sous sa propre license ; les nuances intermédiaires entre leurs couleurs publiées sont dérivées, et chacune est vérifiée pour le contraste. La couleur d'accentuation ci-dessous s'applique toujours par-dessus n'importe laquelle d'entre elles.",
"Earlier": "Plus tôt",
"Every folder": "Tous les dossiers",
"Everyone addressed will receive this.": "Tous les destinataires le recevront.",
diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts
index 0422f3e..6865c7a 100644
--- a/web/src/locales/ja.ts
+++ b/web/src/locales/ja.ts
@@ -113,7 +113,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "メールサーバーはリクエストを実行できませんでした({code})。",
"You can't give an account permissions your own role doesn't have.": "自分のロールにない権限をアカウントに付与することはできません。",
"This account signs in through an external directory, so its password can't be set here.": "このアカウントは外部ディレクトリでサインインするため、ここではパスワードを設定できません。",
- "The server's licence allows no more accounts.": "サーバーのライセンスでは、これ以上アカウントを追加できません。",
+ "The server's license allows no more accounts.": "サーバーのライセンスでは、これ以上アカウントを追加できません。",
"That domain name is already in use on this server, as a domain or another domain's other name.": "このドメイン名は、ドメインまたは別のドメインの別名としてこのサーバーで既に使われています。",
"Your organisation has reached the number of domains it is allowed.": "組織で許可されているドメイン数の上限に達しました。",
"That is more than the mail server accepts in one change.": "メールサーバーが一度の変更で受け付けられる量を超えています。",
@@ -1446,7 +1446,7 @@ export const catalog: Catalog = {
"Date received": "受信日時",
"Date sent": "送信日時",
"Day view": "日表示",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "他のプロジェクトの名前が付いたパレットは、そのプロジェクトの成果物であり、そのプロジェクト自身のライセンスのもとで使用しています。公開されている色の中間の階調は派生させたもので、いずれもコントラストを検証しています。下のアクセントカラーは、どのパレットの上にも適用されます。",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "他のプロジェクトの名前が付いたパレットは、そのプロジェクトの成果物であり、そのプロジェクト自身のライセンスのもとで使用しています。公開されている色の中間の階調は派生させたもので、いずれもコントラストを検証しています。下のアクセントカラーは、どのパレットの上にも適用されます。",
"Earlier": "これより前",
"Every folder": "すべてのフォルダー",
"Everyone addressed will receive this.": "宛先の全員がこれを受け取ります。",
diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts
index ee6f6d3..eb60afc 100644
--- a/web/src/locales/nl.ts
+++ b/web/src/locales/nl.ts
@@ -110,7 +110,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "De mailserver kon het verzoek niet uitvoeren ({code}).",
"You can't give an account permissions your own role doesn't have.": "U kunt een account geen rechten geven die uw eigen rol niet heeft.",
"This account signs in through an external directory, so its password can't be set here.": "Dit account logt in via een externe adreslijst, dus het wachtwoord kan hier niet worden ingesteld.",
- "The server's licence allows no more accounts.": "De licentie van de server staat geen extra accounts toe.",
+ "The server's license allows no more accounts.": "De licentie van de server staat geen extra accounts toe.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Die domeinnaam is op deze server al in gebruik, als domein of als andere naam van een ander domein.",
"Your organisation has reached the number of domains it is allowed.": "Uw organisatie heeft het toegestane aantal domeinen bereikt.",
"That is more than the mail server accepts in one change.": "Dat is meer dan de mailserver in één wijziging accepteert.",
@@ -1434,7 +1434,7 @@ export const catalog: Catalog = {
"Date received": "Ontvangstdatum",
"Date sent": "Verzenddatum",
"Day view": "Dagweergave",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Paletten die naar een ander project zijn genoemd, zijn het werk van dat project en worden gebruikt onder de eigen licentie daarvan; de tinten tussen de gepubliceerde kleuren zijn afgeleid en elk daarvan wordt op contrast gecontroleerd. De accentkleur hieronder geldt nog steeds over elk ervan.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Paletten die naar een ander project zijn genoemd, zijn het werk van dat project en worden gebruikt onder de eigen licentie daarvan; de tinten tussen de gepubliceerde kleuren zijn afgeleid en elk daarvan wordt op contrast gecontroleerd. De accentkleur hieronder geldt nog steeds over elk ervan.",
"Earlier": "Eerder",
"Every folder": "Elke map",
"Everyone addressed will receive this.": "Iedereen die is geadresseerd ontvangt dit.",
diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts
index 9b7cbc3..450af15 100644
--- a/web/src/locales/pt-BR.ts
+++ b/web/src/locales/pt-BR.ts
@@ -117,7 +117,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "O servidor de e-mail não conseguiu executar a solicitação ({code}).",
"You can't give an account permissions your own role doesn't have.": "Você não pode dar a uma conta permissões que sua própria função não tem.",
"This account signs in through an external directory, so its password can't be set here.": "Esta conta entra por meio de um diretório externo, então a senha dela não pode ser definida aqui.",
- "The server's licence allows no more accounts.": "A licença do servidor não permite mais contas.",
+ "The server's license allows no more accounts.": "A licença do servidor não permite mais contas.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Esse nome de domínio já está em uso neste servidor, como domínio ou como outro nome de outro domínio.",
"Your organisation has reached the number of domains it is allowed.": "Sua organização atingiu o número de domínios permitido.",
"That is more than the mail server accepts in one change.": "Isso é mais do que o servidor de e-mail aceita em uma única alteração.",
@@ -1441,7 +1441,7 @@ export const catalog: Catalog = {
"Date received": "Data de recebimento",
"Date sent": "Data de envio",
"Day view": "Visualização de dia",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "As paletas que levam o nome de outro projeto são obra desse projeto e são usadas sob a licença dele; os tons entre as cores publicadas são derivados, e cada um é verificado quanto ao contraste. A cor de destaque abaixo continua se aplicando sobre qualquer uma delas.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "As paletas que levam o nome de outro projeto são obra desse projeto e são usadas sob a licença dele; os tons entre as cores publicadas são derivados, e cada um é verificado quanto ao contraste. A cor de destaque abaixo continua se aplicando sobre qualquer uma delas.",
"Earlier": "Antes",
"Every folder": "Todas as pastas",
"Everyone addressed will receive this.": "Todos os destinatários receberão isto.",
diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts
index 6e5fe5c..47ecfa3 100644
--- a/web/src/locales/ru.ts
+++ b/web/src/locales/ru.ts
@@ -116,7 +116,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "Почтовый сервер не смог выполнить запрос ({code}).",
"You can't give an account permissions your own role doesn't have.": "Нельзя дать учётной записи разрешения, которых нет у вашей роли.",
"This account signs in through an external directory, so its password can't be set here.": "Эта учётная запись входит через внешний каталог, поэтому её пароль нельзя задать здесь.",
- "The server's licence allows no more accounts.": "Лицензия сервера не допускает новых учётных записей.",
+ "The server's license allows no more accounts.": "Лицензия сервера не допускает новых учётных записей.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Это имя домена уже используется на сервере — как домен или как другое имя другого домена.",
"Your organisation has reached the number of domains it is allowed.": "Ваша организация достигла допустимого числа доменов.",
"That is more than the mail server accepts in one change.": "Это больше, чем почтовый сервер принимает за одно изменение.",
@@ -1440,7 +1440,7 @@ export const catalog: Catalog = {
"Date received": "Дата получения",
"Date sent": "Дата отправки",
"Day view": "День",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Палитры, названные в честь другого проекта, созданы этим проектом и используются по его собственной лицензии; оттенки между опубликованными цветами выводятся расчётом, и каждый из них проверяется на контраст. Акцентный цвет ниже по-прежнему применяется поверх любой из них.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Палитры, названные в честь другого проекта, созданы этим проектом и используются по его собственной лицензии; оттенки между опубликованными цветами выводятся расчётом, и каждый из них проверяется на контраст. Акцентный цвет ниже по-прежнему применяется поверх любой из них.",
"Earlier": "Раньше",
"Every folder": "Все папки",
"Everyone addressed will receive this.": "Это получат все указанные адресаты.",
diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts
index 34515d7..beedd8b 100644
--- a/web/src/locales/uk.ts
+++ b/web/src/locales/uk.ts
@@ -110,7 +110,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "Поштовий сервер не зміг виконати запит ({code}).",
"You can't give an account permissions your own role doesn't have.": "Не можна надати обліковому запису дозволи, яких немає у вашої ролі.",
"This account signs in through an external directory, so its password can't be set here.": "Цей обліковий запис входить через зовнішній каталог, тому його пароль не можна задати тут.",
- "The server's licence allows no more accounts.": "Ліцензія сервера не дозволяє нових облікових записів.",
+ "The server's license allows no more accounts.": "Ліцензія сервера не дозволяє нових облікових записів.",
"That domain name is already in use on this server, as a domain or another domain's other name.": "Це ім'я домену вже використовується на сервері — як домен або як інше ім'я іншого домену.",
"Your organisation has reached the number of domains it is allowed.": "Ваша організація досягла дозволеної кількості доменів.",
"That is more than the mail server accepts in one change.": "Це більше, ніж поштовий сервер приймає за одну зміну.",
@@ -1434,7 +1434,7 @@ export const catalog: Catalog = {
"Date received": "Дата отримання",
"Date sent": "Дата надсилання",
"Day view": "День",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Палітри, названі на честь іншого проєкту, є роботою цього проєкту й використовуються за його власною ліцензією; відтінки між опублікованими кольорами обчислюються, і кожен із них перевіряється на контраст. Акцентний колір нижче й надалі застосовується поверх будь-якої з них.",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "Палітри, названі на честь іншого проєкту, є роботою цього проєкту й використовуються за його власною ліцензією; відтінки між опублікованими кольорами обчислюються, і кожен із них перевіряється на контраст. Акцентний колір нижче й надалі застосовується поверх будь-якої з них.",
"Earlier": "Раніше",
"Every folder": "Усі теки",
"Everyone addressed will receive this.": "Це отримають усі зазначені адресати.",
diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts
index 2d7423a..ad2d9cb 100644
--- a/web/src/locales/zh-Hans.ts
+++ b/web/src/locales/zh-Hans.ts
@@ -112,7 +112,7 @@ export const catalog: Catalog = {
"The mail server could not carry out the request ({code}).": "邮件服务器无法执行该请求({code})。",
"You can't give an account permissions your own role doesn't have.": "您不能授予账户您自己的角色所没有的权限。",
"This account signs in through an external directory, so its password can't be set here.": "该账户通过外部目录登录,因此无法在此设置其密码。",
- "The server's licence allows no more accounts.": "服务器许可证不允许再添加账户。",
+ "The server's license allows no more accounts.": "服务器许可证不允许再添加账户。",
"That domain name is already in use on this server, as a domain or another domain's other name.": "该域名已在此服务器上被使用,可能是一个域名,也可能是另一个域名的其他名称。",
"Your organisation has reached the number of domains it is allowed.": "您的组织已达到允许的域名数量上限。",
"That is more than the mail server accepts in one change.": "这超出了邮件服务器单次更改可接受的范围。",
@@ -1445,7 +1445,7 @@ export const catalog: Catalog = {
"Date received": "接收日期",
"Date sent": "发送日期",
"Day view": "日视图",
- "Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "以其他项目命名的配色方案是该项目的作品,按其自身的许可证使用;已发布颜色之间的过渡色是推导得出的,并且每一种都经过对比度检查。下方的强调色仍会应用于其中任何一种配色方案。",
+ "Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.": "以其他项目命名的配色方案是该项目的作品,按其自身的许可证使用;已发布颜色之间的过渡色是推导得出的,并且每一种都经过对比度检查。下方的强调色仍会应用于其中任何一种配色方案。",
"Earlier": "更早",
"Every folder": "所有文件夹",
"Everyone addressed will receive this.": "所有收件人都会收到此邮件。",
diff --git a/web/src/views/admin/TenantsAdmin.tsx b/web/src/views/admin/TenantsAdmin.tsx
index dcaa6bb..0a76088 100644
--- a/web/src/views/admin/TenantsAdmin.tsx
+++ b/web/src/views/admin/TenantsAdmin.tsx
@@ -24,7 +24,7 @@ const PAGE_SIZE = 50;
* beyond an ordinary user's permissions, so there is nothing worth creating or
* listing. On Enterprise the notice is left out, unless the installation asks
* for it (SHOW_ENTERPRISE_NOTICES), as the public demo does so as not to
- * suggest tenants come without the licence.
+ * suggest tenants come without the license.
*/
export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
const edition = useSession((s) => s.session?.ihasmail?.server?.edition ?? null);
diff --git a/web/src/views/settings/AppearanceSettings.tsx b/web/src/views/settings/AppearanceSettings.tsx
index a9cea41..9d4745d 100644
--- a/web/src/views/settings/AppearanceSettings.tsx
+++ b/web/src/views/settings/AppearanceSettings.tsx
@@ -107,7 +107,7 @@ export function AppearanceSettings() {
})}
- {translate("Palettes named after another project are that project's work, used under its own licence; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.")}
+ {translate("Palettes named after another project are that project's work, used under its own license; the shades between their published colours are derived, and every one is checked for contrast. The accent colour below still applies over any of them.")}
Date: Tue, 15 Sep 2026 11:45:58 -0700
Subject: [PATCH 2/2] Use American English spelling throughout
---
.github/workflows/ci.yml | 2 +-
.github/workflows/publish.yml | 4 +-
.palette-sources/palettes-upstream.md | 12 +-
CONTRIBUTING.md | 14 +--
FEATURES.md | 116 +++++++++---------
KNOWN-ISSUES.md | 24 ++--
NOTICE | 8 +-
ROADMAP.md | 6 +-
docs/screenshots.mjs | 2 +-
scripts/basePath.mjs | 2 +-
scripts/build-palettes.py | 44 +++----
scripts/i18n-catalog-check.mjs | 26 ++--
scripts/i18n-literals.mjs | 18 +--
scripts/i18n-strings.mjs | 6 +-
server/src/account.test.ts | 2 +-
server/src/account.ts | 8 +-
server/src/accountinfo.test.ts | 6 +-
server/src/adminGate.ts | 4 +-
server/src/app.ts | 14 +--
server/src/clientip.test.ts | 2 +-
server/src/config.ts | 4 +-
server/src/domainrouting.test.ts | 2 +-
server/src/imageproxy.test.ts | 2 +-
server/src/mock/directory.ts | 2 +-
server/src/mock/index.ts | 16 +--
server/src/mock/recurrence.test.ts | 2 +-
server/src/mock/recurrence.ts | 2 +-
server/src/mock/signedMessages.ts | 2 +-
server/src/permissions.test.ts | 2 +-
server/src/sessions.ts | 2 +-
server/src/settingspolicy.test.ts | 2 +-
server/src/static.ts | 4 +-
server/src/totp.ts | 4 +-
server/src/upstream.ts | 8 +-
web/index.html | 2 +-
web/src/App.tsx | 20 +--
web/src/jmap/client.ts | 2 +-
web/src/lib/__tests__/adminAccess.test.ts | 2 +-
web/src/lib/__tests__/adminDashboard.test.ts | 10 +-
web/src/lib/__tests__/adminDirectory.test.ts | 2 +-
web/src/lib/__tests__/adminDomains.test.ts | 8 +-
.../lib/__tests__/availabilityWindow.test.ts | 2 +-
web/src/lib/__tests__/birthdays.test.ts | 4 +-
web/src/lib/__tests__/brand.test.ts | 2 +-
web/src/lib/__tests__/datetime.test.ts | 4 +-
web/src/lib/__tests__/describeRules.test.ts | 10 +-
web/src/lib/__tests__/folderMove.test.ts | 4 +-
web/src/lib/__tests__/html.test.ts | 44 +++----
web/src/lib/__tests__/htmlAlternative.test.ts | 2 +-
web/src/lib/__tests__/i18n.test.tsx | 10 +-
web/src/lib/__tests__/ics.test.ts | 2 +-
web/src/lib/__tests__/icsWrite.test.ts | 6 +-
web/src/lib/__tests__/languages.test.ts | 8 +-
web/src/lib/__tests__/listSort.test.ts | 2 +-
web/src/lib/__tests__/mailboxName.test.ts | 14 +--
web/src/lib/__tests__/markdown.test.ts | 2 +-
web/src/lib/__tests__/mozillaAb.test.ts | 2 +-
web/src/lib/__tests__/palette.test.ts | 4 +-
.../lib/__tests__/permissionLabels.test.ts | 2 +-
web/src/lib/__tests__/settingsSync.test.ts | 2 +-
web/src/lib/__tests__/share.test.ts | 2 +-
web/src/lib/__tests__/signatureHtml.test.ts | 2 +-
web/src/lib/__tests__/swFacts.test.ts | 6 +-
web/src/lib/__tests__/swipe.test.ts | 2 +-
web/src/lib/__tests__/theme.test.ts | 2 +-
web/src/lib/__tests__/tnef.test.ts | 2 +-
web/src/lib/__tests__/touch.test.ts | 2 +-
web/src/lib/__tests__/unsavedChanges.test.tsx | 2 +-
web/src/lib/accountRouting.ts | 2 +-
web/src/lib/adminAccess.ts | 2 +-
web/src/lib/adminDashboard.ts | 2 +-
web/src/lib/adminDirectory.ts | 12 +-
web/src/lib/adminDomains.ts | 10 +-
web/src/lib/adminRoles.ts | 2 +-
web/src/lib/appointment.ts | 4 +-
web/src/lib/basePath.ts | 2 +-
web/src/lib/birthdays.ts | 2 +-
web/src/lib/datetime.ts | 10 +-
web/src/lib/emlName.ts | 2 +-
web/src/lib/filenode.ts | 2 +-
web/src/lib/folderMove.ts | 2 +-
web/src/lib/html.ts | 32 ++---
web/src/lib/i18n.ts | 32 ++---
web/src/lib/languages.ts | 10 +-
web/src/lib/ldif.ts | 8 +-
web/src/lib/mailboxName.ts | 6 +-
web/src/lib/markdown.ts | 2 +-
web/src/lib/mozillaAb.ts | 2 +-
web/src/lib/palette.ts | 8 +-
web/src/lib/permissionLabels.ts | 2 +-
web/src/lib/recurrence.ts | 8 +-
web/src/lib/settingsSync.ts | 4 +-
web/src/lib/shareTarget.ts | 2 +-
web/src/lib/sieve.ts | 6 +-
web/src/lib/smime/__tests__/verify.test.ts | 6 +-
web/src/lib/smime/mime.ts | 2 +-
web/src/lib/smime/verify.ts | 2 +-
web/src/lib/spamScore.ts | 2 +-
web/src/lib/storage.ts | 2 +-
web/src/lib/swFacts.ts | 2 +-
web/src/lib/swipe.ts | 2 +-
web/src/lib/templatePlaceholders.ts | 2 +-
web/src/lib/touch.ts | 14 +--
web/src/locales/__tests__/ru-plurals.test.ts | 4 +-
web/src/locales/de.ts | 43 ++++---
web/src/locales/es.ts | 43 ++++---
web/src/locales/fr.ts | 43 ++++---
web/src/locales/ja.ts | 43 ++++---
web/src/locales/nl.ts | 43 ++++---
web/src/locales/pt-BR.ts | 43 ++++---
web/src/locales/ru.ts | 43 ++++---
web/src/locales/uk.ts | 43 ++++---
web/src/locales/zh-Hans.ts | 43 ++++---
web/src/store/__tests__/event-scope.test.ts | 2 +-
web/src/store/__tests__/ics-import.test.ts | 2 +-
.../store/__tests__/lang-attribute.test.ts | 2 +-
web/src/store/__tests__/ldif-import.test.ts | 2 +-
web/src/store/__tests__/ldif-likeness.test.ts | 2 +-
web/src/store/__tests__/recurrence.test.ts | 4 +-
.../store/__tests__/reply-addressing.test.ts | 4 +-
.../__tests__/scheduled-reconcile.test.ts | 4 +-
web/src/store/calendar.ts | 18 +--
web/src/store/compose.ts | 2 +-
web/src/store/contacts.ts | 4 +-
web/src/store/mail.ts | 2 +-
web/src/store/scheduled.ts | 4 +-
web/src/store/session.ts | 2 +-
web/src/store/settings.ts | 20 +--
web/src/store/sieve.ts | 2 +-
web/src/styles/app.css | 40 +++---
web/src/ui/TranslateBoundary.tsx | 4 +-
.../ui/__tests__/translate-boundary.test.tsx | 12 +-
web/src/ui/datefield.tsx | 2 +-
web/src/ui/dialog.tsx | 2 +-
web/src/ui/filepreview.tsx | 2 +-
web/src/ui/qrcode.tsx | 2 +-
web/src/views/AppShell.tsx | 4 +-
web/src/views/admin/AccountSheet.tsx | 2 +-
web/src/views/admin/AccountsAdmin.tsx | 16 +--
web/src/views/admin/AdminDashboard.tsx | 10 +-
web/src/views/admin/DomainSheet.tsx | 22 ++--
web/src/views/admin/DomainsAdmin.tsx | 12 +-
web/src/views/admin/GroupSheet.tsx | 16 +--
web/src/views/admin/GroupsAdmin.tsx | 18 +--
web/src/views/admin/ListsAdmin.tsx | 16 +--
web/src/views/admin/RolesAdmin.tsx | 18 +--
web/src/views/admin/TenantSheet.tsx | 14 +--
web/src/views/admin/TenantsAdmin.tsx | 22 ++--
.../views/calendar/CalendarContextMenu.tsx | 14 +--
web/src/views/calendar/CalendarView.tsx | 2 +-
web/src/views/calendar/EventEditor.tsx | 10 +-
web/src/views/calendar/EventPopover.tsx | 2 +-
web/src/views/compose/Composer.tsx | 2 +-
.../compose/__tests__/composer-dock.test.tsx | 2 +-
web/src/views/files/FilesTree.tsx | 2 +-
web/src/views/files/FilesView.tsx | 2 +-
web/src/views/mail/InviteCard.tsx | 10 +-
web/src/views/mail/MailView.tsx | 2 +-
web/src/views/mail/MailboxTree.tsx | 12 +-
web/src/views/mail/MessageList.tsx | 2 +-
web/src/views/mail/MessageView.tsx | 10 +-
.../mail/__tests__/signature-banner.test.tsx | 4 +-
web/src/views/settings/AppearanceSettings.tsx | 10 +-
web/src/views/settings/CalendarSettings.tsx | 2 +-
web/src/views/settings/FoldersSettings.tsx | 6 +-
web/src/views/settings/LabelsSettings.tsx | 2 +-
web/src/views/settings/PrivacySettings.tsx | 4 +-
web/src/views/settings/ShortcutsSettings.tsx | 2 +-
.../__tests__/privacy-settings.test.tsx | 2 +-
169 files changed, 786 insertions(+), 795 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 22a92a2..5051716 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ on:
# Without this there is no way to re-run a check that never started: a run
# GitHub queues and then orphans -- as it did to every run created during the
# Actions outage on 2026-08-26 -- can be neither rerun ("already running")
- # nor cancelled ("already completed"), and the workflow has no other trigger
+ # nor canceled ("already completed"), and the workflow has no other trigger
# to reach for. Useful too for putting a check on a commit that predates a CI
# change, without pushing an empty commit to move it.
workflow_dispatch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c50e2a5..a734e8d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -4,7 +4,7 @@
# `ghcr.io/coffey-labs/ihasmail:latest` for a long time, and nothing ever
# pushed it: `docker pull` answered `denied`, because the package did not
# exist. This is the workflow that makes those instructions true. It is also
-# the prerequisite for the self-hosted app catalogues -- TrueNAS and Unraid
+# the prerequisite for the self-hosted app catalogs -- TrueNAS and Unraid
# both install by pulling an image and neither builds from source.
#
# FIRST RUN: a package GHCR creates for the first time is **private**, even in
@@ -44,7 +44,7 @@ on:
type: boolean
default: false
# Same reasoning as ci.yml's dispatch trigger: a run GitHub queues and then
- # orphans can be neither rerun nor cancelled, and this workflow otherwise
+ # orphans can be neither rerun nor canceled, and this workflow otherwise
# only fires on a release -- which is not something to cut twice because a
# runner died. `ref` also allows publishing an image for a tag that predates
# this workflow, which is how the first one gets built.
diff --git a/.palette-sources/palettes-upstream.md b/.palette-sources/palettes-upstream.md
index 5a530c5..85fca2e 100644
--- a/.palette-sources/palettes-upstream.md
+++ b/.palette-sources/palettes-upstream.md
@@ -84,23 +84,23 @@ violet #6c71c4 · blue #268bd2 · cyan #2aa198 · green #859900
The accents are shared by both modes by design. Two tiers are **derived**: the
sunken dark surface #001f28 (below base03) and the raised light surface
#fffdf6 (above base3), neither of which Solarized publishes, plus the two
-rule colours #0d4552 and #e6dfc8.
+rule colors #0d4552 and #e6dfc8.
## Everforest — sainnhe/everforest, MIT (palette.md), medium contrast
### Dark
bg_dim #232a2e · bg0 #2d353b · bg1 #343f44 · bg3 #475258
-fg #d3c6aa · grey1 #859289
+fg #d3c6aa · gray1 #859289
red #e67e80 · orange #e69875 · yellow #dbbc7f · green #a7c080 · aqua #83c092
blue #7fbbb3 · purple #d699b6
### Light
bg_dim #efebd4 · bg0 #fdf6e3 · bg3 #e6e2cc · bg5 #bdc3af
-fg #5c6a72 · grey1 #939f91
+fg #5c6a72 · gray1 #939f91
red #f85552 · orange #f57d26 · yellow #dfa000 · green #8da101 · aqua #35a77c
blue #3a94c5 · purple #df69ba
Light uses bg_dim as the page and bg0 as the raised surface, so the card the
-reader looks at is the colour Everforest calls its background.
+reader looks at is the color Everforest calls its background.
## Kanagawa — rebelot/kanagawa.nvim, MIT (lua/kanagawa/colors.lua)
### Wave (dark)
@@ -118,7 +118,7 @@ lotusGreen #6f894e · lotusYellow #77713f · lotusPink #b35b79
## Ayu — ayu-theme/ayu-colors, MIT (themes/dark.yaml, themes/light.yaml)
The YAMLs give the base palette and the surfaces as literals but express syntax
roles as references (`$palette.indigo.l2`), and the resolved files are not
-committed. The two signature accents are taken from the same organisation's
+committed. The two signature accents are taken from the same organization's
MIT-licensed ayu-theme/vscode-ayu build.
### Dark
@@ -134,7 +134,7 @@ red #F07171 · orange #FA8532 · yellow #EBA400 · green #86B300 · teal #4CBF99
indigo #55B4D4 · blue #22A4E6 · purple #A37ACC · accent #F29718 (vscode-ayu)
## Primer — primer/primitives, MIT (src/tokens/base/color/{dark,light})
-Named "Primer" after the design system. The colour values are MIT; "GitHub"
+Named "Primer" after the design system. The color values are MIT; "GitHub"
and the Invertocat are trademarks, and nothing here is endorsed by them.
### Dark
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ee5ec63..783d893 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,11 +74,11 @@ failing, so an untranslated string is invisible until somebody reading that
language finds it.
**Any change that adds or alters a user-visible string adds work in all nine
-catalogues.** Say so explicitly in the PR — how many keys, and the fallback
+catalogs.** Say so explicitly in the PR — how many keys, and the fallback
count before and after — and say so just as explicitly when a change adds none,
so it is never left to be inferred.
-#### The catalogue key for a plural is the `other` form
+#### The catalog key for a plural is the `other` form
`plural()` looks the entry up by `forms.other`, so a call site written as
@@ -86,13 +86,13 @@ so it is never left to be inferred.
plural(n, { one: "Deleted {n} contact", other: "Deleted {n} contacts" })
```
-is keyed on **`"Deleted {n} contacts"`**. Keying the catalogue on the `one`
+is keyed on **`"Deleted {n} contacts"`**. Keying the catalog on the `one`
form type-checks, builds, passes every test, and silently falls back to English
in all nine languages. Nothing errors. The only signal is the fallback count
going up, so read it:
```sh
-npm run i18n:check # literals wrapped, and catalogue health; exits 1 on a finding
+npm run i18n:check # literals wrapped, and catalog health; exits 1 on a finding
node scripts/i18n-catalog-check.mjs # per-language: translated / used / falling back
```
@@ -152,7 +152,7 @@ browser ──(same-origin /api/*)──► ihasmail server (Node + Hono) ─
JMAP client + stores • /api/jmap, /api/blob, /api/upload, /api/events (SSE), /api/image
```
-- `web/` — Vite + React 19 + TypeScript SPA. `src/jmap` (client, push, types), `src/store` (zustand: session, mail, compose, contacts, calendar, files, sieve, settings), `src/views`, `src/lib` (sanitiser, search parser, Sieve codec, locale-aware dates, vCard, …).
+- `web/` — Vite + React 19 + TypeScript SPA. `src/jmap` (client, push, types), `src/store` (zustand: session, mail, compose, contacts, calendar, files, sieve, settings), `src/views`, `src/lib` (sanitizer, search parser, Sieve codec, locale-aware dates, vCard, …).
- `server/` — Node/Hono backend: authenticates against Stalwart's JMAP session endpoint, seals the credentials with a key derived from the cookie secret, proxies JMAP/blob/SSE, serves the SPA under a strict CSP. `src/mock/` is an in-memory fake Stalwart for development and demos.
Capabilities used: `core`, `mail`, `submission`, `vacationresponse`, `sieve`,
@@ -179,10 +179,10 @@ RFC 8984's.
| `MOCK_EDITION=enterprise` | Reports Enterprise, which Tenants needs |
It tracks the current Stalwart release rather than 0.16 in general, and each
-behaviour is confirmed against a real server before it is copied here — the
+behavior is confirmed against a real server before it is copied here — the
comments say which version and on what date. Where a release changes something
a client can see, the mock changes with it, and the test that pinned the old
-behaviour is rewritten rather than deleted, so the reversal stays on the record.
+behavior is rewritten rather than deleted, so the reversal stays on the record.
#### Version numbers
diff --git a/FEATURES.md b/FEATURES.md
index bd4c5c2..0d550f7 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -13,7 +13,7 @@ questions:
| [docs.ihasmail.org](https://docs.ihasmail.org) | How to install, configure and drive each of these |
Written against the tree at Stalwart **0.16.22**, which is the version the live
-instance runs. Behaviours carrying an older version below were checked against
+instance runs. Behaviors carrying an older version below were checked against
that one and have not changed since; where a later release changed something,
the entry says so and names both. 0.16.22 changed nothing described here: its
client-visible changes are in what `CalendarEvent/get` and `ContactCard/get`
@@ -103,7 +103,7 @@ every drag.
archives and left deletes by default, which is what the mail app the phone
came with already does. Either direction can be set to archive, delete,
report spam, read/unread, star or move to… — or to nothing, which turns that
- direction off. The coloured strip revealed behind the row names what will
+ direction off. The colored strip revealed behind the row names what will
actually happen *in the folder it is happening in*: "Delete forever" out of
Deleted Items, "Not spam" inside Junk Mail. Where an action
is meaningless there — archiving out of the archive, calling your own drafts
@@ -143,14 +143,14 @@ it is silently nothing there.
The arithmetic lives in `web/src/lib/touch.ts`, away from the components and
under test, because the numbers are the whole thing. The axis lock is
-deliberately biased towards the vertical: scrolling is what a finger on a
+deliberately biased toward the vertical: scrolling is what a finger on a
message list is doing almost every time, and a scroll misread as a swipe grabs
the list out from under the reader, while a swipe misread as a scroll costs one
more attempt. A drag that is merely more sideways than not stays a scroll.
## The message list
-- **Virtualised** — rows are windowed with `@tanstack/react-virtual`, so a
+- **Virtualized** — rows are windowed with `@tanstack/react-virtual`, so a
folder of 100,000 messages scrolls at the same speed as one of ten. Row
height follows density and the one- or two-line layout.
- **Infinite scroll** with server-side paging, 50 at a time by default.
@@ -236,7 +236,7 @@ for that one, and the dialog says so.
## Folders
-Real JMAP mailboxes, with the server's roles honoured.
+Real JMAP mailboxes, with the server's roles honored.
- Create, rename, create a subfolder, delete (with or without its mail).
- **Drag a folder onto another** to reparent it. Folders with a server role
@@ -246,18 +246,18 @@ Real JMAP mailboxes, with the server's roles honoured.
unsubscribed folder still exists and still receives; it is just out of the
way. Inbox cannot be hidden.
- **Mark all as read**, optionally including subfolders.
-- **Folder colours**, per mailbox id.
+- **Folder colors**, per mailbox id.
- **Unread counts** per folder, live.
- **Storage quota** bar under the tree where the server reports one.
- Rights are respected per folder: rename, delete, create-child and share each
- grey out when `myRights` says no.
+ gray out when `myRights` says no.
- A folder in the address that this account does not have says *this folder is
missing*, rather than drawing an empty folder — a stale link should not read
as a folder that emptied itself.
## Labels
-Labels are **IMAP keywords** with a colour and a display name kept in settings.
+Labels are **IMAP keywords** with a color and a display name kept in settings.
Because they are keywords, every other client that reads the mailbox sees them,
and they survive ihasmail entirely. A message can carry any number. They are
managed in Settings › Labels, applied from `l` or the context menu, and
@@ -311,7 +311,7 @@ same query string — so what it builds can be read, edited and learned from.
## Reading a message
-- **Sanitised HTML**, rendered inside a **Shadow DOM** so the sender's CSS
+- **Sanitized HTML**, rendered inside a **Shadow DOM** so the sender's CSS
cannot reach the app. DOMPurify strips scripts, event handlers, forms and
anything that could navigate the top window.
- **Remote images blocked by default**, with a banner offering *Show images* or
@@ -370,14 +370,14 @@ same query string — so what it builds can be read, edited and learned from.
since the filter applies policy ihasmail cannot see. Mail that arrived without
these headers shows nothing.
- **Message body theming** is off by default — sender HTML is left exactly as it
- was designed, on a light card. One setting lets mail that brings no colours of
+ was designed, on a light card. One setting lets mail that brings no colors of
its own follow the app's theme instead. That is a low bar in practice: one
`color:#FFFFFF` on one button label opts a whole message out, so for mail
built from a template it changed nothing. A second setting, off unless the
- first is on, forces the theme over the sender's own colours. It tells a
+ first is on, forces the theme over the sender's own colors. It tells a
*sheet* the design sits on, like a white wrapper table, from a *painted
surface* like a button or a banner, by relative luminance: the first is
- neutralised so the bright card goes away, the second is kept whole so its
+ neutralized so the bright card goes away, the second is kept whole so its
label stays readable on it. Nothing the sender wrote is removed, so the
switch is reversible, and print is unaffected either way.
@@ -397,7 +397,7 @@ same query string — so what it builds can be read, edited and learned from.
- **Invitations (iTIP)** render an invite card: what, when, where, the guest
list with each person's status, and Yes / Maybe / No. The reply is written to
- the event and sent back to the organiser. Cancellations are recognised too.
+ the event and sent back to the organizer. Cancellations are recognized too.
- **vCard attachments** render a card offering to add the person to an address
book.
- **Right-click anyone named** in the message — From, To, Cc, Bcc or Reply-To —
@@ -427,9 +427,9 @@ Requesting one on your own outgoing mail is a separate switch.
## Composing
**Multiple composers at once**, floating in a dock at the bottom right, each
-minimisable and maximisable; full-screen on mobile.
+minimizable and maximizable; full-screen on mobile.
-- **Rich text**: bold, italic, underline, strikethrough, text colour, highlight,
+- **Rich text**: bold, italic, underline, strikethrough, text color, highlight,
font size, alignment, bulleted and numbered lists, indent/outdent, blockquote,
code block, links (`Ctrl+K`), inline images, an emoji picker, and remove
formatting. Tab and Shift+Tab indent inside the body.
@@ -468,7 +468,7 @@ minimisable and maximisable; full-screen on mobile.
- **Attachments** by picking or dragging onto the composer, with progress per
file and the size limit the server states (`MAX_UPLOAD_BYTES`, 50 MB by
default). A pasted image is inserted inline instead, and pasted HTML is
- sanitised on the way in.
+ sanitized on the way in.
- **Attach from Files** — anything the server already holds attaches with **no
upload at all**, however large. A file from someone else's shared folder is
copied to your account first, because a message can only carry blobs from the
@@ -509,7 +509,7 @@ out whether or not ihasmail is open, or ever opened again.
Held messages wait in a **Scheduled** folder ihasmail maintains itself (JMAP has
no role for one), and reconciles when you next open it: released messages move
-to Sent, cancelled ones back to Drafts. The picker offers presets and an exact
+to Sent, canceled ones back to Drafts. The picker offers presets and an exact
date and time, bounded by the maximum delay the server advertises.
> If Stalwart's `futureRelease` is not configured, a "scheduled" message is sent
@@ -536,14 +536,14 @@ are settings.
The sidebar keeps three groups apart:
-- **My calendars** — yours, each with a colour, each hideable with a click.
+- **My calendars** — yours, each with a color, each hideable with a click.
- **Shared with me** — other people's, once added.
- **Available to add** — shared with you but not yet added, with a plus beside
each. An unadded calendar draws nothing. This is deliberate: the server
reports every collection in an account you can reach, whether or not anyone
meant to share it, so being handed one is not evidence that it was offered.
-Right-click your own to rename, recolour, share, stop sharing or delete;
+Right-click your own to rename, recolor, share, stop sharing or delete;
right-click one of someone else's to remove it from your view, which changes
nothing for anybody else.
@@ -551,7 +551,7 @@ nothing for anybody else.
events), from the calendar's own menu, into that calendar. The events are
filed rather than scheduled: no invitations go out to anyone named in them.
- **Re-importing updates rather than duplicates**, as a contacts import does.
- An event is recognised by its UID, per calendar, and what the file carries
+ An event is recognized by its UID, per calendar, and what the file carries
wins -- so a corrected export corrects what the first attempt got wrong.
Two things are deliberately left alone: **who accepted**, and **edits to a
@@ -566,11 +566,11 @@ nothing for anybody else.
since the last import does not arrive, because nothing here can tell that
apart from an answer given in ihasmail. And an import still sends no
scheduling messages, so an event a re-import moves is moved *here* --
- everybody else's copy still says the old time until whoever is organising
+ everybody else's copy still says the old time until whoever is organizing
sends the update from the event itself.
- **Subscribed calendars** by URL — a timetable, a rota, a public holiday list.
Added in Settings › Calendar & contacts, read-only, and shown beside your own
- with their own colour.
+ with their own color.
**Nothing is stored.** The document is fetched when you open the calendar and
parsed in the browser; the server keeps no copy, no cache and no schedule,
@@ -608,7 +608,7 @@ nothing for anybody else.
They cannot be edited or deleted, and that falls out of the design rather
than being special-cased: the virtual calendar reports no write rights, so
every control that asks before offering Edit or Delete already declines. The
- store refuses a synthesised id as well, whatever calls it.
+ store refuses a synthesized id as well, whatever calls it.
A card that records only a day and month — the common case — gets a birthday
with no age rather than no birthday. And 29 February falls on the 28th in a
@@ -623,16 +623,16 @@ empty space offers a timed or all-day event at that moment, or *Go to day* /
The editor covers title, start and end (all-day or timed, with a time zone),
calendar, location, meeting link, guests, description, reminders, repeat,
-status (confirmed / tentative / cancelled), show-as (busy / free), visibility
-(default / private / secret), category and colour.
+status (confirmed / tentative / canceled), show-as (busy / free), visibility
+(default / private / secret), category and color.
- **Recurrence** — none, daily, weekly, weekdays, monthly, yearly, or a custom
builder: interval, by-weekday, by-month-day, and an end by count or by date.
- **Reminders** — one or more alerts before the start, with a default in settings.
-- **Colour categories**, Outlook-style: named colours managed in Settings ›
+- **Color categories**, Outlook-style: named colors managed in Settings ›
Calendar, assigned from the editor or the context menu, and stored as
- JSCalendar `categories` so other clients see them. (The per-event colour
- picker that predated them is gone; a colour comes from the category, or the
+ JSCalendar `categories` so other clients see them. (The per-event color
+ picker that predated them is gone; a color comes from the category, or the
calendar.)
- **Duplicate** an event from the context menu.
- **Create event…** from a message, in its context menu and its ⋮ menu (and,
@@ -649,7 +649,7 @@ status (confirmed / tentative / cancelled), show-as (busy / free), visibility
## Attendees, invitations and free/busy
Invitations go out as iTIP when guests are added, replies come back and are
-applied to the event, and cancelling notifies the guests. Guests are added by
+applied to the event, and canceling notifies the guests. Guests are added by
name or address with the same autocomplete the composer uses.
Where the server implements `Principal/getAvailability`, the event editor grows
@@ -756,24 +756,24 @@ JMAP Contacts and JSContact.
afterwards is what the server confirmed rather than what was asked for.
- **Letter index** down the list, with `#` for everything that does not start
with a letter.
-- **Search** across name, address, organisation and notes, in one book or all.
+- **Search** across name, address, organization and notes, in one book or all.
- **vCard import** through `ContactCard/parse` (a file of any number of cards),
and **export** of one card or the whole book as `.vcf`.
- **LDIF import**, for address books coming from SOGo, Thunderbird or an LDAP
directory. Nothing on the server reads LDIF, so the file is read here:
RFC 2849 for the syntax, [Mozilla's address book schema][ldif-schema] for what
the attributes mean, which is the one such exports almost always use. Work and
- home addresses, every phone kind, second email, organisation and units, job
+ home addresses, every phone kind, second email, organization and units, job
title, nickname, web pages and the custom fields all come across. The import
control takes either format and decides by what is in the file, not by what it
is called.
-- **Re-importing updates rather than duplicates.** A vCard is recognised by its
+- **Re-importing updates rather than duplicates.** A vCard is recognized by its
UID; an LDIF entry, whose schema has none, by its distinguished name. The card
already here is merged with the file's version -- what the file carries wins,
what it does not mention is left alone -- so a corrected export can correct
what the first attempt got wrong. Matching is per address book, which is also
how two directories that each hold a `cn=John Smith` stay two people. An entry
- no longer recognisable, because its `dn` moved between exports, is imported
+ no longer recognizable, because its `dn` moved between exports, is imported
again and counted: *"3 of them look like contacts you already had."*
[ldif-schema]: https://wiki.mozilla.org/MailNews:Mozilla_LDAP_Address_Book_Schema
@@ -898,7 +898,7 @@ individual rights by hand.
Preferences live in a `settings.json` in the account's own JMAP Files, beside
the signature images. So identity, signatures, locale, date and time formats,
-theme, labels, templates, folder colours, trusted image senders and added shares
+theme, labels, templates, folder colors, trusted image senders and added shares
are the same wherever you sign in, private windows included — and they are
backed up with the mail store, because they *are* in the mail store. ihasmail
still stores nothing of its own.
@@ -923,14 +923,14 @@ not reach another that already has ihasmail open until it signs in again.
| --- | --- |
| **General** | Reading pane, mark-as-read delay, auto-advance, conversation view, snippets, avatars; compose format, quoting, signature placement, spell check; time zone, week start, language & region, date format, time format; `mailto:` handler; export / import / reset |
| **Privacy & safety** | Remote images and the senders trusted with them, read receipts asked for and answered; the three warnings and the domains they measure against; undo-send window, attachment reminder, confirm-before-delete |
-| **Appearance** | Theme, accent colour, density, font size, sidebar, swipe actions, interface language |
+| **Appearance** | Theme, accent color, density, font size, sidebar, swipe actions, interface language |
| **Identities & signatures** | Addresses, names, Reply-To, HTML signatures, the default, and which to hide from the picker |
| **Filters & rules** | The visual builder and raw Sieve editor |
| **Out of office** | Vacation response |
-| **Folders** | Create, rename, colour, subscribe |
-| **Labels** | Keyword, display name, colour |
+| **Folders** | Create, rename, color, subscribe |
+| **Labels** | Keyword, display name, color |
| **Templates** | Named subject + body |
-| **Calendar & contacts** | Colour categories, working hours, default view, default duration, default reminder |
+| **Calendar & contacts** | Color categories, working hours, default view, default duration, default reminder |
| **Notifications** | In-tab notifications, notify-when-closed (Web Push), sound |
| **Security & sessions** | Password, two-factor state, app passwords, active webmail sessions |
| **Keyboard shortcuts** | The full list, grouped |
@@ -940,7 +940,7 @@ not reach another that already has ihasmail open until it signs in again.
between them is worth stating because two similar words in one nav is how a
menu becomes something people hunt through. Security & sessions is credentials
and access: password, two-factor state, app passwords, live sessions. Privacy &
-safety is how the app behaves towards the reader and towards senders: what
+safety is how the app behaves toward the reader and toward senders: what
loads, what leaks, and what asks before it happens. These had been spread
through General, which had grown five unrelated headings — remote images filed
under "Reading", the read-receipt policy under "Composing", the undo-send window
@@ -1010,12 +1010,12 @@ is why they are two settings and not one.
| | |
| --- | --- |
-| English | the source language, and what every other catalogue falls back to |
+| English | the source language, and what every other catalog falls back to |
| Deutsch · Español · Français · Nederlands · Português (Brasil) | Beta |
| Русский · Українська · 简体中文 · 日本語 | Beta |
**All nine translations are marked Beta, and the label is not modesty.**
-The catalogues were produced by AI against standard dictionaries and have not
+The catalogs were produced by AI against standard dictionaries and have not
been read by anybody who speaks the language. That is stated in Settings, next
to a link for reporting anything that reads wrongly, because the alternative —
shipping them quietly — would ask people to trust text nobody has checked. A
@@ -1025,7 +1025,7 @@ deliberate act by a person and not something a percentage earns.
Two things follow from the design rather than the translation:
- **A missing entry renders its English source.** So deleting a bad line is a
- valid fix, not a regression, and a catalogue is never in a half-broken state.
+ valid fix, not a regression, and a catalog is never in a half-broken state.
- **Plurals are asked for, never assumed.** `Intl.PluralRules` decides the form,
so Russian and Ukrainian get their three (1 письмо, 2–4 письма, 5+ писем) and
Japanese and Chinese get the one they actually have — with counters doing the
@@ -1037,7 +1037,7 @@ The interface language also feeds the *automatic* date locale, so choosing
page that is already in the reader's language — and accepting that offer is
what rewrites the DOM underneath React.
-Only languages with a catalogue shipped appear in the picker. A language
+Only languages with a catalog shipped appear in the picker. A language
offered without strings behind it would leave the page claiming to be in a
language it is not, which is worse than not offering it: it stops a browser
offering to translate a page the reader cannot read.
@@ -1061,28 +1061,28 @@ at two.
| **Ayu** | |
| **Kanagawa** | Wave, with Lotus as its light half |
| **Everforest** | The medium-contrast variant of each side |
-| **Primer** | The colours behind GitHub's design system. Named for the system, not for GitHub, which has not endorsed anything here |
+| **Primer** | The colors behind GitHub's design system. Named for the system, not for GitHub, which has not endorsed anything here |
Every one has both halves, so the top-bar toggle only ever changes the side and
-never the colours. Accent colours still sit on top of any of them.
+never the colors. Accent colors still sit on top of any of them.
The ten borrowed palettes are the work of their own projects and are used
-under the MIT license — see [NOTICE](NOTICE). Only the published colour values
+under the MIT license — see [NOTICE](NOTICE). Only the published color values
are used, taken from each project's own repository; the values as fetched are
recorded in `.palette-sources/palettes-upstream.md`.
**The shades between those values are derived, and every one is checked.**
ihasmail needs about thirty tokens and these projects publish between twelve
and twenty, so the tiers in between are computed by
-`scripts/build-palettes.py`, which then measures every text colour against the
+`scripts/build-palettes.py`, which then measures every text color against the
surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts
-anything that falls short, towards white on a dark ground and towards black on
+anything that falls short, toward white on a dark ground and toward black on
a light one so the hue survives. The script refuses to write a palette that
would not pass.
That check is not a formality. **Twenty-one of the twenty-two palette halves
needed at least one lift**, because these palettes are designed for code
-editors rather than for prose at this size: Dracula's comment grey is 3.03:1 on
+editors rather than for prose at this size: Dracula's comment gray is 3.03:1 on
its own background, and Rosé Pine's gold is 2.7:1 on Dawn. Shipping them as
published would have quietly ended the WCAG AA claim two sections down.
@@ -1266,7 +1266,7 @@ Stalwart shows the server's English until it is translated.
## Tenants
-A tenant is a separate organisation on the same server — its own people,
+A tenant is a separate organization on the same server — its own people,
domains and limits, and an administrator who manages only what is in it. It is
a Stalwart Enterprise feature. On a server that does not report Enterprise — or
reports no edition at all — the page is only the notice *Tenants are a Stalwart
@@ -1336,7 +1336,7 @@ one question the sign-in page already asks about where it is being used.
It is enforced the same way as the switch below: an untrusted session is sent
no permissions, and the JMAP proxy refuses registry methods beyond the account's
own. The menu still shows **Administration** to an administrator in that
-session, greyed out, with the reason and what to do about it — signing in again
+session, grayed out, with the reason and what to do about it — signing in again
with the box ticked — rather than losing the entry without a word. All the
server tells that session is that the account administers, never what it may do.
@@ -1463,7 +1463,7 @@ server settings is deliberately out of scope.
worker genuinely cannot reach is anything a *tab* holds in memory — and the
API asks for none of it.
- What it cannot reach is a catalogue. The worker is plain JavaScript outside
+ What it cannot reach is a catalog. The worker is plain JavaScript outside
the bundle, with no i18n and no idea which mailbox is the archive, so the app
writes both down for it whenever the language, the account or the folder list
changes. Where there is no such note — between installing a new worker and
@@ -1550,7 +1550,7 @@ costs something to get wrong is the one that assumes the machine is yours.
| Administration | unavailable | available, if the role allows it |
Local storage is gated on that answer for **reads** as well as writes — a
-machine trusted once still has residue, and honouring it would let a previous
+machine trusted once still has residue, and honoring it would let a previous
session's data surface in a later untrusted one. Signing out clears the settings
cache and recent addresses and tears down the push subscription, whichever
answer was given.
@@ -1614,10 +1614,10 @@ This is S/MIME only, and it stops at reading: nothing here signs, encrypts or
decrypts anything.
**What it checks.** For a `multipart/signed` message carrying a PKCS#7
-signature, the exact bytes of the signed part — headers included, canonicalised
+signature, the exact bytes of the signed part — headers included, canonicalized
to CRLF — are hashed and compared against the `messageDigest` the signature
covers, and the signature over the signed attributes is verified with WebCrypto
-against the certificate travelling inside the message. RSA (PKCS#1 v1.5) and
+against the certificate traveling inside the message. RSA (PKCS#1 v1.5) and
ECDSA over P-256, P-384 and P-521 are supported, with SHA-256, SHA-384 or
SHA-512.
@@ -1634,12 +1634,12 @@ authority:
| what happened | what you see |
|---|---|
-| first signed message from this address | *"Signed by X, seen here for the first time"* — grey, and deliberately not congratulatory |
+| first signed message from this address | *"Signed by X, seen here for the first time"* — gray, and deliberately not congratulatory |
| same certificate as before | *"the same signer as before"* — the only case that gets a tick |
| **different certificate than before** | **loud**: both names, and told to check by some other route |
| valid signature, certificate for a different address | **loud**: the signature is not for this sender |
| body changed after signing | **loud**: the signature does not check out |
-| signed, but uncheckable | grey, and careful to say *could not check* rather than *did not check out* |
+| signed, but uncheckable | gray, and careful to say *could not check* rather than *did not check out* |
The pins live in the account's settings file rather than in the browser, so the
same correspondent is not greeted as new on every device — which is what trains
@@ -1689,7 +1689,7 @@ docker run --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= ...
```
`IMMUTABLE=1` is an **assertion the server checks at startup**, not a switch
-that changes behaviour. It refuses to boot if `SESSION_FILE` is still set, or if
+that changes behavior. It refuses to boot if `SESSION_FILE` is still set, or if
the filesystem it is installed on turns out to be writable after all. Without
it, the same misconfiguration is silent — sessions are held in memory and
persisting them is best-effort, so a read-only `/data` costs one warning at the
diff --git a/KNOWN-ISSUES.md b/KNOWN-ISSUES.md
index 1e1f41a..00f4f8e 100644
--- a/KNOWN-ISSUES.md
+++ b/KNOWN-ISSUES.md
@@ -54,7 +54,7 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
- **The Basic credential ihasmail proxies with reaches the admin `x:` methods**, as it already reached the self-service ones. No separate token is involved.
- **An account reads back in the shapes the code expects**: `credentials` as `{"0": {"@type": "Password", …}}`, aliases and group memberships as objects, the disk limit under `quotas.maxDiskQuota`.
- **A new domain gets automatic DKIM straight away** — an Ed25519 and an RSA key, both `active`, with their records already in the zone file — and manual DNS and certificates.
- - **`dnsZoneFile` is BIND text**, one record per line as `name IN TYPE value`, with a long TXT record split into a parenthesised run of quoted chunks. A throwaway domain's file held 18 records and 3 continuation lines; every record parsed and the panel showed 18 rows. The production domains also carry TLSA records, which show as rows like any other.
+ - **`dnsZoneFile` is BIND text**, one record per line as `name IN TYPE value`, with a long TXT record split into a parenthesized run of quoted chunks. A throwaway domain's file held 18 records and 3 continuation lines; every record parsed and the panel showed 18 rows. The production domains also carry TLSA records, which show as rows like any other.
- **`x:DkimSignature/query` accepts a `domainId` filter.**
- **`catchAllAddress` wants a whole address.** A bare local part is refused with `invalidPatch`, *"Invalid email address"*.
- **A domain its keys still name cannot be destroyed**: `objectIsLinked`, with `linkedObjects` listing each as `{"object": "DkimSignature", "id": …}` and no description. Removing through the panel destroys the keys first and then the domain; both were gone afterwards.
@@ -90,7 +90,7 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
- **A role another role builds on cannot be deleted**: `objectIsLinked`, `objectId` `{"object": "Role", …}`, `linkedObjects` naming the child.
- **The defaults** read from `x:Authentication`: users get User; groups get Group; tenant administrators get Tenant Administrator and User; administrators get System Administrator and User.
- **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` through ihasmail's server was confirmed on production after the deploy (2026-09-15, v2026.9.15+pr364)**: `/api/admin/permissions` answered 200 with all 661 permissions, the same list as the 0.16.22 snapshot, and the Roles picker drew them under 60 headings. The four bootstrap roles grant 244 (User), 229 (Group), 50 (Tenant Administrator) and 452 (System Administrator) once their trees are followed.
+ **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 toward 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` through ihasmail's server was confirmed on production after the deploy (2026-09-15, v2026.9.15+pr364)**: `/api/admin/permissions` answered 200 with all 661 permissions, the same list as the 0.16.22 snapshot, and the Roles picker drew them under 60 headings. The four bootstrap roles grant 244 (User), 229 (Group), 50 (Tenant Administrator) and 452 (System Administrator) once their trees are followed.
- **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, all removed. The live run changed the design twice:
@@ -102,15 +102,15 @@ works the same way — and dropped where 0.15 was the whole subject. Support for
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 catalog'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.
-- **A refused password shows the server's reason in English.** Every other refusal from the registry is said in the reader's language: each error type has its own message, and a value one of Stalwart's validators refused — a domain name, an address, an empty field — is recognised by the validator's wording and explained again rather than shown. A password policy is the exception, on purpose. Its rule is the server's to set, so there is nothing to translate it from in advance, and its reason follows a translated sentence rather than being dropped, which would leave "not accepted" with no way to find out why.
+- **A refused password shows the server's reason in English.** Every other refusal from the registry is said in the reader's language: each error type has its own message, and a value one of Stalwart's validators refused — a domain name, an address, an empty field — is recognized by the validator's wording and explained again rather than shown. A password policy is the exception, on purpose. Its rule is the server's to set, so there is nothing to translate it from in advance, and its reason follows a translated sentence rather than being dropped, which would leave "not accepted" with no way to find out why.
- **Administration is off for a device not marked as your own, and for an installation that says so.** Both are enforced by the server rather than hidden by the menu: such a session is sent no permissions, and the JMAP proxy refuses registry methods beyond the account's own. That is worth stating because the proxy otherwise forwards whatever the browser sends, and before these gates an administrator's console could make any registry call their role allowed. For a session that may not administer, the proxy reads a request body only when it could name a registry method — a `"x:` in the text, or a `\u` escape that could spell one — so ordinary mail traffic is forwarded untouched.
-- **All nine translations have never been read by anybody who speaks them.** They were produced by AI against standard dictionaries on 2026-08-31 — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, which with English makes ten languages in the picker — and every one of the nine is marked **Beta** in the picker, with that stated in Settings beside a link for reporting anything that reads wrongly. This is the entry that matters most on this page, because it is the one thing here that cannot be closed by testing: a translation can be complete, consistent, pass every check, and still read like a machine wrote it, and nobody on this project can tell which. What *is* verified is the machinery around them. A missing key renders its English source, so a bad line can simply be deleted; a stale key — one whose English no longer exists — is caught by `npm run i18n:check` rather than sitting in the file looking correct and never being looked up. Plurals are asked of `Intl.PluralRules` rather than assumed, which is why Russian and Ukrainian carry three forms and Japanese and Chinese carry one; supplying `one` for Japanese would have been filling in a distinction the language does not draw. Confirmed live on the deployed instance (2026-08-31) against a 6,289-message mailbox: role folders localise and the ~20 custom folders keep the names their owner gave them, dates and the calendar follow the language, and 6,289 renders as *6289 листувань* — the genitive plural a number ending in nine takes, which is the first time the plural machinery ran on anything but a hand-picked value.
+- **All nine translations have never been read by anybody who speaks them.** They were produced by AI against standard dictionaries on 2026-08-31 — German, Spanish, French, Dutch, Portuguese (Brazil), Russian, Ukrainian, Simplified Chinese and Japanese, which with English makes ten languages in the picker — and every one of the nine is marked **Beta** in the picker, with that stated in Settings beside a link for reporting anything that reads wrongly. This is the entry that matters most on this page, because it is the one thing here that cannot be closed by testing: a translation can be complete, consistent, pass every check, and still read like a machine wrote it, and nobody on this project can tell which. What *is* verified is the machinery around them. A missing key renders its English source, so a bad line can simply be deleted; a stale key — one whose English no longer exists — is caught by `npm run i18n:check` rather than sitting in the file looking correct and never being looked up. Plurals are asked of `Intl.PluralRules` rather than assumed, which is why Russian and Ukrainian carry three forms and Japanese and Chinese carry one; supplying `one` for Japanese would have been filling in a distinction the language does not draw. Confirmed live on the deployed instance (2026-08-31) against a 6,289-message mailbox: role folders localize and the ~20 custom folders keep the names their owner gave them, dates and the calendar follow the language, and 6,289 renders as *6289 листувань* — the genitive plural a number ending in nine takes, which is the first time the plural machinery ran on anything but a hand-picked value.
-- **`npm run i18n:coverage` reported 100% while about two hundred strings rendered English in every language.** It reads JSX text, and it was not wrong about what it measured — none of them were JSX text. They were `toast.error(...)` arguments, `confirmDialog({ title, confirmLabel })` props, `title=` and `aria-label=` attributes, and template literals: every one built from an expression a codemod cannot read. The calendar's own view switcher was the clearest case, spelling its labels `v[0].toUpperCase() + v.slice(1)` — correct English, untranslatable anywhere else, and galling because **Day**, **Week**, **Month** and **Agenda** were already in all nine catalogues and the buttons simply never asked for them. Reported from production, where the switcher stayed English in a Japanese interface. All of them are now wrapped, and `npm run i18n:check` grew a second half (`scripts/i18n-literals.mjs`) that accepts a string wrapped where it is written *or* present as a catalogue key — the constant-table convention, where `SECTIONS` holds `label: "About"` and the render site calls `t(s.label)` — and refuses one that is neither, because that is a string no catalogue can translate however many languages ship. It found twenty more than a hand sweep had. Worth recording as a general lesson rather than an i18n one: a coverage number measures the thing it can see, and the strings it cannot see are exactly the ones nobody is checking. **The check had the same blind spot one level down (2026-09-14).** It looked at `title=`, `aria-label=`, `placeholder=` and `alt=` on elements, but not at props passed to components, so `