Files
template-app/modules/documents/__tests__/i18n.test.ts
T

107 lines
4.1 KiB
TypeScript

// Every string a page of the module shows is a message in English and in Lithuanian (spec §10.4), the
// Lithuanian plural forms are real ones, and no component of the module carries a string of its own.
import { readdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { buildRegistry } from "@conusai-kit/core"
import { createI18n, lintUiStrings } from "@conusai-kit/i18n"
import { describe, expect, it } from "vitest"
import { appModules } from "../../index.js"
import { PAGE_CLASSES } from "../contract/constants.js"
import { exportLabels, listLabels, reviewLabels, uploadLabels } from "../ui/labels.js"
// The messages as the app has them: from the manifest, flattened and merged by the kit.
const catalogues = buildRegistry([...appModules]).catalogues
const en = catalogues.en?.documents ?? {}
const lt = catalogues.lt?.documents ?? {}
/** The keys a builder asks for, recorded by running it with a lookup that notes each one. */
function keysUsed(): string[] {
const asked: string[] = []
const t = (key: string) => {
asked.push(key)
return key
}
uploadLabels(t, "failed")
listLabels(t)
reviewLabels(t, { failed: "failed", aiDisclosure: "ai", pageWord: "page" })
exportLabels(t, "failed")
return asked
}
describe("the documents catalogues", () => {
it("hold the same keys in English and in Lithuanian", () => {
expect(Object.keys(lt).sort()).toEqual(Object.keys(en).sort())
})
it("hold no empty message", () => {
for (const [key, text] of [...Object.entries(en), ...Object.entries(lt)]) expect(text.trim(), key).not.toBe("")
})
it("hold every message the components' labels ask for, in both languages", () => {
const asked = keysUsed()
expect(asked.length).toBeGreaterThan(30)
for (const key of asked) {
expect(en[key as keyof typeof en], `en is missing documents.${key}`).toBeTruthy()
expect(lt[key as keyof typeof lt], `lt is missing documents.${key}`).toBeTruthy()
}
})
it("hold no message nothing asks for, so a renamed label does not leave a dead string behind", () => {
// The pages ask for these directly (a heading, a sentence), not through a label builder.
const direct = new Set([
"title",
"intro",
"review.title",
"review.back",
"review.waiting",
"review.pendingCount",
"review.approvedCount",
"review.skippedTitle",
"review.skippedPage",
...PAGE_CLASSES.map((pageClass) => `review.pageClass.${pageClass}`),
])
const asked = new Set([...keysUsed(), ...direct])
expect(Object.keys(en).filter((key) => !asked.has(key))).toEqual([])
})
})
describe("the Lithuanian plural forms", () => {
const count = (locale: "en" | "lt", n: number) =>
createI18n(locale, { en: { documents: en }, lt: { documents: lt } }).message("documents.review.pendingCount", {
count: n,
})
it("select one, few and other the way Lithuanian counts", () => {
expect(count("lt", 1)).toBe("1 eilutė peržiūrai")
expect(count("lt", 2)).toBe("2 eilutės peržiūrai")
expect(count("lt", 9)).toBe("9 eilutės peržiūrai")
expect(count("lt", 10)).toBe("10 eilučių peržiūrai")
expect(count("lt", 21)).toBe("21 eilutė peržiūrai")
})
it("keep English to its two forms", () => {
expect(count("en", 1)).toBe("1 line to review")
expect(count("en", 2)).toBe("2 lines to review")
expect(lt["review.pendingCount"]).toContain("few {")
expect(en["review.pendingCount"]).not.toContain("few {")
})
})
describe("the components", () => {
const dir = new URL("../ui/", import.meta.url).pathname
const sources = readdirSync(dir)
.filter((name) => name.endsWith(".tsx"))
.map((name) => ({ name, text: readFileSync(join(dir, name), "utf8") }))
it("carry no hard-coded user-facing string", () => {
expect(sources.length).toBeGreaterThanOrEqual(4)
for (const { name, text } of sources) expect(lintUiStrings(text, name), name).toEqual([])
})
it("carry no colour literal: the brand tokens are the only source of colour", () => {
for (const { name, text } of sources)
expect(text, name).not.toMatch(/#[0-9a-fA-F]{3,8}\b|\brgb\(|\bhsl\(|\boklch\(/)
})
})