Files
ihasmail/web/src/lib/languages.ts
T
jcoffey-dev f9d521a412 Russian, and the first real use of the plural machinery
781 of 796 strings. Generated by AI, unreviewed, marked Beta.

This is the catalogue plural() was designed for. Russian needs three forms
where English has two, and the choice is not a question about the number 1:
1 is "one", 2-4 are "few", 5-20 are "many", 11-14 are "many" despite ending in
1-4, and 21 is "one" again. Intl.PluralRules knows all of that; a two-form
assumption would have shipped "5 письмо" and read as machine output however
good the vocabulary was.

Tested against the shipped catalogue rather than a fixture -- 1, 2, 3, 5, 11,
21, 22, 25 and 0 -- plus a check that every counted string carries all four
categories, because a missing "few" falls back to "other" silently and is
grammatical often enough to go unnoticed.

"Выбрано: {n}" for the selection count rather than an agreeing form: the
impersonal construction sidesteps agreement entirely and is what Russian
interfaces actually do there.

Register is "вы", lowercase. Capitalised «Вы» is correspondence style and
reads as a letter rather than as software, so it would be a small constant
wrongness on every screen. Most of the interface avoids the question anyway,
because Russian UI convention is the infinitive for actions.

«Письмо» rather than «сообщение» for a mail message, which is what Russian
mail clients call one; «сообщение» reads as a chat message. «Ярлык» for label,
Gmail's word in Russian -- a fifth answer to the same rule about using what the
reader will meet elsewhere.

A stray CJK character got typed into one Russian sentence during drafting and
was caught by sweeping the file for anything outside the expected scripts. Not
a mistake a spellcheck would find, and not one a reader would forgive.
2026-08-31 12:38:51 -07:00

64 lines
2.7 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 },
{ tag: "es", name: "Español", beta: true },
{ tag: "fr", name: "Français", beta: true },
{ tag: "nl", name: "Nederlands", beta: true },
{ tag: "pt-BR", name: "Português (Brasil)", beta: true },
{ tag: "ru", name: "Русский", 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;
}