Files
ihasmail/web/src/store/__tests__/lang-attribute.test.ts
T
jcoffey-dev 53c38ed3c3 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.
2026-08-31 11:44:40 -07:00

71 lines
2.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { applyLang, DEFAULT_SETTINGS } from "@/store/settings";
import { UI_LANGUAGES } from "@/lib/languages";
/**
* `<html lang>` has to be right *before first paint*, not after mount.
*
* Chrome decides whether to offer a translation from the language it detects
* against the language the page declares. A `lang` patched in from a
* `useEffect` leaves a window where the two disagree, and that window is
* enough to raise the prompt on a page that was already in the reader's
* language — after which accepting it rewrites the DOM under React.
*
* Two halves, and they are different claims: the served HTML already says so,
* and the stored preference is applied without waiting for a render.
*/
describe("the served HTML", () => {
it("declares a language in the markup itself, not from script", () => {
// jsdom rewrites import.meta.url to an http URL, so resolve from the
// Vite root instead of relative to this file.
const html = readFileSync(resolve(process.cwd(), "index.html"), "utf8");
expect(html).toMatch(/<html[^>]*\slang="en"/);
});
});
describe("applyLang", () => {
beforeEach(() => {
document.documentElement.removeAttribute("lang");
});
it("puts the resolved language on the document", () => {
applyLang({ ...DEFAULT_SETTINGS, uiLanguage: "en" });
expect(document.documentElement.lang).toBe("en");
});
it("serves every language whose catalogue is shipped", () => {
for (const l of UI_LANGUAGES) {
applyLang({ ...DEFAULT_SETTINGS, uiLanguage: l.tag });
expect(document.documentElement.lang).toBe(l.tag);
}
});
it("falls back to English rather than claiming a language it cannot render", () => {
/*
* The tag is derived, not written down. Naming a real language here means
* the test breaks the day that language ships -- which it did, twice, for
* German and then French, each time reporting a failure that was really
* the test being out of date.
*/
const unshipped = ["cy", "is", "mt", "eu"].find((tag) => !UI_LANGUAGES.some((l) => l.tag === tag));
expect(unshipped).toBeDefined();
applyLang({ ...DEFAULT_SETTINGS, uiLanguage: unshipped! });
expect(document.documentElement.lang).toBe("en");
});
it("is applied from the module the app imports before it renders", async () => {
/*
* main.tsx imports App, which reaches this store, before it calls
* createRoot().render(). Re-importing runs the module's own side effects
* against a document that has just had the attribute stripped, which is
* the closest a test runner gets to "was it set before the first paint".
*/
document.documentElement.removeAttribute("lang");
vi.resetModules();
await import("@/store/settings");
expect(document.documentElement.lang).toBe("en");
});
});