| 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { |
| 3 | DICTIONARY_LOCALES, |
| 4 | EN_CHROME, |
| 5 | EN_DOCS_GUIDE, |
| 6 | EN_DOCS_CONSTITUTION, |
| 7 | EN_DOCS_HOOKS, |
| 8 | EN_DOCS_MCP, |
| 9 | EN_DOCS_RUNTIME_API, |
| 10 | EN_DOCS_SANDBOX, |
| 11 | EN_DOCS_SUBAGENTS, |
| 12 | EN_DOCS_WEB, |
| 13 | EN_DOCS_COMPUTERS, |
| 14 | EN_DOCS_AUTH, |
| 15 | EN_DOCS_TRUST, |
| 16 | EN_COMPUTER_USE, |
| 17 | EN_CHANGELOG, |
| 18 | EN_DOCS_SHELL, |
| 19 | EN_DOCS_TROUBLESHOOTING, |
| 20 | EN_HOME, |
| 21 | fill, |
| 22 | getChrome, |
| 23 | getDocsGuide, |
| 24 | getDocsConstitution, |
| 25 | getDocsHooks, |
| 26 | getDocsMcp, |
| 27 | getDocsRuntimeApi, |
| 28 | getDocsSandbox, |
| 29 | getDocsSubagents, |
| 30 | getDocsWeb, |
| 31 | getDocsComputers, |
| 32 | getDocsAuth, |
| 33 | getDocsTrust, |
| 34 | getComputerUse, |
| 35 | getChangelog, |
| 36 | getDocsShell, |
| 37 | getDocsTroubleshooting, |
| 38 | getHome, |
| 39 | pickText, |
| 40 | splitToken, |
| 41 | splitTokens, |
| 42 | } from "./dictionaries"; |
| 43 | import { locales, partialLocales } from "./config"; |
| 44 | import type { ChromeDict, HomeDict } from "./dictionaries/types"; |
| 45 | |
| 46 | /** |
| 47 | * Keys whose value is a mark, a proper noun, or a formatting tag rather |
| 48 | * than prose — a locale sharing English's value here is correct, not a |
| 49 | * missing translation. |
| 50 | */ |
| 51 | const NON_PROSE_KEYS = new Set([ |
| 52 | "wordmarkSeal", |
| 53 | "dateLocale", |
| 54 | "githubFallback", |
| 55 | "tickerLiveTag", |
| 56 | ]); |
| 57 | |
| 58 | /** Chrome keys that are real sentences/labels and must be translated. */ |
| 59 | const CHROME_PROSE_KEYS = [ |
| 60 | "skipToContent", |
| 61 | "navDocs", |
| 62 | "navProduct", |
| 63 | "navCommunity", |
| 64 | "navPrimaryAria", |
| 65 | "navHomeAria", |
| 66 | "wordmarkTag", |
| 67 | "starsAria", |
| 68 | "traceLabel", |
| 69 | "traceTabsAria", |
| 70 | "menuOpen", |
| 71 | "menuClose", |
| 72 | "themeAria", |
| 73 | "themeTitle", |
| 74 | "footerTagline", |
| 75 | "footerProduct", |
| 76 | "footerProject", |
| 77 | "footerGuide", |
| 78 | "footerCanonicalSource", |
| 79 | "footerReleasesLink", |
| 80 | "switcherLabel", |
| 81 | "switcherSwitchTo", |
| 82 | "partialBadge", |
| 83 | // Ticker chrome. The repository's own record (titles, handles, tags) stays |
| 84 | // verbatim, but the verbs the strip prints around it are copy. |
| 85 | "tickerMerged", |
| 86 | "tickerOpened", |
| 87 | "tickerClosed", |
| 88 | "tickerReleased", |
| 89 | "tickerFirstContribution", |
| 90 | "tickerBy", |
| 91 | "tickerAria", |
| 92 | ] as const satisfies readonly (keyof ChromeDict)[]; |
| 93 | |
| 94 | /** |
| 95 | * Locale/key pairs whose English-identical value is a native loanword in |
| 96 | * that locale, not a missing translation — asserted below so the equality |
| 97 | * is deliberate and visible. German "Community" matches the TUI pack |
| 98 | * (crates/tui/locales/de.json: "Community & Mitwirken"). |
| 99 | */ |
| 100 | const CHROME_LOANWORDS: Record<string, readonly string[]> = { |
| 101 | de: ["navCommunity"], |
| 102 | }; |
| 103 | |
| 104 | /** Home keys that are real sentences and must be translated. */ |
| 105 | const HOME_PROSE_KEYS = [ |
| 106 | "metaTitle", |
| 107 | "metaDescription", |
| 108 | "heroTitle", |
| 109 | "heroIntro", |
| 110 | "getCodewhale", |
| 111 | "exploreProduct", |
| 112 | "shotPreview", |
| 113 | "shotBuild", |
| 114 | "screenshotAlt", |
| 115 | "chapterTerminal", |
| 116 | "chapterTerminalTitle", |
| 117 | "gainHeading", |
| 118 | "gainLede", |
| 119 | "chapterModels", |
| 120 | "modelsHeading", |
| 121 | "modelsBody", |
| 122 | "modelsLink", |
| 123 | "startHeading", |
| 124 | "startLede", |
| 125 | "startGuideLink", |
| 126 | "startVocabularyLink", |
| 127 | "chapterAccount", |
| 128 | "availabilityHeading", |
| 129 | "availabilityLede", |
| 130 | "availabilityNote", |
| 131 | "accountLink", |
| 132 | "surfacesHeading", |
| 133 | "runtimeLink", |
| 134 | "installBandHeading", |
| 135 | "installGuideLink", |
| 136 | "communityHeading", |
| 137 | "communityBody", |
| 138 | "communityLinksAria", |
| 139 | ] as const satisfies readonly (keyof HomeDict)[]; |
| 140 | |
| 141 | function templateTokens(value: string): string[] { |
| 142 | return [...value.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).sort(); |
| 143 | } |
| 144 | |
| 145 | function flattenStrings(dict: object): Record<string, string> { |
| 146 | const out: Record<string, string> = {}; |
| 147 | for (const [key, value] of Object.entries(dict)) { |
| 148 | if (typeof value === "string") { |
| 149 | out[key] = value; |
| 150 | } else if (Array.isArray(value)) { |
| 151 | value.forEach((row: string[], i: number) => { |
| 152 | row.forEach((cell, j) => { |
| 153 | out[`${key}[${i}][${j}]`] = cell; |
| 154 | }); |
| 155 | }); |
| 156 | } |
| 157 | } |
| 158 | return out; |
| 159 | } |
| 160 | |
| 161 | describe("website dictionaries", () => { |
| 162 | it("cover every routed locale except the English reference", () => { |
| 163 | expect([...DICTIONARY_LOCALES].sort()).toEqual( |
| 164 | [ |
| 165 | "zh", "es", "id", "ja", "ko", "pt-BR", "ru", "uk", "vi", |
| 166 | "fr", "de", "ca", "hi", "tr", "it", "pl", "ar", |
| 167 | ].sort(), |
| 168 | ); |
| 169 | // Chinese is dictionary-backed like every other locale — no inline |
| 170 | // en/zh special case survives in the page/component sources (#4934). |
| 171 | expect(DICTIONARY_LOCALES).toContain("zh"); |
| 172 | // Every routed locale either has its own dictionary or *is* English. |
| 173 | for (const locale of locales) { |
| 174 | expect( |
| 175 | locale === "en" || DICTIONARY_LOCALES.includes(locale), |
| 176 | `${locale} has no dictionary`, |
| 177 | ).toBe(true); |
| 178 | } |
| 179 | // Every partial locale is dictionary-backed, so the partial badge marks |
| 180 | // untranslated page bodies — never untranslated chrome. |
| 181 | for (const locale of partialLocales) { |
| 182 | expect(DICTIONARY_LOCALES, `${locale} partial pack`).toContain(locale); |
| 183 | } |
| 184 | }); |
| 185 | |
| 186 | it("holds every dictionary to exact key parity with the English reference", () => { |
| 187 | const enChromeKeys = Object.keys(EN_CHROME).sort(); |
| 188 | const enHomeKeys = Object.keys(EN_HOME).sort(); |
| 189 | for (const locale of DICTIONARY_LOCALES) { |
| 190 | expect(Object.keys(getChrome(locale)).sort(), `${locale} chrome keys`).toEqual( |
| 191 | enChromeKeys, |
| 192 | ); |
| 193 | expect(Object.keys(getHome(locale)).sort(), `${locale} home keys`).toEqual( |
| 194 | enHomeKeys, |
| 195 | ); |
| 196 | } |
| 197 | }); |
| 198 | |
| 199 | it("preserves {token} template placeholders through translation", () => { |
| 200 | const enChromeTokens = flattenStrings(EN_CHROME); |
| 201 | const enHomeTokens = flattenStrings(EN_HOME); |
| 202 | for (const locale of DICTIONARY_LOCALES) { |
| 203 | const chrome = flattenStrings(getChrome(locale)); |
| 204 | const home = flattenStrings(getHome(locale)); |
| 205 | for (const key of Object.keys(enChromeTokens)) { |
| 206 | expect(templateTokens(chrome[key]), `${locale} chrome ${key}`).toEqual( |
| 207 | templateTokens(enChromeTokens[key]), |
| 208 | ); |
| 209 | } |
| 210 | for (const key of Object.keys(enHomeTokens)) { |
| 211 | expect(templateTokens(home[key]), `${locale} home ${key}`).toEqual( |
| 212 | templateTokens(enHomeTokens[key]), |
| 213 | ); |
| 214 | } |
| 215 | } |
| 216 | }); |
| 217 | |
| 218 | it("holds every shipped page dictionary to key parity and English fallback (#5337)", () => { |
| 219 | const enKeys = Object.keys(EN_DOCS_GUIDE).sort(); |
| 220 | for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) { |
| 221 | // Page dictionaries are optional per locale: whatever getDocsGuide |
| 222 | // resolves — the locale's own file or the English fallback — must |
| 223 | // carry the exact reference shape, so a page never sees a missing key. |
| 224 | expect(Object.keys(getDocsGuide(locale)).sort(), `${locale} docs-guide keys`).toEqual( |
| 225 | enKeys, |
| 226 | ); |
| 227 | } |
| 228 | // zh ships a real translation, not an English pass-through. |
| 229 | expect(getDocsGuide("zh").overviewTitle).not.toBe(EN_DOCS_GUIDE.overviewTitle); |
| 230 | // The wave-2 locales ship docs-guide too, translated rather than passed through. |
| 231 | for (const locale of ["fr", "de", "ca", "hi", "tr", "it", "pl", "ar"]) { |
| 232 | expect(getDocsGuide(locale).overviewTitle, `${locale} docs-guide`).not.toBe( |
| 233 | EN_DOCS_GUIDE.overviewTitle, |
| 234 | ); |
| 235 | } |
| 236 | // A locale without the file falls back to the English reference object. |
| 237 | expect(getDocsGuide("ja")).toBe(EN_DOCS_GUIDE); |
| 238 | }); |
| 239 | |
| 240 | it("holds the docs shell dictionary to the same contract (#5337)", () => { |
| 241 | const enKeys = Object.keys(EN_DOCS_SHELL).sort(); |
| 242 | for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) { |
| 243 | expect(Object.keys(getDocsShell(locale)).sort(), `${locale} docs-shell keys`).toEqual( |
| 244 | enKeys, |
| 245 | ); |
| 246 | } |
| 247 | // zh ships a real translation, not an English pass-through. |
| 248 | expect(getDocsShell("zh").heroTitle).not.toBe(EN_DOCS_SHELL.heroTitle); |
| 249 | // Every other locale renders the English shell today, exactly as the |
| 250 | // `isZh` ternaries in docs/layout.tsx did before the move. |
| 251 | for (const locale of ["ja", "fr", "ar", "und"]) { |
| 252 | expect(getDocsShell(locale), `${locale} docs-shell`).toBe(EN_DOCS_SHELL); |
| 253 | } |
| 254 | }); |
| 255 | |
| 256 | it("holds the docs page-body dictionaries to the same contract (#5337)", () => { |
| 257 | for (const [label, get, reference] of [ |
| 258 | ["docs-hooks", getDocsHooks, EN_DOCS_HOOKS], |
| 259 | ["docs-troubleshooting", getDocsTroubleshooting, EN_DOCS_TROUBLESHOOTING], |
| 260 | ["docs-constitution", getDocsConstitution, EN_DOCS_CONSTITUTION], |
| 261 | ["docs-runtime-api", getDocsRuntimeApi, EN_DOCS_RUNTIME_API], |
| 262 | ["docs-sandbox", getDocsSandbox, EN_DOCS_SANDBOX], |
| 263 | ["docs-subagents", getDocsSubagents, EN_DOCS_SUBAGENTS], |
| 264 | ["docs-mcp", getDocsMcp, EN_DOCS_MCP], |
| 265 | ["docs-web", getDocsWeb, EN_DOCS_WEB], |
| 266 | ["docs-computers", getDocsComputers, EN_DOCS_COMPUTERS], |
| 267 | ["docs-auth", getDocsAuth, EN_DOCS_AUTH], |
| 268 | ["docs-trust", getDocsTrust, EN_DOCS_TRUST], |
| 269 | ["changelog", getChangelog, EN_CHANGELOG], |
| 270 | ] as const) { |
| 271 | const enKeys = Object.keys(reference).sort(); |
| 272 | for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) { |
| 273 | expect(Object.keys(get(locale)).sort(), `${locale} ${label} keys`).toEqual(enKeys); |
| 274 | } |
| 275 | // zh ships a real translation, not an English pass-through. The probe is |
| 276 | // `metaTitle` rather than `overviewTitle` because docs/mcp's heading is |
| 277 | // the code-owned literal `MCP` and stays in the page. |
| 278 | expect(get("zh").metaTitle, `zh ${label}`).not.toBe(reference.metaTitle); |
| 279 | // Every other locale renders English today, exactly as the `isZh` |
| 280 | // ternaries in the page did before the move. |
| 281 | for (const locale of ["ja", "fr", "ar", "und"]) { |
| 282 | expect(get(locale), `${locale} ${label}`).toBe(reference); |
| 283 | } |
| 284 | } |
| 285 | }); |
| 286 | |
| 287 | it("ships the Computer Use page dictionary for every routed locale", () => { |
| 288 | const enKeys = Object.keys(EN_COMPUTER_USE).sort(); |
| 289 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 290 | expect(Object.keys(getComputerUse(locale)).sort(), `${locale} computer-use keys`).toEqual(enKeys); |
| 291 | expect(getComputerUse(locale).steps, `${locale} computer-use steps`).toHaveLength(4); |
| 292 | } |
| 293 | // The download page is translated for every routed locale, not passed |
| 294 | // through: each dictionary locale resolves its own object with its own |
| 295 | // primary-button label; only an unknown locale gets the English reference. |
| 296 | for (const locale of DICTIONARY_LOCALES) { |
| 297 | expect(getComputerUse(locale), `${locale} computer-use`).not.toBe(EN_COMPUTER_USE); |
| 298 | expect(getComputerUse(locale).download, `${locale} computer-use download`).not.toBe(EN_COMPUTER_USE.download); |
| 299 | } |
| 300 | expect(getComputerUse("und")).toBe(EN_COMPUTER_USE); |
| 301 | }); |
| 302 | |
| 303 | it("keeps the docs page lists structurally aligned", () => { |
| 304 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 305 | expect(getDocsHooks(locale).events, `${locale} hook events`).toHaveLength(4); |
| 306 | expect( |
| 307 | getDocsTroubleshooting(locale).incidents, |
| 308 | `${locale} triage entries`, |
| 309 | ).toHaveLength(5); |
| 310 | expect( |
| 311 | getDocsConstitution(locale).principles.map(([key]) => key), |
| 312 | `${locale} constitution principles`, |
| 313 | ).toEqual(["userGlobal", "repoLocal", "runtime"]); |
| 314 | expect( |
| 315 | getDocsRuntimeApi(locale).entries.map(([key]) => key), |
| 316 | `${locale} runtime entries`, |
| 317 | ).toEqual(["http", "mobile", "stdio", "web", "doctor", "acp", "exec"]); |
| 318 | // The platform rows are keyed by their own translated name rather than a |
| 319 | // code-owned key, so only the count is comparable across locales. |
| 320 | expect(getDocsSandbox(locale).platforms, `${locale} sandbox platforms`).toHaveLength(4); |
| 321 | // Role names are identifiers the page owns, so the keys are comparable |
| 322 | // across locales rather than only the count. |
| 323 | expect( |
| 324 | getDocsSubagents(locale).roles.map(([key]) => key), |
| 325 | `${locale} subagent roles`, |
| 326 | ).toEqual([ |
| 327 | "worker", |
| 328 | "scout", |
| 329 | "planner", |
| 330 | "reviewer", |
| 331 | "builder", |
| 332 | "verifier", |
| 333 | "consultant", |
| 334 | "custom", |
| 335 | ]); |
| 336 | } |
| 337 | }); |
| 338 | |
| 339 | it("carries every code-span token through the hooks intro for splitTokens()", () => { |
| 340 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 341 | const parts = splitTokens(getDocsHooks(locale).configIntro); |
| 342 | const tokens = parts.flatMap((part) => ("token" in part ? [part.token] : [])); |
| 343 | expect(tokens, `${locale} configIntro tokens`).toEqual([ |
| 344 | "hooksTable", |
| 345 | "hooksCommand", |
| 346 | "enabledKey", |
| 347 | ]); |
| 348 | } |
| 349 | }); |
| 350 | |
| 351 | it("carries every code-span token through the constitution and runtime-api copy", () => { |
| 352 | const tokensOf = (template: string) => |
| 353 | splitTokens(template).flatMap((part) => ("token" in part ? [part.token] : [])); |
| 354 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 355 | const constitution = getDocsConstitution(locale); |
| 356 | expect(tokensOf(constitution.overviewLead), `${locale} overviewLead`).toEqual([ |
| 357 | "constitutionCommand", |
| 358 | "homeConfig", |
| 359 | "repoConfig", |
| 360 | ]); |
| 361 | // Exactly one link slot, so the translated label is never concatenated |
| 362 | // onto a fragment the call site owns. |
| 363 | expect(tokensOf(constitution.authorityNote), `${locale} authorityNote`).toEqual([ |
| 364 | "configDocs", |
| 365 | ]); |
| 366 | expect(tokensOf(getDocsRuntimeApi(locale).securityLead), `${locale} securityLead`).toEqual([ |
| 367 | "authToken", |
| 368 | "runtimeTokenEnv", |
| 369 | "legacyTokenEnv", |
| 370 | "insecureFlag", |
| 371 | "mobileFlag", |
| 372 | ]); |
| 373 | } |
| 374 | }); |
| 375 | |
| 376 | it("carries every code-span token through the subagents and mcp copy", () => { |
| 377 | const tokensOf = (template: string) => |
| 378 | splitTokens(template).flatMap((part) => ("token" in part ? [part.token] : [])); |
| 379 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 380 | const subagents = getDocsSubagents(locale); |
| 381 | expect(tokensOf(subagents.forkLead), `${locale} forkLead`).toEqual([ |
| 382 | "agentTool", |
| 383 | "forkContext", |
| 384 | ]); |
| 385 | expect(tokensOf(subagents.worktreeLead), `${locale} worktreeLead`).toEqual([ |
| 386 | "worktreeFlag", |
| 387 | "branchPattern", |
| 388 | "worktreeDir", |
| 389 | "writeAuthority", |
| 390 | "writeRoots", |
| 391 | "exactFiles", |
| 392 | "coordinationContracts", |
| 393 | ]); |
| 394 | const mcp = getDocsMcp(locale); |
| 395 | expect(tokensOf(mcp.overviewConfig), `${locale} mcp overviewConfig`).toEqual([ |
| 396 | "configPath", |
| 397 | "legacyConfigPath", |
| 398 | "configPathOption", |
| 399 | "configEnvVar", |
| 400 | "serversKey", |
| 401 | ]); |
| 402 | expect(tokensOf(mcp.setupLead), `${locale} mcp setupLead`).toEqual([ |
| 403 | "initCommand", |
| 404 | "mcpCommand", |
| 405 | ]); |
| 406 | expect(tokensOf(mcp.toolsLead), `${locale} mcp toolsLead`).toEqual([ |
| 407 | "toolNamePattern", |
| 408 | "gitServer", |
| 409 | "statusTool", |
| 410 | "gitStatusTool", |
| 411 | ]); |
| 412 | expect(tokensOf(mcp.serverLead), `${locale} mcp serverLead`).toEqual([ |
| 413 | "serveMcp", |
| 414 | "mcpServerCommand", |
| 415 | "addSelfCommand", |
| 416 | "serveHttp", |
| 417 | ]); |
| 418 | } |
| 419 | }); |
| 420 | |
| 421 | it("carries every code-span token through the sandbox and web copy", () => { |
| 422 | const tokensOf = (template: string) => |
| 423 | splitTokens(template).flatMap((part) => ("token" in part ? [part.token] : [])); |
| 424 | for (const locale of [...DICTIONARY_LOCALES, "und"]) { |
| 425 | // Two of these repeat, and the order is the sentence's, so this is a |
| 426 | // stricter check than check-locales.mjs, which compares token sets. |
| 427 | expect(tokensOf(getDocsSandbox(locale).policiesLead), `${locale} policiesLead`).toEqual([ |
| 428 | "sandboxMode", |
| 429 | "readOnly", |
| 430 | "workspaceWrite", |
| 431 | "dangerFullAccess", |
| 432 | "externalSandbox", |
| 433 | "dangerFullAccess", |
| 434 | "externalSandbox", |
| 435 | ]); |
| 436 | const web = getDocsWeb(locale); |
| 437 | expect(tokensOf(web.overviewLead), `${locale} web overviewLead`).toEqual([ |
| 438 | "webCommand", |
| 439 | "loopbackHost", |
| 440 | "defaultUrl", |
| 441 | "portExample", |
| 442 | ]); |
| 443 | expect(tokensOf(web.localLead), `${locale} localLead`).toEqual([ |
| 444 | "webCommand", |
| 445 | "portFlag", |
| 446 | "hostFlag", |
| 447 | "mobileCommand", |
| 448 | "httpFlag", |
| 449 | ]); |
| 450 | } |
| 451 | }); |
| 452 | |
| 453 | it("splitTokens interleaves literal text and token names in template order", () => { |
| 454 | expect(splitTokens("a {one} b {two}")).toEqual([ |
| 455 | { text: "a " }, |
| 456 | { token: "one" }, |
| 457 | { text: " b " }, |
| 458 | { token: "two" }, |
| 459 | ]); |
| 460 | // A template with no token is one literal run, and an empty run between |
| 461 | // adjacent tokens is dropped rather than rendered as an empty node. |
| 462 | expect(splitTokens("plain")).toEqual([{ text: "plain" }]); |
| 463 | expect(splitTokens("{one}{two}")).toEqual([{ token: "one" }, { token: "two" }]); |
| 464 | }); |
| 465 | |
| 466 | it("pickText selects the locale side of legacy { en, zh } pairs", () => { |
| 467 | const pair = { en: "English", zh: "中文" }; |
| 468 | expect(pickText(pair, "zh")).toBe("中文"); |
| 469 | expect(pickText(pair, "en")).toBe("English"); |
| 470 | expect(pickText(pair, "ja"), "non-zh locales read the English side").toBe("English"); |
| 471 | }); |
| 472 | |
| 473 | it("keeps the gain, models, availability, and surface lists structurally aligned", () => { |
| 474 | for (const locale of DICTIONARY_LOCALES) { |
| 475 | const home = getHome(locale); |
| 476 | expect(home.gain, `${locale} gain`).toHaveLength(3); |
| 477 | expect(home.modelsFacts, `${locale} modelsFacts`).toHaveLength(3); |
| 478 | expect(home.availability, `${locale} availability`).toHaveLength(4); |
| 479 | expect(home.surfaces, `${locale} surfaces`).toHaveLength(5); |
| 480 | for (const row of [...home.gain, ...home.modelsFacts, ...home.availability, ...home.surfaces]) { |
| 481 | for (const cell of row) { |
| 482 | expect(cell.length, `${locale} empty cell`).toBeGreaterThan(0); |
| 483 | } |
| 484 | } |
| 485 | } |
| 486 | }); |
| 487 | |
| 488 | it("falls back to the English dictionary for unrouted locales — no missing markers", () => { |
| 489 | for (const key of Object.keys(EN_CHROME) as (keyof ChromeDict)[]) { |
| 490 | expect(getChrome("xx")[key]).toBe(EN_CHROME[key]); |
| 491 | expect(getChrome("en")[key]).toBe(EN_CHROME[key]); |
| 492 | } |
| 493 | for (const key of Object.keys(EN_HOME) as (keyof HomeDict)[]) { |
| 494 | expect(getHome("xx")[key]).toEqual(EN_HOME[key]); |
| 495 | } |
| 496 | }); |
| 497 | |
| 498 | it("has no empty strings anywhere", () => { |
| 499 | for (const locale of ["en", ...DICTIONARY_LOCALES]) { |
| 500 | for (const [key, value] of Object.entries(flattenStrings(getChrome(locale)))) { |
| 501 | expect(value.trim().length, `${locale} chrome ${key}`).toBeGreaterThan(0); |
| 502 | } |
| 503 | for (const [key, value] of Object.entries(flattenStrings(getHome(locale)))) { |
| 504 | expect(value.trim().length, `${locale} home ${key}`).toBeGreaterThan(0); |
| 505 | } |
| 506 | } |
| 507 | }); |
| 508 | |
| 509 | it("keeps the Cyrillic packs script-pure (no cross-leakage, no mixed copy)", () => { |
| 510 | const cyrillic = /[Ѐ-ӿ]/; |
| 511 | for (const [key, value] of Object.entries(flattenStrings(getChrome("uk")))) { |
| 512 | expect(value, `uk chrome ${key}`).not.toMatch(/[ыэъЫЭЪ]/); |
| 513 | void cyrillic; |
| 514 | } |
| 515 | for (const [key, value] of Object.entries(flattenStrings(getHome("uk")))) { |
| 516 | expect(value, `uk home ${key}`).not.toMatch(/[ыэъЫЭЪ]/); |
| 517 | } |
| 518 | for (const [key, value] of Object.entries(flattenStrings(getChrome("ru")))) { |
| 519 | expect(value, `ru chrome ${key}`).not.toMatch(/[іІїЇєЄґҐ]/); |
| 520 | } |
| 521 | for (const [key, value] of Object.entries(flattenStrings(getHome("ru")))) { |
| 522 | expect(value, `ru home ${key}`).not.toMatch(/[іІїЇєЄґҐ]/); |
| 523 | } |
| 524 | // Prose values are actually translated, not English pass-through. |
| 525 | expect(getHome("ru").heroIntro).toMatch(cyrillic); |
| 526 | expect(getHome("uk").heroIntro).toMatch(cyrillic); |
| 527 | expect(getChrome("ru").navDocs).not.toBe(EN_CHROME.navDocs); |
| 528 | expect(getChrome("uk").navDocs).not.toBe(EN_CHROME.navDocs); |
| 529 | expect(getChrome("ru").navDocs).not.toBe(getChrome("uk").navDocs); |
| 530 | }); |
| 531 | |
| 532 | it("keeps the Chinese pack in Han script for prose (no English pass-through)", () => { |
| 533 | const han = /[一-鿿]/; |
| 534 | const chrome = getChrome("zh"); |
| 535 | const home = getHome("zh"); |
| 536 | for (const key of CHROME_PROSE_KEYS) { |
| 537 | expect(chrome[key], `zh chrome ${key}`).toMatch(han); |
| 538 | } |
| 539 | for (const key of HOME_PROSE_KEYS) { |
| 540 | expect(home[key], `zh home ${key}`).toMatch(han); |
| 541 | } |
| 542 | // Chinese resolves to its OWN dictionary, not the English reference. |
| 543 | expect(chrome.navDocs).not.toBe(EN_CHROME.navDocs); |
| 544 | expect(home.heroTitle).not.toBe(EN_HOME.heroTitle); |
| 545 | }); |
| 546 | |
| 547 | it("leaves no unmarked English prose in any non-English dictionary", () => { |
| 548 | for (const locale of DICTIONARY_LOCALES) { |
| 549 | const chrome = getChrome(locale); |
| 550 | const home = getHome(locale); |
| 551 | const loanwords = new Set(CHROME_LOANWORDS[locale] ?? []); |
| 552 | for (const key of CHROME_PROSE_KEYS) { |
| 553 | if (loanwords.has(key)) { |
| 554 | // A documented loanword: the shared value IS the native word. |
| 555 | expect(chrome[key], `${locale} chrome ${key} loanword`).toBe(EN_CHROME[key]); |
| 556 | continue; |
| 557 | } |
| 558 | expect(chrome[key], `${locale} chrome ${key} is English pass-through`).not.toBe( |
| 559 | EN_CHROME[key], |
| 560 | ); |
| 561 | } |
| 562 | for (const key of HOME_PROSE_KEYS) { |
| 563 | expect(home[key], `${locale} home ${key} is English pass-through`).not.toBe( |
| 564 | EN_HOME[key], |
| 565 | ); |
| 566 | } |
| 567 | } |
| 568 | }); |
| 569 | |
| 570 | it("keeps marks, tags, and proper nouns out of the translated-prose rule", () => { |
| 571 | // Documents the deliberate exceptions so a future audit does not read a |
| 572 | // shared value here as a missing translation. |
| 573 | for (const key of NON_PROSE_KEYS) { |
| 574 | const inChrome = key in EN_CHROME; |
| 575 | const inHome = key in EN_HOME; |
| 576 | expect(inChrome || inHome, `${key} is not a real dictionary key`).toBe(true); |
| 577 | expect(CHROME_PROSE_KEYS as readonly string[]).not.toContain(key); |
| 578 | expect(HOME_PROSE_KEYS as readonly string[]).not.toContain(key); |
| 579 | } |
| 580 | }); |
| 581 | |
| 582 | it("carries the {brand} token through every hero lede for splitToken()", () => { |
| 583 | for (const locale of ["en", ...DICTIONARY_LOCALES]) { |
| 584 | const lede = getHome(locale).heroIntro; |
| 585 | expect(lede, `${locale} heroIntro`).toContain("{brand}"); |
| 586 | const parts = splitToken(lede, "brand"); |
| 587 | expect(parts.length, `${locale} heroIntro brand split`).toBe(2); |
| 588 | expect(parts.join("").includes("{brand}")).toBe(false); |
| 589 | } |
| 590 | }); |
| 591 | |
| 592 | it("carries the {handle} token through every ticker by-line", () => { |
| 593 | // components/ticker.tsx splits on the token so the handle is typeset in |
| 594 | // its own element. A locale that drops it would print a by-line with no |
| 595 | // contributor in it — the opposite of the point. |
| 596 | for (const locale of ["en", ...DICTIONARY_LOCALES]) { |
| 597 | const byLine = getChrome(locale).tickerBy; |
| 598 | expect(byLine, `${locale} tickerBy`).toContain("{handle}"); |
| 599 | const parts = splitToken(byLine, "handle"); |
| 600 | expect(parts.length, `${locale} tickerBy split`).toBe(2); |
| 601 | } |
| 602 | }); |
| 603 | |
| 604 | it("interpolates templates with fill() and leaves unknown tokens visible", () => { |
| 605 | expect(fill("Latest release {tag}", { tag: "v0.9.2" })).toBe("Latest release v0.9.2"); |
| 606 | expect(fill("{count} provider routes", { count: 30 })).toBe("30 provider routes"); |
| 607 | expect(fill("v{version} {state}", { version: "0.9.2" })).toBe("v0.9.2 {state}"); |
| 608 | }); |
| 609 | }); |
| 610 |