freezmo/tests/i18n-test.mjs
Martin Dvorak 67e2cefcfe Aero goes underwater with rising bubbles; floating cookie notice; Czech title
Frutiger Aero, closer to the mock:
- The scene is water now: sun shafts through a bright surface, caustic
  patches, reeds on the left, a city silhouette on the right, and ten soap
  bubbles that actually rise (own keyframes, varied sizes/speeds/sway; the
  spans are display:none outside aero so they cost nothing elsewhere, and
  they sit still under reduced motion).
- Pale blue glass at 0.8: still reads as glass, but dark navy text clears
  4.5:1 over the water. The phone tab bar mixes the sheet glass instead of
  the page — in aero the page *is* the water, which left its labels at 3.9:1.
- The aero page-level text (muted/subtle) is much darker for the same reason.
- The PWA status bar follows the new water blue (#2fa7e6).

The cookie notice is floating glass now (blurred backdrop, rim, shadow): as a
plain card it read as part of the page background instead of a notice.

Czech: "Tvoje mraznička" -> "Tvůj mrazák" (library title), test updated.

Contrast: 0 below-minimum findings and 0 axe violations across every screen,
all three themes. Version 0.2.1, SW -> freezmo-v55.
2026-10-13 05:46:53 +00:00

71 lines
3.6 KiB
JavaScript

/**
* i18n suite: EN/CS dictionary parity, the locale switch, Czech copy on the
* desktop shell and the phone tab bar, and Czech plurals.
*/
import { readFileSync } from "node:fs";
import path from "node:path";
import { BASE, DECK, REPO, launchBrowser, makeChecker, makePage, seed } from "./lib.mjs";
const { check, finish } = makeChecker();
const browser = await launchBrowser();
/* 1. dictionary parity ----------------------------------------------- */
{
const source = readFileSync(path.join(REPO, "lib/i18n-dict.ts"), "utf8");
const split = source.indexOf("const cs");
const pattern = /^\s{2}"([^"]+)":/gm;
const en = [...source.slice(0, split).matchAll(pattern)].map((match) => match[1]);
const cs = [...source.slice(split).matchAll(pattern)].map((match) => match[1]);
const missingInCs = en.filter((key) => !cs.includes(key));
const missingInEn = cs.filter((key) => !en.includes(key));
check("key parity: same keys in both dictionaries", missingInCs.length === 0 && missingInEn.length === 0, JSON.stringify({ missingInCs, missingInEn }).slice(0, 200));
check("dictionary is substantial", en.length > 700, String(en.length));
}
/* 2. Czech desktop shell --------------------------------------------- */
{
const { context, page } = await makePage(browser, { width: 1280, height: 900, deviceScaleFactor: 1 });
await seed(page, { locale: "cs", decks: [DECK] });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(900);
check("library title is Czech", await page.getByRole("heading", { name: "Tvůj mrazák" }).isVisible());
check("sidebar says Všechny sady", await page.getByRole("button", { name: /Všechny sady/ }).first().isVisible());
check("sidebar says Nezařazené", await page.getByRole("button", { name: /Nezařazené/ }).first().isVisible());
check("deck card CTA is Studovat", (await page.getByRole("link", { name: "Studovat" }).count()) === 1);
check("Czech plural on the card (2 aktivity)", (await page.getByText("2 aktivity").filter({ visible: true }).count()) >= 1);
check("review card is Czech", await page.getByText("Chytré opakování").isVisible());
check("desktop has no tab bar visible", !(await page.locator("nav.tabbar").isVisible()));
// Locale switch from the settings dialog (desktop footer).
await page.locator("aside").getByRole("button", { name: "Nastavení" }).click();
const dialog = page.getByRole("dialog");
await dialog.waitFor({ timeout: 6000 });
await page.waitForTimeout(600);
await dialog.locator(".segmented").first().getByRole("button", { name: /English/ }).click();
await page.waitForTimeout(500);
await page.keyboard.press("Escape");
await page.waitForTimeout(600);
check("locale switch to English updates the UI", await page.getByRole("heading", { name: "Your icebox" }).isVisible());
await context.close();
}
/* 3. Czech phone tab bar + review card ------------------------------- */
{
const { context, page } = await makePage(browser);
await seed(page, { locale: "cs", decks: [DECK] });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(900);
const tabTexts = (await page.locator("nav.tabbar").locator("a, button").allInnerTexts()).map((text) => text.trim());
check(
"cs tab labels are the short ones",
JSON.stringify(tabTexts) === JSON.stringify(["Knihovna", "Pokrok", "Nová sada", "Zkouška", "Nastavení"]),
JSON.stringify(tabTexts),
);
check("cs review card present", await page.getByText("Chytré opakování").isVisible());
check("cs exam tab label is not the long one", !tabTexts.includes("Zkouškový režim"));
await context.close();
}
await browser.close();
finish();