French, on the same terms as German

Second of Phase 1. Generated by AI, unreviewed, marked Beta, with the report
link in Settings doing the job a native speaker would otherwise do. 781 of 796
strings; the fifteen left are product names, bare URLs and example addresses,
which should stay English.

Register is "vous", following the "Sie" decision and for the same reason: a
mail client a workplace deployed has no business addressing anybody as "tu".

One terminology decision goes the opposite way to German, deliberately.
"Label" stays English in German because no German client translates it, and
becomes "Libellé" in French because Gmail did and a French reader will meet it
there. The rule being followed is "use what the reader will find elsewhere",
not "always translate" or "never" -- which only looks inconsistent if the rule
is mistaken for the outcome.

French typography: the narrow no-break space before ? ! and : is the rule and
is deliberately not used. It is invisible in a diff, trivially lost in an
editor, and no French webmail actually ships it. Guillemets are used, because
those are visible and do read as wrong when missing.

Two things worth recording from doing this a second time.

The folder-context separator arrived as a raw U+0004 byte rather than the
\\u0004 escape the German file uses. It would have worked -- TypeScript accepts
it -- and it is invisible in an editor and in a diff, which is exactly why the
German catalogue writes it as an escape. Converted, so both files say the same
thing in the same way.

And the language tests named a real language as their example of one that is
not shipped, so shipping German broke them, and shipping French broke them
again. Both times the failure was the test being out of date rather than
anything wrong. They derive an unshipped tag now, and assert over every shipped
language rather than a hardcoded pair, so the third and fourth languages will
not repeat it.
This commit is contained in:
2026-08-31 11:44:40 -07:00
parent 8cbc04e730
commit 53c38ed3c3
4 changed files with 888 additions and 10 deletions
+10 -4
View File
@@ -22,16 +22,22 @@ describe("resolveUiLanguage", () => {
// The account travels between machines and can outlive a catalogue. A
// page that says lang="fr" while rendering English is worse than one that
// admits to English: it stops the reader translating it themselves.
expect(resolveUiLanguage("fr")).toBe("en");
// Derived rather than named, so shipping another language does not turn
// this into a failing test that is really just out of date.
const unshipped = ["cy", "is", "mt", "eu"].find((tag) => !UI_LANGUAGES.some((l) => l.tag === tag))!;
expect(resolveUiLanguage(unshipped)).toBe("en");
expect(resolveUiLanguage("xx-XX")).toBe("en");
});
it("carries the Beta flag until a person has signed the language off", () => {
// Not a completeness measure. A catalogue can be word-for-word finished
// and still read like a machine wrote it, which is what this marks.
const de = UI_LANGUAGES.find((l) => l.tag === "de");
expect(de?.beta).toBe(true);
expect(UI_LANGUAGES.find((l) => l.tag === "en")?.beta).toBeUndefined();
// Every shipped language except English is unreviewed, and stays marked
// until a person says otherwise.
for (const l of UI_LANGUAGES) {
if (l.tag === "en") expect(l.beta).toBeUndefined();
else expect(l.beta).toBe(true);
}
});
it("honours one that is", () => {
+1
View File
@@ -38,6 +38,7 @@ export interface UiLanguage {
export const UI_LANGUAGES: readonly UiLanguage[] = [
{ tag: "en", name: "English" },
{ tag: "de", name: "Deutsch", beta: true },
{ tag: "fr", name: "Français", beta: true },
];
/** Where to report a bad translation. Beta languages depend on it. */