From 33dc8be864d798bce21b71a927e30ce5b5be5e03 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Mon, 5 Oct 2026 11:49:12 -0700 Subject: [PATCH] Turkish out of Beta: it comes from a native speaker Hakan Arslan, who contributed the Turkish catalog, is a native speaker and checked it himself. Turkish leaves the Beta mark, the way Dutch did after its native review. README, FEATURES, ROADMAP and KNOWN-ISSUES now say eight Beta translations rather than nine, and the language test counts Turkish among the reviewed ones. No new strings. --- FEATURES.md | 12 ++++++------ KNOWN-ISSUES.md | 4 ++-- README.md | 2 +- ROADMAP.md | 2 +- web/src/lib/__tests__/languages.test.ts | 7 ++++--- web/src/lib/languages.ts | 2 +- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index df568f0..3109a9e 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1038,11 +1038,11 @@ is why they are two settings and not one. | Nederlands | reviewed by a native speaker | | Deutsch · Español · Français · Português (Brasil) | Beta | | Русский · Українська · 简体中文 · 日本語 | Beta | -| Türkçe | Beta, contributed by Hakan Arslan | +| Türkçe | by a native speaker, Hakan Arslan | -**Nine of the ten translations are marked Beta, and the label is not +**Eight of the ten translations are marked Beta, and the label is not modesty.** The catalogs were produced by AI against standard dictionaries, and -apart from Dutch none has been signed off by anybody who speaks the language. That is +apart from Dutch and Turkish none has been signed off 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 language loses the Beta mark when a speaker has read it and said so, which is a @@ -1052,9 +1052,9 @@ mbjboon-netizen), a native speaker, read every string, including the permission labels, and his corrections shipped in September 2026. Turkish came the other way, as a contribution: Hakan Arslan ([harslannet](https://github.com/harslannet)) -translated every string and permission label, made with AI and checked by him, -and sent it in October 2026. He marked it Beta himself, and it keeps the mark -until a reader signs it off. +translated every string and permission label, made with AI and checked by him +as a native speaker, and sent it in October 2026. It ships without the Beta +mark. Two things follow from the design rather than the translation: diff --git a/KNOWN-ISSUES.md b/KNOWN-ISSUES.md index ef28dc7..f0177ec 100644 --- a/KNOWN-ISSUES.md +++ b/KNOWN-ISSUES.md @@ -108,13 +108,13 @@ 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 nine languages are machine translations awaiting native review.** Dutch has been reviewed by a native speaker, and Turkish was checked by the contributor who made it; the others have 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. +- **The permission labels in eight languages are machine translations awaiting native review.** Dutch has been reviewed by a native speaker, and Turkish was made and checked by one; the others have 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 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. -- **Eight of the ten 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. Dutch has since been read and corrected by a native speaker and left Beta in September 2026. Turkish was added in October 2026, contributed by Hakan Arslan ([harslannet](https://github.com/harslannet)), made with AI and checked by him, which makes eleven languages in the picker. The other eight, and Turkish, are 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. +- **Eight of the ten 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. Dutch has since been read and corrected by a native speaker and left Beta in September 2026. Turkish was added in October 2026, contributed by Hakan Arslan ([harslannet](https://github.com/harslannet)), made with AI and checked by him as a native speaker, which makes eleven languages in the picker. Turkish ships without the Beta mark; the other eight are 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 catalogs 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 catalog 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 catalog 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 `` passed. It also accepted a JSX literal that was a catalog key, although no component here runs its props through `t()`, so 19 strings with translations in every catalog (Report spam, Mark as read, Add star, Save…) still rendered in English. And the script only exited non-zero with `--check`, which `npm run i18n:check` never passed, so it could print a finding without failing. Component props are checked now, a key no longer excuses a literal in an attribute, and both halves run with `--check`. That turned up 28 strings, all fixed: 19 wrapped, and 9 that needed new keys in all nine catalogs. English built with a template literal inside an attribute, such as ``aria-label={`Remove ${email}`}``, was the last gap. It can't be a catalog key as written. Since 2026-09-14 the check flags any template literal in one of these positions that has words between its values, and the twelve that existed are now keys with placeholders. They were the quota bar, the address menu, a folder's unread count, the recipient chips, the contact editor's title, shared calendars and address books, the date and time fields, the attachment fallback name, and the free/busy bar. That bar showed the raw JMAP value (`confirmed`) in every language. diff --git a/README.md b/README.md index 1e25108..90c5e99 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ layout, on [ihasmail.org](https://ihasmail.org/#screenshots). - **Settings that follow the account**, kept in the account's own storage on Stalwart - **On a phone** — swipe to archive or delete, pull to refresh, hold to select - **Administration** — a dashboard, accounts, groups, mailing lists, roles, tenants and domains, each shown only when the Stalwart role allows it -- **Eleven interface languages and twelve themes** — Dutch has been read by a native speaker; the other nine translations are marked Beta until one has +- **Eleven interface languages and twelve themes** — Dutch and Turkish come from native speakers; the other eight translations are marked Beta until one has read them - **Platform** — installable PWA, Web Push, `mailto:` handler, no credentials in the browser, strict CSP The long version is [FEATURES.md](FEATURES.md) and diff --git a/ROADMAP.md b/ROADMAP.md index a7035cb..ac2036b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,7 +13,7 @@ See [KNOWN-ISSUES.md](KNOWN-ISSUES.md) for what is built but worth knowing about - **A scheduling view of its own**, for asking "when is everyone free next week?" without an event in hand. The grid itself is built and lives in the event editor — a row per participant, steppable, and clickable to place the event — which is where the question gets asked while you are arranging something. What is not built is the same thing as a destination you can visit with nothing in progress. Came out of [#172](https://git.coffeylabs.org/coffey-labs/ihasmail-github-archive/issues/172), which asked for a separate view and is closed by the panel: the reasoning for putting it in the editor is that a separate surface can only ever tell you a time you then retype, whereas one beside the event can set it. It stays here rather than in the tracker because nobody has yet said they want to ask the question on its own. - **Per-message actions from the message list on a touchscreen.** Reply, Forward and compose-as-new are on the list row's context menu, which is a right-click — and holding a row on a phone starts selection instead, so none of them are reachable there. They are all available inside a thread, which is where the actions on a single message belong; what is missing is the shortcut from the list. Fixing it means deciding what a long press should do when it already means something, which is a bigger question than the actions themselves. - Snooze (nothing in JMAP or Stalwart supports it, and ihasmail never stores a password, so nothing could act on a mailbox while you are away) -- **A translation anybody has checked.** The translations themselves shipped on 2026-08-31 and are no longer on this page: nine of them, alongside English, and the extraction that had always been the hard half is done — see [FEATURES.md](FEATURES.md#interface-language). What is *not* done is the other half, and it is the half that cannot be bought or automated. All nine were produced by AI against standard dictionaries, and **only Dutch has been read by anybody who speaks the language** — Michael ([mbjboon82](https://git.coffeylabs.org/mbjboon82), also mbjboon-netizen) reviewed every string and it left Beta in September 2026. Turkish followed in October 2026 as a contribution from Hakan Arslan ([harslannet](https://github.com/harslannet)), made with AI and checked by him, and is Beta as well. The other eight have not been read, which is exactly where a bad translation does harm rather than merely looking untidy. They ship marked Beta, with that said in Settings and a link for reporting anything wrong, because shipping them quietly would ask people to trust text nobody has checked. A language loses the Beta mark when a speaker reads it and says so — a deliberate act by a person, not something a coverage percentage earns. If you speak one of the other nine and are willing to read a few hundred strings, that is the single most useful thing anyone could contribute right now. +- **A translation anybody has checked.** The translations themselves shipped on 2026-08-31 and are no longer on this page: nine of them, alongside English, and the extraction that had always been the hard half is done — see [FEATURES.md](FEATURES.md#interface-language). What is *not* done is the other half, and it is the half that cannot be bought or automated. All nine were produced by AI against standard dictionaries, and **only Dutch has been read by anybody who speaks the language** — Michael ([mbjboon82](https://git.coffeylabs.org/mbjboon82), also mbjboon-netizen) reviewed every string and it left Beta in September 2026. Turkish followed in October 2026 as a contribution from Hakan Arslan ([harslannet](https://github.com/harslannet)), made with AI and checked by him as a native speaker, and it ships without the Beta mark too. The other eight have not been read, which is exactly where a bad translation does harm rather than merely looking untidy. They ship marked Beta, with that said in Settings and a link for reporting anything wrong, because shipping them quietly would ask people to trust text nobody has checked. A language loses the Beta mark when a speaker reads it and says so — a deliberate act by a person, not something a coverage percentage earns. If you speak one of the other eight and are willing to read a few hundred strings, that is the single most useful thing anyone could contribute right now. - **Right-to-left languages.** Arabic, Hebrew and Persian are held back deliberately, and not for want of translators. RTL is bidi and layout work throughout — mirrored panes, gesture directions, icon sides, the message list's own geometry — and a catalog without it produces a page that is translated and unusable. Adding one is not another entry in the picker. - **Two-factor sign-in.** Today an account with 2FA must use an app password (see [Quick start](README.md#quick-start-docker)), and Settings › Security offers no way to switch 2FA *on* — only off, for an account that already has it. Supporting a TOTP code directly means implementing OAuth: Stalwart offers the authorization-code and device flows and no password grant, so ihasmail would hand sign-in to Stalwart's own login and come back with a token. That is a better security posture than the sealed password it holds now — a refresh token rather than a credential — but it replaces ihasmail's own sign-in page for those users and may need an OAuth client registered. Came out of [#75](https://git.coffeylabs.org/coffey-labs/ihasmail-github-archive/issues/75), which is closed: what was reported there was a sign-in refused with nothing but "Invalid credentials", and that was fixed by saying what is actually happening and pointing at app passwords. The OAuth work it uncovered is tracked here rather than as an open issue, so there is no ticket to watch for it. - **Signing and encrypting mail.** *Reading* a signature is built: S/MIME signed mail is checked as it is read, and the signer is remembered so a change is called out — see [Checking a signature](FEATURES.md#checking-a-signature). What is not built is anything that produces a signature or touches ciphertext, and the reason is not Stalwart. This is client work over the message body: JMAP hands over the MIME blob and the rest is ours. diff --git a/web/src/lib/__tests__/languages.test.ts b/web/src/lib/__tests__/languages.test.ts index 6fb35f9..5471174 100644 --- a/web/src/lib/__tests__/languages.test.ts +++ b/web/src/lib/__tests__/languages.test.ts @@ -33,9 +33,10 @@ describe("resolveUiLanguage", () => { // Not a completeness measure. A catalog can be word-for-word finished // and still read like a machine wrote it, which is what this marks. // English is the source and Dutch was signed off by a native speaker in - // September 2026. Every other language is unreviewed, and stays marked - // until a person says otherwise. - const reviewed = new Set(["en", "nl"]); + // September 2026. Turkish was made and checked by a native speaker (Hakan + // Arslan, October 2026). Every other language is unreviewed, and stays + // marked until a person says otherwise. + const reviewed = new Set(["en", "nl", "tr"]); for (const l of UI_LANGUAGES) { if (reviewed.has(l.tag)) expect(l.beta).toBeUndefined(); else expect(l.beta).toBe(true); diff --git a/web/src/lib/languages.ts b/web/src/lib/languages.ts index 5bdde70..30fa373 100644 --- a/web/src/lib/languages.ts +++ b/web/src/lib/languages.ts @@ -46,7 +46,7 @@ export const UI_LANGUAGES: readonly UiLanguage[] = [ { tag: "ru", name: "Русский", beta: true }, { tag: "uk", name: "Українська", beta: true }, { tag: "zh-Hans", name: "简体中文", beta: true }, - { tag: "tr", name: "Türkçe", beta: true }, + { tag: "tr", name: "Türkçe" }, ]; /** Where to report a bad translation. Beta languages depend on it. */ -- 2.54.0