返回 CodeWhale
dictionaries.test.ts
根目录 / web / lib / i18n / dictionaries.test.ts
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
610 lines TYPESCRIPT