import Link from "next/link"; import { readFileSync } from "node:fs"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it, vi } from "vitest"; import { LocaleBoundary } from "@/components/i18n/locale-provider"; import { LanguageDropdown } from "@/components/i18n/language-dropdown"; import { Text, Localized, ExportText, ConditionLabel, SectionTitle } from "@/components/i18n/text"; import { GuideImageExport } from "@/components/public/guide-image-export"; import { createLocaleStore } from "./locale-store"; import { isLocale, languages, localeStorageKey } from "./locales"; import { messages, translate } from "./messages"; import { formatGuideValidationIssues } from "@/lib/guides/validation-messages"; function memoryStorage(initial?: string) { const values = new Map(); if (initial) values.set(localeStorageKey, initial); return { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => { values.set(key, value); } }; } describe("personal language preference", () => { it("renders Thai on the server and restores a saved browser choice on subscription", () => { const storage = memoryStorage("en"); const store = createLocaleStore(() => storage); expect(store.getServerSnapshot()).toBe("th"); const listener = vi.fn(); const unsubscribe = store.subscribe(listener); expect(store.getSnapshot()).toBe("en"); store.setLocale("th"); expect(storage.getItem(localeStorageKey)).toBe("th"); expect(listener).toHaveBeenCalledTimes(2); unsubscribe(); store.setLocale("en"); expect(listener).toHaveBeenCalledTimes(2); }); it("keeps choices independent between users' browsers", () => { const first = createLocaleStore(() => memoryStorage()); const second = createLocaleStore(() => memoryStorage()); first.subscribe(() => {}); second.subscribe(() => {}); first.setLocale("en"); expect(first.getSnapshot()).toBe("en"); expect(second.getSnapshot()).toBe("th"); }); it("falls back to Thai for invalid storage and still changes language when storage is blocked", () => { const invalid = createLocaleStore(() => memoryStorage("invalid")); invalid.subscribe(() => {}); expect(invalid.getSnapshot()).toBe("th"); const blocked = createLocaleStore(() => { throw new Error("Storage disabled"); }); blocked.subscribe(() => {}); blocked.setLocale("en"); expect(blocked.getSnapshot()).toBe("en"); expect(isLocale("invalid")).toBe(false); }); }); describe("interface translations", () => { it("has matching named placeholders and English translations without Thai interface text", () => { for (const [key, value] of Object.entries(messages)) { const placeholders = (text: string) => [...text.matchAll(/\{(\w+)\}/gu)].map((match) => match[1]).sort(); expect(placeholders(value.en), key).toEqual(placeholders(value.th)); expect(value.en, key).not.toMatch(/[\u0e00-\u0e7f]/u); } }); it("translates app-owned API messages with interpolated values without changing their response shape", () => { expect(translate("สร้างบัญชี buzz@example.com แล้ว", {}, "en")).toBe("Account created for buzz@example.com"); expect(translate("ดึงข้อมูลตัวละคร 4/10", {}, "en")).toBe("Fetching characters 4/10"); expect(translate("{character} ไม่รองรับอาวุธ {weapon}", { character: "Raiden", weapon: "Bow" }, "en")).toBe("Raiden cannot use Bow."); }); it("localises validation while preserving an authored Thai team name", () => { expect(formatGuideValidationIssues("team", [{ path: ["teams", 0, "members", 1, "damage"], message: "Invalid input" }], { locale: "en", teamNames: ["ทีมของฉัน"] })).toEqual(["ทีมของฉัน · Character 2: Enter damage of zero or more."]); }); it("updates text and accessible attributes through the same locale context", () => { const html = renderToStaticMarkup(Home); expect(html).toContain("Search guides"); expect(html).toContain('aria-label="Back to all guides"'); expect(html).toContain("Talent Priority"); }); it("preserves custom content while translating only generated headings and conditions", () => { const html = renderToStaticMarkup(); expect(html).toContain('value="ชื่อภาษาไทย"'); expect(html).toContain("เงื่อนไขของฉัน"); expect(html).toContain("Condition 2"); expect(html).toContain("หัวข้อของฉัน"); }); it.each(["th", "en"] as const)("shows a labelled dropdown with the selected SVG flag in %s", (locale) => { const language = languages.find((item) => item.code === locale)!; const html = renderToStaticMarkup(); expect(html).toContain(language.name); expect(html).toContain(language.flag); expect(html).toContain('aria-haspopup="menu"'); for (const item of languages) expect(readFileSync(`public${item.flag}`, "utf8")).toContain(" { const html = renderToStaticMarkup(No notes); expect(html).toContain(locale === "en" ? "Export without note" : "ส่งออกภาพโดยไม่รวมโน้ต"); expect(html.match(/