feat(i18n): pure scaffold helpers for adding a locale to config, README and indexes

This commit is contained in:
Markus Hartung
2026-09-02 08:27:39 -03:00
parent 694866a3a1
commit 45c385e7a2
2 changed files with 775 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
/**
* Pure text helpers for scaffolding a new locale across every surface that
* lists the supported languages. Text in, text out — no filesystem and no
* Prettier: the orchestrator (scripts/i18n/add-locale.mjs) reads each file,
* transforms it here and writes it back.
*
* `entry` is a `config/i18n.json` locale object (`code`, `label`, `name`,
* `native`, `english`, `flag`, optional `aliases`) plus one helper key that is
* never stored: `flagFile`, the `docs/assets/flags/<file>` to use when the
* ISO-3166 file cannot be derived from the flag emoji (e.g. `sw` → `tz.svg`).
* `total` is the number of UI locales after the insertion, English included.
*
* insertLocaleEntry(configText, entry) config/i18n.json
* flagFileFor(entry) 🇬🇷 → "gr.svg"
* insertReadmeFlagLink(readmeText, entry, total) README.md language block
* insertDocsIndexRow(indexText, entry, total) docs/i18n/README.md
* insertI18nGuideRow(guideText, entry, total, rtlCodes) docs/guides/I18N.md locale table
* bumpCounts(text, total) "N languages" / "N translated documentation sets"
* buildMirrorStub({ heading, native, bar, body }) docs/i18n/<code>/llm.txt, CHANGELOG.md
*
* Every insertion leaves the existing lines exactly as they are and places the
* new one in `code` order (`localeCompare`, "en"): right before the first
* existing entry that sorts after it, or after the last one. Duplicates throw.
*/
const REGIONAL_INDICATOR_A = 0x1f1e6;
const REGIONAL_INDICATOR_Z = REGIONAL_INDICATOR_A + 25;
const DEFAULT_RTL_CODES = ["ar", "fa", "he", "ur"];
const README_MARKER = /<b>🌐 In \d+ languages<\/b>/;
const INDEX_SENTENCE =
/into \d+ languages; together with the English source, the UI supports \d+ locales/;
const INDEX_ROW = /^- .+ \(`([^`]+)`\): \[Docs Root\]/;
const GUIDE_HEADLINE = /supports \*\*\d+ languages\*\*/;
const GUIDE_HEADER = /^\| Code +\| Language +\| RTL +\| Google Translate Code +\|$/;
const GUIDE_ROW = /^\| `([^`]+)` *\|/;
const TABLE_SEPARATOR = /^\|(?: *:?-{3,}:? *\|)+$/;
function compareCodes(a, b) {
return a.localeCompare(b, "en");
}
function requireCode(entry) {
if (!entry || typeof entry.code !== "string" || entry.code.length === 0) {
throw new Error("locale entry needs a non-empty code");
}
return entry.code;
}
function insertInCodeOrder(items, item, codeOf) {
const code = codeOf(item);
const at = items.findIndex((existing) => compareCodes(codeOf(existing), code) > 0);
return at === -1 ? [...items, item] : [...items.slice(0, at), item, ...items.slice(at)];
}
// Insert `row` among `rows` (`{ index, code }` of the existing locale rows in
// `lines`); every other line — headings, blank lines, table header, prose —
// stays exactly where it is.
function insertAmongRows(lines, rows, row, code, surface) {
if (rows.length === 0) throw new Error(`${surface}: no locale rows found`);
if (rows.some((existing) => existing.code === code)) {
throw new Error(`${surface}: ${code} is already listed`);
}
const next = rows.find((existing) => compareCodes(existing.code, code) > 0);
const at = next ? next.index : rows[rows.length - 1].index + 1;
return [...lines.slice(0, at), row, ...lines.slice(at)];
}
export function flagFileFor(entry) {
if (entry.flagFile) return entry.flagFile;
const codePoints =
typeof entry.flag === "string" ? [...entry.flag].map((char) => char.codePointAt(0)) : [];
const isRegionalPair =
codePoints.length === 2 &&
codePoints.every((cp) => cp >= REGIONAL_INDICATOR_A && cp <= REGIONAL_INDICATOR_Z);
if (!isRegionalPair) {
throw new Error(`cannot derive a flag file from ${entry.flag}; pass flagFile explicitly`);
}
return (
codePoints.map((cp) => String.fromCharCode(cp - REGIONAL_INDICATOR_A + 97)).join("") + ".svg"
);
}
export function insertLocaleEntry(configText, entry) {
const code = requireCode(entry);
const config = JSON.parse(configText);
if (!Array.isArray(config.locales)) throw new Error("config has no locales array");
if (config.locales.some((locale) => locale.code === code)) {
throw new Error(`${code} is already configured`);
}
const stored = { ...entry };
delete stored.flagFile;
config.locales = insertInCodeOrder(config.locales, stored, (locale) => locale.code);
return JSON.stringify(config, null, 2) + "\n";
}
export function insertReadmeFlagLink(readmeText, entry, total) {
const code = requireCode(entry);
const marker = readmeText.match(README_MARKER);
if (!marker || marker.index === undefined) throw new Error("README language block not found");
const close = readmeText.indexOf("</div>", marker.index);
if (close === -1) throw new Error("README language block is not closed");
const href = `docs/i18n/${code}/README.md`;
if (readmeText.slice(marker.index, close).includes(`href="${href}"`)) {
throw new Error(`${code} is already linked in the README language block`);
}
const label = `${entry.native} (${code})`;
const link = ` <a href="${href}"><img src="docs/assets/flags/${flagFileFor(entry)}" width="30" alt="${label}" title="${label}"></a>`;
// `</div>` sits on its own line in README.md; the link goes on the line before it.
const lineStart = readmeText.lastIndexOf("\n", close) + 1;
return (
readmeText.slice(0, marker.index) +
`<b>🌐 In ${total} languages</b>` +
readmeText.slice(marker.index + marker[0].length, lineStart) +
`${link}\n` +
readmeText.slice(lineStart)
);
}
export function insertDocsIndexRow(indexText, entry, total) {
const code = requireCode(entry);
if (!INDEX_SENTENCE.test(indexText)) {
throw new Error("docs/i18n/README.md counts sentence not found");
}
const lines = indexText
.replace(
INDEX_SENTENCE,
`into ${total - 1} languages; together with the English source, the UI supports ${total} locales`
)
.split("\n");
const rows = lines
.map((line, index) => ({ index, code: line.match(INDEX_ROW)?.[1] ?? null }))
.filter((candidate) => candidate.code !== null);
const row = `- ${entry.flag} **${entry.native}** (\`${code}\`): [Docs Root](./${code}/README.md)`;
return insertAmongRows(lines, rows, row, code, "docs/i18n/README.md").join("\n");
}
export function insertI18nGuideRow(guideText, entry, total, rtlCodes = DEFAULT_RTL_CODES) {
const code = requireCode(entry);
if (!GUIDE_HEADLINE.test(guideText)) throw new Error("docs/guides/I18N.md headline not found");
const lines = guideText.replace(GUIDE_HEADLINE, `supports **${total} languages**`).split("\n");
// The guide has other tables with backticked first cells, so the locale rows
// are the contiguous block right under the "Supported Locales" table header.
const header = lines.findIndex((line) => GUIDE_HEADER.test(line));
if (header === -1 || !TABLE_SEPARATOR.test(lines[header + 1] ?? "")) {
throw new Error("docs/guides/I18N.md locale table not found");
}
const rows = [];
for (let index = header + 2; index < lines.length; index += 1) {
const rowCode = lines[index].match(GUIDE_ROW)?.[1] ?? null;
if (rowCode === null) break;
rows.push({ index, code: rowCode });
}
const cells = [
`\`${code}\``,
entry.native,
rtlCodes.includes(code) ? "Yes" : "No",
`\`${code}\``,
];
const row = formatTableRow(cells, tableColumnWidths(lines[header], lines[header + 1]));
return insertAmongRows(lines, rows, row, code, "docs/guides/I18N.md").join("\n");
}
export function bumpCounts(text, total) {
return text
.replace(/\d+ languages/g, `${total} languages`)
.replace(/\d+ translated documentation sets/g, `${total - 1} translated documentation sets`);
}
export function buildMirrorStub({ heading, native, bar, body }) {
return `# ${heading} (${native})\n\n${bar}\n\n---\n\n${body}`;
}
// --- markdown table alignment ------------------------------------------------
//
// docs/guides/I18N.md is Prettier-formatted, so every cell is padded to its
// column width. A new row is padded the same way when the table is aligned
// (header cell widths equal the separator dash counts, which is how Prettier
// emits it); otherwise the compact `| a | b |` form is used.
function tableCells(line) {
return line.replace(/^\| ?/, "").replace(/ ?\|$/, "").split(" | ");
}
function tableColumnWidths(headerLine, separatorLine) {
const headerWidths = tableCells(headerLine).map((cell) => cell.length);
const separatorWidths = tableCells(separatorLine).map((cell) => cell.length);
const aligned =
headerWidths.length === separatorWidths.length &&
headerWidths.every((width, index) => width === separatorWidths[index]);
return aligned ? separatorWidths : null;
}
function formatTableRow(cells, widths) {
const padded =
widths && widths.length === cells.length
? cells.map(
(cell, index) => cell + " ".repeat(Math.max(0, widths[index] - displayWidth(cell)))
)
: cells;
return `| ${padded.join(" | ")} |`;
}
// Column width the way Prettier measures markdown tables: East Asian wide and
// fullwidth code points take two columns, combining diacritics none, everything
// else one. (Emoji never appear in these cells, so they are not special-cased.)
function displayWidth(text) {
let width = 0;
for (const char of text) {
const cp = char.codePointAt(0);
if (cp >= 0x0300 && cp <= 0x036f) continue;
width += isWide(cp) ? 2 : 1;
}
return width;
}
function isWide(cp) {
return (
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals, ideographic punctuation
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana, Katakana, Bopomofo, Hangul compat, CJK compat
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Extension A
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified Ideographs
(cp >= 0xa960 && cp <= 0xa97f) || // Hangul Jamo Extended-A
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
(cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK Compatibility Forms
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
(cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Extensions B and later
);
}

View File

@@ -0,0 +1,543 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
insertLocaleEntry,
flagFileFor,
insertReadmeFlagLink,
insertDocsIndexRow,
insertI18nGuideRow,
bumpCounts,
buildMirrorStub,
} from "../../scripts/i18n/lib/locale-scaffold.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const readRepo = (rel: string): string => readFileSync(path.join(ROOT, rel), "utf8");
type LocaleEntry = {
code: string;
label?: string;
name?: string;
native: string;
english?: string;
flag: string;
flagFile?: string;
aliases?: string[];
};
type I18nConfig = {
$schema?: string;
default: string;
rtl: string[];
uiOnly?: string[];
docsExcluded?: string[];
locales: LocaleEntry[];
};
const el: LocaleEntry = {
code: "el",
label: "EL",
name: "Ελληνικά",
native: "Ελληνικά",
english: "Greek",
flag: "🇬🇷",
};
const EL_README_LINK =
' <a href="docs/i18n/el/README.md"><img src="docs/assets/flags/gr.svg" width="30" alt="Ελληνικά (el)" title="Ελληνικά (el)"></a>';
const EL_INDEX_ROW = "- 🇬🇷 **Ελληνικά** (`el`): [Docs Root](./el/README.md)";
const realConfig = (): I18nConfig => JSON.parse(readRepo("config/i18n.json")) as I18nConfig;
// Column positions of every `|` — two table rows are aligned when these match.
const pipeColumns = (line: string): number[] =>
[...line].flatMap((char, index) => (char === "|" ? [index] : []));
// ---------------------------------------------------------------------------
// insertLocaleEntry
// ---------------------------------------------------------------------------
test("insertLocaleEntry keeps alphabetical order by code and rejects duplicates", () => {
const cfg = JSON.stringify({
default: "en",
rtl: [],
locales: [{ code: "de" }, { code: "en" }, { code: "es" }],
});
const out = JSON.parse(insertLocaleEntry(cfg, el)) as I18nConfig;
assert.deepEqual(
out.locales.map((l) => l.code),
["de", "el", "en", "es"]
);
assert.throws(() => insertLocaleEntry(JSON.stringify(out), el), /already configured/);
});
test("insertLocaleEntry inserts at both ends, strips flagFile and stores the entry's own keys", () => {
const cfg = JSON.stringify({ default: "en", rtl: [], locales: [{ code: "de" }, { code: "es" }] });
const first = JSON.parse(insertLocaleEntry(cfg, { ...el, code: "aa" })) as I18nConfig;
assert.deepEqual(
first.locales.map((l) => l.code),
["aa", "de", "es"]
);
const last = JSON.parse(
insertLocaleEntry(cfg, { ...el, code: "zz", flagFile: "gr.svg", aliases: ["zz-x"] })
) as I18nConfig;
assert.deepEqual(
last.locales.map((l) => l.code),
["de", "es", "zz"]
);
assert.deepEqual(last.locales[2], {
code: "zz",
label: "EL",
name: "Ελληνικά",
native: "Ελληνικά",
english: "Greek",
flag: "🇬🇷",
aliases: ["zz-x"],
});
assert.throws(() => insertLocaleEntry(cfg, { ...el, code: "" }), /code/);
});
test("insertLocaleEntry on the real config keeps every top-level key, its order and the existing code order", () => {
const raw = readRepo("config/i18n.json");
const before = JSON.parse(raw) as I18nConfig;
const text = insertLocaleEntry(raw, { ...el, flagFile: "gr.svg" });
assert.ok(text.endsWith("}\n"));
const after = JSON.parse(text) as I18nConfig;
assert.deepEqual(Object.keys(after), Object.keys(before));
const { locales: beforeLocales, ...beforeRest } = before;
const { locales: afterLocales, ...afterRest } = after;
assert.deepEqual(afterRest, beforeRest);
// The 43 existing entries are untouched and keep their relative order.
assert.equal(afterLocales.length, beforeLocales.length + 1);
assert.deepEqual(
afterLocales.filter((l) => l.code !== "el"),
beforeLocales
);
// ...and the new entry sits exactly where a full localeCompare("en") sort would put it.
const sorted = [...beforeLocales.map((l) => l.code), "el"].sort((a, b) =>
a.localeCompare(b, "en")
);
assert.deepEqual(
afterLocales.map((l) => l.code),
sorted
);
assert.equal(
afterLocales.findIndex((l) => l.code === "el"),
beforeLocales.findIndex((l) => l.code === "en")
);
assert.deepEqual(
afterLocales.find((l) => l.code === "el"),
el
);
});
// ---------------------------------------------------------------------------
// flagFileFor
// ---------------------------------------------------------------------------
test("flagFileFor derives the ISO-3166 file from the emoji and honours an override", () => {
assert.equal(flagFileFor(el), "gr.svg");
assert.equal(flagFileFor({ ...el, code: "kn", flag: "🇮🇳", flagFile: "in.svg" }), "in.svg");
// An override that differs from the derivation proves the override really wins.
assert.equal(flagFileFor({ ...el, code: "sw", flag: "🇰🇪", flagFile: "tz.svg" }), "tz.svg");
assert.equal(flagFileFor({ ...el, code: "ja", flag: "🇯🇵" }), "jp.svg");
});
test("flagFileFor rejects anything that is not a regional-indicator pair unless flagFile is given", () => {
assert.throws(() => flagFileFor({ ...el, flag: "🏳️" }), /pass flagFile explicitly/);
assert.throws(() => flagFileFor({ ...el, flag: "🇬" }), /pass flagFile explicitly/);
assert.throws(() => flagFileFor({ ...el, flag: "GR" }), /pass flagFile explicitly/);
const noFlag: Partial<LocaleEntry> = { code: "x", native: "x" };
assert.throws(() => flagFileFor(noFlag as LocaleEntry), /pass flagFile explicitly/);
assert.equal(flagFileFor({ ...el, flag: "🏳️", flagFile: "custom.svg" }), "custom.svg");
});
test("flagFileFor reproduces the README flag file of every configured locale (legacy `in`/`sw` need flagFile)", () => {
const readme = readRepo("README.md");
// Pre-existing README drift, exactly what the flagFile override is for: the legacy Hindi
// code `in` keeps in.svg although its emoji is 🇮🇩, and Kiswahili uses tz.svg for 🇰🇪.
const overrides: Record<string, string> = { in: "in.svg", sw: "tz.svg" };
for (const entry of realConfig().locales) {
const href = entry.code === "en" ? "README.md" : `docs/i18n/${entry.code}/README.md`;
const pattern = new RegExp(
`<a href="${href.replace(/\./g, "\\.")}"><img src="docs/assets/flags/([a-z]+\\.svg)"`
);
const match = readme.match(pattern);
assert.ok(match, `README has no flag link for ${entry.code}`);
assert.equal(flagFileFor({ ...entry, flagFile: overrides[entry.code] }), match[1], entry.code);
}
});
// ---------------------------------------------------------------------------
// insertReadmeFlagLink
// ---------------------------------------------------------------------------
const README_BLOCK = `<div align="center">\n <b>🌐 In 43 languages</b>\n <br/><br/>\n <a href="README.md"><img src="docs/assets/flags/us.svg" width="30" alt="English (en)" title="English (en)"></a>\n</div>\n`;
test("insertReadmeFlagLink appends the link inside the block and bumps the headline", () => {
const out = insertReadmeFlagLink(README_BLOCK, el, 44);
assert.match(out, /In 44 languages/);
assert.match(
out,
/<a href="docs\/i18n\/el\/README\.md"><img src="docs\/assets\/flags\/gr\.svg" width="30" alt="Ελληνικά \(el\)" title="Ελληνικά \(el\)"><\/a>\n<\/div>/
);
});
test("insertReadmeFlagLink lands after the last link, before </div>, and only rewrites the marker", () => {
const link = (code: string, file: string): string =>
` <a href="docs/i18n/${code}/README.md"><img src="docs/assets/flags/${file}" width="30" alt="${code}" title="${code}"></a>`;
const lines = [
"# OmniRoute",
"",
"Docs In 43 languages, see below.",
"",
'<div align="center">',
" <b>🌐 In 43 languages</b>",
" <br/><br/>",
link("de", "de.svg"),
link("es", "es.svg"),
link("fr", "fr.svg"),
"</div>",
"",
"<div>In 43 languages</div>",
"",
];
const out = insertReadmeFlagLink(lines.join("\n"), el, 44).split("\n");
const expected = [...lines];
expected[5] = " <b>🌐 In 44 languages</b>";
expected.splice(10, 0, EL_README_LINK);
assert.deepEqual(out, expected);
});
test("insertReadmeFlagLink rejects a duplicate link and a README without the language block", () => {
const once = insertReadmeFlagLink(README_BLOCK, el, 44);
assert.throws(() => insertReadmeFlagLink(once, el, 45), /already linked/);
assert.throws(() => insertReadmeFlagLink("# no block\n", el, 44), /language block not found/);
});
test("insertReadmeFlagLink on the real README adds exactly one line right before the block's </div>", () => {
const total = realConfig().locales.length;
const before = readRepo("README.md").split("\n");
const after = insertReadmeFlagLink(before.join("\n"), el, total + 1).split("\n");
assert.equal(after.length, before.length + 1);
const marker = before.indexOf(` <b>🌐 In ${total} languages</b>`);
const close = before.findIndex((line, index) => index > marker && line === "</div>");
assert.ok(marker > 0 && close > marker);
assert.equal(after[marker], ` <b>🌐 In ${total + 1} languages</b>`);
assert.match(after[close - 1], /^ <a href="docs\/i18n\/[^"]+\/README\.md"><img /);
assert.equal(after[close], EL_README_LINK);
assert.equal(after[close + 1], "</div>");
assert.deepEqual(
[...after.slice(0, marker), ...after.slice(marker + 1, close), ...after.slice(close + 1)],
[...before.slice(0, marker), ...before.slice(marker + 1)]
);
});
// ---------------------------------------------------------------------------
// insertDocsIndexRow
// ---------------------------------------------------------------------------
const INDEX_FIXTURE =
"Translations of documentation into 42 languages; together with the English source, the UI supports 43 locales. Code blocks remain in English.\n\n---\n\n- 🇩🇪 **Deutsch** (`de`): [Docs Root](./de/README.md)\n- 🇪🇸 **Español** (`es`): [Docs Root](./es/README.md)\n";
test("insertDocsIndexRow inserts in code order and updates the counts sentence", () => {
const out = insertDocsIndexRow(INDEX_FIXTURE, el, 44);
assert.match(
out,
/into 43 languages; together with the English source, the UI supports 44 locales/
);
assert.equal(out.split("\n").indexOf(EL_INDEX_ROW), 5);
});
test("insertDocsIndexRow inserts at the beginning and the end of the row block and leaves other lines alone", () => {
const withTrailer = `${INDEX_FIXTURE}\nSee also [the guide](../guides/I18N.md).\n`;
const lines = withTrailer.split("\n");
const sentence = lines[0]
.replace("into 42 languages", "into 43 languages")
.replace("supports 43 locales", "supports 44 locales");
const first = insertDocsIndexRow(
withTrailer,
{ ...el, code: "aa", native: "Aa", flag: "🇦🇦" },
44
);
const aaRow = "- 🇦🇦 **Aa** (`aa`): [Docs Root](./aa/README.md)";
assert.deepEqual(first.split("\n"), [sentence, ...lines.slice(1, 4), aaRow, ...lines.slice(4)]);
const last = insertDocsIndexRow(withTrailer, { ...el, code: "zz", native: "Zz", flag: "🇿🇿" }, 44);
const zzRow = "- 🇿🇿 **Zz** (`zz`): [Docs Root](./zz/README.md)";
assert.deepEqual(last.split("\n"), [sentence, ...lines.slice(1, 6), zzRow, ...lines.slice(6)]);
});
test("insertDocsIndexRow rejects a duplicate row, a missing sentence and an index without rows", () => {
assert.throws(
() => insertDocsIndexRow(INDEX_FIXTURE, { ...el, code: "de" }, 44),
/already listed/
);
assert.throws(
() =>
insertDocsIndexRow("# x\n\n- 🇩🇪 **Deutsch** (`de`): [Docs Root](./de/README.md)\n", el, 44),
/counts sentence/
);
assert.throws(
() => insertDocsIndexRow(`${INDEX_FIXTURE.split("\n").slice(0, 4).join("\n")}\n`, el, 44),
/no locale rows/
);
});
test("insertDocsIndexRow on the real docs/i18n/README.md adds one row before `es` and rewrites only the sentence", () => {
const total = realConfig().locales.length;
const before = readRepo("docs/i18n/README.md").split("\n");
const after = insertDocsIndexRow(before.join("\n"), el, total + 1).split("\n");
assert.equal(after.length, before.length + 1);
const sentence = before.findIndex((line) =>
line.startsWith("Translations of documentation into")
);
const es = before.findIndex((line) => line.includes("(`es`): [Docs Root]"));
assert.ok(sentence >= 0 && es > sentence);
assert.equal(
after[sentence],
before[sentence]
.replace(`into ${total - 1} languages`, `into ${total} languages`)
.replace(`supports ${total} locales`, `supports ${total + 1} locales`)
);
assert.equal(after[es], EL_INDEX_ROW);
assert.deepEqual(
[...after.slice(0, sentence), ...after.slice(sentence + 1, es), ...after.slice(es + 1)],
[...before.slice(0, sentence), ...before.slice(sentence + 1)]
);
});
// ---------------------------------------------------------------------------
// insertI18nGuideRow
// ---------------------------------------------------------------------------
const GUIDE_FIXTURE =
"OmniRoute supports **43 languages** with full dashboard UI translation.\n\n| Code | Language | RTL | Google Translate Code |\n| --- | --- | --- | --- |\n| `de` | Deutsch | No | `de` |\n| `es` | Español | No | `es` |\n";
test("insertI18nGuideRow adds a table row in code order and bumps the headline", () => {
const out = insertI18nGuideRow(GUIDE_FIXTURE, el, 44);
assert.match(out, /supports \*\*44 languages\*\*/);
// headline (0), blank (1), header (2), separator (3), `de` (4), `el` (5), `es` (6)
assert.equal(out.split("\n")[4], "| `de` | Deutsch | No | `de` |");
assert.equal(out.split("\n")[5], "| `el` | Ελληνικά | No | `el` |");
assert.equal(out.split("\n")[6], "| `es` | Español | No | `es` |");
});
test("insertI18nGuideRow inserts at both ends, takes RTL from rtlCodes and leaves other lines alone", () => {
const withTrailer = `${GUIDE_FIXTURE}\nRTL locales flip the layout.\n`;
const lines = withTrailer.split("\n");
const headline = lines[0].replace("**43 languages**", "**44 languages**");
const first = insertI18nGuideRow(withTrailer, { ...el, code: "ar", native: "العربية" }, 44);
const arRow = "| `ar` | العربية | Yes | `ar` |";
assert.deepEqual(first.split("\n"), [headline, ...lines.slice(1, 4), arRow, ...lines.slice(4)]);
const last = insertI18nGuideRow(withTrailer, { ...el, code: "zz", native: "Zz" }, 44, ["zz"]);
const zzRow = "| `zz` | Zz | Yes | `zz` |";
assert.deepEqual(last.split("\n"), [headline, ...lines.slice(1, 6), zzRow, ...lines.slice(6)]);
const he = { ...el, code: "he", native: "עברית" };
assert.equal(
insertI18nGuideRow(GUIDE_FIXTURE, he, 44).split("\n")[6],
"| `he` | עברית | Yes | `he` |"
);
assert.equal(
insertI18nGuideRow(GUIDE_FIXTURE, he, 44, []).split("\n")[6],
"| `he` | עברית | No | `he` |"
);
});
test("insertI18nGuideRow rejects a duplicate row, a missing headline and a guide without rows", () => {
assert.throws(
() => insertI18nGuideRow(GUIDE_FIXTURE, { ...el, code: "es" }, 44),
/already listed/
);
assert.throws(() => insertI18nGuideRow("| `de` | Deutsch | No | `de` |\n", el, 44), /headline/);
assert.throws(
() => insertI18nGuideRow(`${GUIDE_FIXTURE.split("\n").slice(0, 4).join("\n")}\n`, el, 44),
/no locale rows/
);
});
test("insertI18nGuideRow pads the new row to the column widths of a Prettier-aligned table", () => {
const aligned = [
"OmniRoute supports **43 languages** with full dashboard UI translation.",
"",
"| Code | Language | RTL | Google Translate Code |",
"| ------- | -------------------- | --- | --------------------- |",
"| `de` | Deutsch | No | `de` |",
"| `zh-CN` | 中文 (简体) | No | `zh-CN` |",
"",
].join("\n");
const greek = insertI18nGuideRow(aligned, el, 44).split("\n");
assert.equal(greek[5], "| `el` | Ελληνικά | No | `el` |");
assert.deepEqual(pipeColumns(greek[5]), pipeColumns(greek[4]));
// CJK ideographs are two columns wide for Prettier, so 中文 (香港) gets the same 9 spaces
// of padding as the 中文 (简体) row above it — not the 13 a plain .length would give.
const hk = insertI18nGuideRow(aligned, { ...el, code: "zh-HK", native: "中文 (香港)" }, 44);
assert.equal(
hk.split("\n")[6],
"| `zh-HK` | 中文 (香港) | No | `zh-HK` |"
);
});
test("insertI18nGuideRow only touches the locale table, not other tables with backticked cells", () => {
const guide = [
"OmniRoute supports **43 languages** with full dashboard UI translation.",
"",
"| Variable | Default |",
"| --- | --- |",
"| `OMNIROUTE_LANG` | `en` |",
"| `zz` | `zz` |",
"",
"### Supported Locales",
"",
"| Code | Language | RTL | Google Translate Code |",
"| --- | --- | --- | --- |",
"| `de` | Deutsch | No | `de` |",
"| `es` | Español | No | `es` |",
"",
"| Other | Table |",
"| --- | --- |",
"| `aa` | after |",
"",
];
const out = insertI18nGuideRow(guide.join("\n"), el, 44).split("\n");
const expected = [...guide];
expected[0] = expected[0].replace("**43 languages**", "**44 languages**");
expected.splice(12, 0, "| `el` | Ελληνικά | No | `el` |");
assert.deepEqual(out, expected);
// A duplicate in a decoy table is not a duplicate locale row.
assert.doesNotThrow(() =>
insertI18nGuideRow(guide.join("\n"), { ...el, code: "zz", native: "Zz" }, 44)
);
assert.throws(
() =>
insertI18nGuideRow(
"OmniRoute supports **43 languages**\n\n| `de` | Deutsch | No | `de` |\n",
el,
44
),
/locale table not found/
);
});
test("insertI18nGuideRow on the real docs/guides/I18N.md aligns the new row with its neighbours", () => {
const config = realConfig();
const total = config.locales.length;
const before = readRepo("docs/guides/I18N.md").split("\n");
const after = insertI18nGuideRow(before.join("\n"), el, total + 1, config.rtl).split("\n");
assert.equal(after.length, before.length + 1);
const headline = before.findIndex((line) => line.includes(`supports **${total} languages**`));
const es = before.findIndex((line) => line.startsWith("| `es` "));
assert.ok(headline >= 0 && es > headline);
assert.equal(
after[headline],
before[headline].replace(`**${total} languages**`, `**${total + 1} languages**`)
);
assert.match(after[es], /^\| `el` +\| Ελληνικά +\| No +\| `el` +\|$/);
assert.deepEqual(pipeColumns(after[es]), pipeColumns(before[es]));
assert.deepEqual(pipeColumns(after[es]), pipeColumns(before[es - 1]));
assert.deepEqual(
[...after.slice(0, headline), ...after.slice(headline + 1, es), ...after.slice(es + 1)],
[...before.slice(0, headline), ...before.slice(headline + 1)]
);
});
// ---------------------------------------------------------------------------
// bumpCounts
// ---------------------------------------------------------------------------
test("bumpCounts rewrites language and doc-set counts", () => {
assert.equal(
bumpCounts("next-intl with 43 languages\n42 translated documentation sets", 44),
"next-intl with 44 languages\n43 translated documentation sets"
);
});
test("bumpCounts touches only those two phrases", () => {
const text =
"352 providers, 43 languages for UI, 42 translated documentation sets, 43 locales, 7 language packs, in 42 locales, 43 langs";
assert.equal(
bumpCounts(text, 44),
"352 providers, 44 languages for UI, 43 translated documentation sets, 43 locales, 7 language packs, in 42 locales, 43 langs"
);
});
test("bumpCounts on the real llm.txt rewrites exactly the three count lines, each by one", () => {
const total = realConfig().locales.length;
const before = readRepo("llm.txt").split("\n");
const after = bumpCounts(before.join("\n"), total + 1).split("\n");
assert.equal(after.length, before.length);
const changed = before
.map((line, index) => [line, after[index]])
.filter(([from, to]) => from !== to);
assert.equal(changed.length, 3);
for (const [from, to] of changed) {
assert.match(from, /\d+ (languages|translated documentation sets)/);
assert.equal(
to,
from.replace(/\d+(?= (?:languages|translated documentation sets))/, (n) =>
String(Number(n) + 1)
)
);
}
});
// ---------------------------------------------------------------------------
// buildMirrorStub
// ---------------------------------------------------------------------------
test("buildMirrorStub produces the header + separator layout check-docs-sync expects", () => {
assert.equal(
buildMirrorStub({
heading: "OmniRoute",
native: "Ελληνικά",
bar: "🌐 **Languages:** x",
body: "body\n",
}),
"# OmniRoute (Ελληνικά)\n\n🌐 **Languages:** x\n\n---\n\nbody\n"
);
});
test("buildMirrorStub is accepted by check-docs-sync's separator logic and is a fixed point of sync-llm-mirrors", () => {
const body = "> OmniRoute is a proxy.\n\n## Section\n\ntext\n";
const stub = buildMirrorStub({
heading: "OmniRoute",
native: "Ελληνικά",
bar: "🌐 **Languages:** x",
body,
});
// scripts/check/check-docs-sync.mjs::extractI18nMirrorBody
const separator = stub.match(/^---\s*$/m);
assert.ok(separator && separator.index !== undefined);
const extracted = stub.slice(separator.index + separator[0].length).replace(/^\r?\n+/, "");
assert.equal(extracted, body);
// scripts/i18n/sync-llm-mirrors.mjs keeps the header up to `---` and re-appends the root body
const header = stub.slice(0, separator.index + separator[0].length).replace(/\n+$/, "");
assert.equal(`${header}\n\n${body.trimStart()}`, stub);
});
test("buildMirrorStub reproduces a real mirror byte for byte", () => {
const mirror = readRepo("docs/i18n/pt-BR/llm.txt");
const parts = mirror.match(
/^# (\S+) \((.+)\)\n\n(🌐 \*\*Languages:\*\* [^\n]+)\n\n---\n\n([\s\S]*)$/
);
assert.ok(parts, "docs/i18n/pt-BR/llm.txt does not have the five-part mirror layout");
const [, heading, native, bar, body] = parts;
assert.equal(heading, "OmniRoute");
assert.equal(native, realConfig().locales.find((l) => l.code === "pt-BR")?.native);
assert.equal(buildMirrorStub({ heading, native, bar, body }), mirror);
});