Files
inbuxa-webmail/web/src/lib/languages.ts
T
jcoffey-dev 87383440bb German, generated by AI and marked Beta until somebody signs it off
The first language, and the first one where the honest thing to say is not
flattering: no native speaker has read it. That is stated in the app rather
than in a commit nobody reads, because it is the fact a reader needs to judge
what they are looking at. Somebody told a translation is unchecked forgives an
odd sentence and reports it; somebody told it was reviewed reasonably concludes
the product is sloppy. The setting carries a link straight to a report, which
is the whole review process here.

`beta` is a property of the language, not of the catalogue's completeness. A
file can be word-for-word finished and still read like a machine wrote it, and
that is what the flag marks. Removing it is a person's decision.

Register is "Sie", consistently, and written down in the file so the next
language and the next contributor inherit the decision rather than re-take it.
Thunderbird and Outlook use it; ihasmail is as often a company's mail as
somebody's own, where "du" from software the workplace deployed reads as
presumptuous. Where a string can dodge the question it does, which is ordinary
good German UI. The glossary at the top of the file fixes the vocabulary once
-- Posteingang, Papierkorb, Entwürfe, archivieren -- because inconsistency
reads as amateur far more than an imperfect word choice does. "Label" and
"Spam" stay English, since translating them would name things no German mail
client calls that.

766 of 781 strings. The fifteen left are product names, bare URLs and example
addresses, which should stay English and now do.

Two things this turned up that the earlier work had hidden:

Labels defined as module-level constants -- the entire settings navigation,
the theme cards, the swipe choices, the date formats, the sharing permissions
-- are evaluated once, before any catalogue loads, so they could only ever be
English. Nothing failed; the German build simply had an English sidebar. They
are translated where they render now, which keeps the constant as data and
makes its English text the key.

And the codemod's narrowed rule, which let it take 73 more strings last time,
was too broad after all: text stranded after an inline <a> or <strong> came
through as sentence fragments -- ", and what a new account starts on." Eight
of them, rebuilt with tNode so the sentence stays whole and the element is a
named hole a translator can move.

scripts/i18n-catalog-check.mjs is new and earned itself immediately: it found
three keys invented that the code never asks for, which is the silent failure
in a catalogue -- a translation that looks right, is never looked up, and
renders English for ever. It also had to be taught about t(variable), because
it cried wolf 33 times over the constants above, and a check that cries wolf
gets switched off.

Verified in the browser rather than only in tests, which is where the settings
sidebar being English was visible and nowhere else.
2026-08-31 11:06:54 -07:00

59 lines
2.5 KiB
TypeScript

/**
* The interface languages that actually have strings shipped.
*
* Deliberately not `lib/locales.ts`. That list is every tag CLDR can format a
* date in — about 620 of them — and it answers a different question: what
* calendar, clock and numerals to use. This one answers "what language is the
* app written in", and the only honest entries are the ones somebody has
* translated. Offering a language with no strings behind it would set
* `<html lang>` to a language the page is not in, which is worse than not
* offering it: it stops Chrome offering to translate a page the reader cannot
* read.
*
* Wanting German dates with an English interface is a real preference, and so
* is the reverse, which is why `uiLanguage` and `locale` are separate settings
* rather than one.
*
* Adding a language means adding its catalogue and then adding it here, in
* that order. RTL languages — Arabic, Hebrew, Persian — need bidi and layout
* work well beyond strings, so they are not simply a matter of another entry.
*/
export interface UiLanguage {
/** BCP 47, and what `<html lang>` is set to. */
tag: string;
/** The language's name in that language, which is how a picker should read. */
name: string;
/**
* Machine-translated and not yet checked by somebody who speaks it.
*
* Stays true until a native speaker has actually read the catalogue and said
* so. It is not a measure of how complete the file is -- a catalogue can be
* word-for-word finished and still read like a machine wrote it, which is
* the thing this flag is about. Removing it is a deliberate act by a person,
* not something a coverage number earns.
*/
beta?: boolean;
}
export const UI_LANGUAGES: readonly UiLanguage[] = [
{ tag: "en", name: "English" },
{ tag: "de", name: "Deutsch", beta: true },
];
/** Where to report a bad translation. Beta languages depend on it. */
export const TRANSLATION_ISSUE_URL = "https://github.com/Coffey-Labs/ihasmail/issues/new?title=Translation%3A%20";
export const DEFAULT_UI_LANGUAGE = "en";
/**
* The language to actually render in.
*
* A stored preference is only honoured if its strings are still shipped: a
* catalogue can be withdrawn, and an account carrying `de` from another
* machine must not leave this one claiming to be German while showing English.
*/
export function resolveUiLanguage(stored: string | undefined | null): string {
if (!stored) return DEFAULT_UI_LANGUAGE;
return UI_LANGUAGES.some((l) => l.tag === stored) ? stored : DEFAULT_UI_LANGUAGE;
}