diff --git a/tests/unit/i18n-locale-scaffold.test.ts b/tests/unit/i18n-locale-scaffold.test.ts
index 4769919f34..81d3a38a8c 100644
--- a/tests/unit/i18n-locale-scaffold.test.ts
+++ b/tests/unit/i18n-locale-scaffold.test.ts
@@ -55,6 +55,58 @@ const realConfig = (): I18nConfig => JSON.parse(readRepo("config/i18n.json")) as
const pipeColumns = (line: string): number[] =>
[...line].flatMap((char, index) => (char === "|" ? [index] : []));
+// A locale code that can never be configured, so the real-file tests keep passing after any
+// real locale (Greek included) lands. Fails loudly if every candidate is taken.
+const SENTINEL_CANDIDATES = ["zz", "zy", "zx"];
+const sentinelCode = (config: I18nConfig): string => {
+ const configured = new Set(config.locales.map((l) => l.code));
+ const code = SENTINEL_CANDIDATES.find((candidate) => !configured.has(candidate));
+ assert.ok(
+ code,
+ `every sentinel code (${SENTINEL_CANDIDATES.join(", ")}) is configured — extend SENTINEL_CANDIDATES`
+ );
+ return code;
+};
+
+const readmeLinkFor = (entry: LocaleEntry, flagFile: string): string =>
+ `
`;
+const indexRowFor = (entry: LocaleEntry): string =>
+ `- ${entry.flag} **${entry.native}** (\`${entry.code}\`): [Docs Root](./${entry.code}/README.md)`;
+
+type RowRef = { index: number; code: string };
+
+// Where a new row must land: right before the first existing row whose code sorts after it
+// under localeCompare("en"), or right after the last row when no row sorts after it.
+const expectedRowIndex = (rows: RowRef[], code: string): number => {
+ assert.ok(rows.length > 0, "no locale rows found");
+ const next = rows.find((row) => row.code.localeCompare(code, "en") > 0);
+ return next ? next.index : rows[rows.length - 1].index + 1;
+};
+
+const indexRows = (lines: string[]): RowRef[] =>
+ lines.flatMap((line, index) => {
+ const code = line.match(/^- .+ \(`([^`]+)`\): \[Docs Root\]/)?.[1];
+ return code ? [{ index, code }] : [];
+ });
+
+// The locale table of docs/guides/I18N.md: its header line and the contiguous rows under it
+// (the guide has other tables with backticked first cells, which must not count).
+const guideRows = (lines: string[]): { header: number; rows: RowRef[] } => {
+ const header = lines.findIndex((line) =>
+ /^\| Code +\| Language +\| RTL +\| Google Translate Code +\|$/.test(line)
+ );
+ assert.ok(header >= 0, "locale table header not found");
+ const rows: RowRef[] = [];
+ for (let index = header + 2; index < lines.length; index += 1) {
+ const code = lines[index].match(/^\| `([^`]+)` *\|/)?.[1];
+ if (!code) break;
+ rows.push({ index, code });
+ }
+ return { header, rows };
+};
+
+const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+
// ---------------------------------------------------------------------------
// insertLocaleEntry
// ---------------------------------------------------------------------------
@@ -102,7 +154,8 @@ test("insertLocaleEntry inserts at both ends, strips flagFile and stores the ent
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" });
+ const entry: LocaleEntry = { ...el, code: sentinelCode(before) };
+ const text = insertLocaleEntry(raw, { ...entry, flagFile: "gr.svg" });
assert.ok(text.endsWith("}\n"));
const after = JSON.parse(text) as I18nConfig;
@@ -111,27 +164,29 @@ test("insertLocaleEntry on the real config keeps every top-level key, its order
const { locales: afterLocales, ...afterRest } = after;
assert.deepEqual(afterRest, beforeRest);
- // The 43 existing entries are untouched and keep their relative order.
+ // The existing entries are untouched and keep their relative order.
assert.equal(afterLocales.length, beforeLocales.length + 1);
assert.deepEqual(
- afterLocales.filter((l) => l.code !== "el"),
+ afterLocales.filter((l) => l.code !== entry.code),
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) =>
+ // ...and the new entry sits exactly where a full localeCompare("en") sort would put it:
+ // before the first existing code that sorts after it, or last when none does.
+ const sorted = [...beforeLocales.map((l) => l.code), entry.code].sort((a, b) =>
a.localeCompare(b, "en")
);
assert.deepEqual(
afterLocales.map((l) => l.code),
sorted
);
+ const next = beforeLocales.findIndex((l) => l.code.localeCompare(entry.code, "en") > 0);
assert.equal(
- afterLocales.findIndex((l) => l.code === "el"),
- beforeLocales.findIndex((l) => l.code === "en")
+ afterLocales.findIndex((l) => l.code === entry.code),
+ next === -1 ? beforeLocales.length : next
);
assert.deepEqual(
- afterLocales.find((l) => l.code === "el"),
- el
+ afterLocales.find((l) => l.code === entry.code),
+ entry
);
});
@@ -220,9 +275,11 @@ test("insertReadmeFlagLink rejects a duplicate link and a README without the lan
});
test("insertReadmeFlagLink on the real README adds exactly one line right before the block's ", () => {
- const total = realConfig().locales.length;
+ const config = realConfig();
+ const total = config.locales.length;
+ const entry: LocaleEntry = { ...el, code: sentinelCode(config) };
const before = readRepo("README.md").split("\n");
- const after = insertReadmeFlagLink(before.join("\n"), el, total + 1).split("\n");
+ const after = insertReadmeFlagLink(before.join("\n"), entry, total + 1).split("\n");
assert.equal(after.length, before.length + 1);
const marker = before.indexOf(` 🌐 In ${total} languages`);
@@ -230,7 +287,7 @@ test("insertReadmeFlagLink on the real README adds exactly one line right before
assert.ok(marker > 0 && close > marker);
assert.equal(after[marker], ` 🌐 In ${total + 1} languages`);
assert.match(after[close - 1], /^
");
assert.deepEqual(
[...after.slice(0, marker), ...after.slice(marker + 1, close), ...after.slice(close + 1)],
@@ -290,26 +347,28 @@ test("insertDocsIndexRow rejects a duplicate row, a missing sentence and an inde
);
});
-test("insertDocsIndexRow on the real docs/i18n/README.md adds one row before `es` and rewrites only the sentence", () => {
- const total = realConfig().locales.length;
+test("insertDocsIndexRow on the real docs/i18n/README.md adds one row at the derived position and rewrites only the sentence", () => {
+ const config = realConfig();
+ const total = config.locales.length;
+ const entry: LocaleEntry = { ...el, code: sentinelCode(config) };
const before = readRepo("docs/i18n/README.md").split("\n");
- const after = insertDocsIndexRow(before.join("\n"), el, total + 1).split("\n");
+ const after = insertDocsIndexRow(before.join("\n"), entry, 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);
+ const at = expectedRowIndex(indexRows(before), entry.code);
+ assert.ok(sentence >= 0 && at > 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.equal(after[at], indexRowFor(entry));
assert.deepEqual(
- [...after.slice(0, sentence), ...after.slice(sentence + 1, es), ...after.slice(es + 1)],
+ [...after.slice(0, sentence), ...after.slice(sentence + 1, at), ...after.slice(at + 1)],
[...before.slice(0, sentence), ...before.slice(sentence + 1)]
);
});
@@ -431,25 +490,34 @@ test("insertI18nGuideRow only touches the locale table, not other tables with ba
);
});
-test("insertI18nGuideRow on the real docs/guides/I18N.md aligns the new row with its neighbours", () => {
+test("insertI18nGuideRow on the real docs/guides/I18N.md adds one aligned row at the derived position", () => {
const config = realConfig();
const total = config.locales.length;
+ const entry: LocaleEntry = { ...el, code: sentinelCode(config) };
const before = readRepo("docs/guides/I18N.md").split("\n");
- const after = insertI18nGuideRow(before.join("\n"), el, total + 1, config.rtl).split("\n");
+ const after = insertI18nGuideRow(before.join("\n"), entry, 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);
+ const { header, rows } = guideRows(before);
+ const at = expectedRowIndex(rows, entry.code);
+ assert.ok(headline >= 0 && header > headline && at > header + 1);
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]));
+ const rtl = config.rtl.includes(entry.code) ? "Yes" : "No";
+ assert.match(
+ after[at],
+ new RegExp(
+ `^\\| \`${entry.code}\` +\\| ${escapeRegExp(entry.native)} +\\| ${rtl} +\\| \`${entry.code}\` +\\|$`
+ )
+ );
+ // Aligned with the table: the same pipe columns as its (ASCII) header and separator lines.
+ assert.deepEqual(pipeColumns(after[at]), pipeColumns(before[header]));
+ assert.deepEqual(pipeColumns(after[at]), pipeColumns(before[header + 1]));
assert.deepEqual(
- [...after.slice(0, headline), ...after.slice(headline + 1, es), ...after.slice(es + 1)],
+ [...after.slice(0, headline), ...after.slice(headline + 1, at), ...after.slice(at + 1)],
[...before.slice(0, headline), ...before.slice(headline + 1)]
);
});