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.
71 lines
2.9 KiB
TypeScript
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");
|
|
});
|
|
});
|