Files
OmniRoute/scripts/i18n/lib/docs-core-set.mjs
Markus Hartung fe28438dbc feat(i18n): add-locale orchestrator (config, flag, ui, docs, cli, readme, bars, site)
One command brings a new locale to every surface, in order: config/i18n.json
entry, flag SVG (lipis/flag-icons), dashboard catalog (scaffold + sync-ui-keys
--translate-markers), docs mirrors (run-translation over the core set every
existing locale carries, then the llm.txt / CHANGELOG.md stubs), CLI catalog
(generate-locales --code + batched common/program translation), README flag
link / docs index row / I18N guide row / locale counts (+ sync-llm-mirrors),
language bars, and the marketing site (lang JSON, SUPPORTED_LANGS, dropdowns).
Every phase checks presence first, so a re-run is a no-op apart from filling in
what is still missing; --dry-run prints the whole plan and touches nothing (no
files, network or child processes); --only / --skip select phases.

- scripts/i18n/lib/docs-core-set.mjs: computeDocsCoreSet — intersection of the
  existing non-docsExcluded mirrors minus llm.txt / CHANGELOG.md / I18N.md and
  paths without an English source (22 files today).
- scripts/i18n/lib/site-scaffold.mjs: addSupportedLang (re-flows the array in
  the file's own fill style) and addDropdownOption (code order, every menu).
- bin/cli/scripts/generate-locales.mjs: --code=<locale> filter.
- translate-backend parseBatchResponse: Object.hasOwn + trimmed values.
- package.json: i18n:add-locale script.
2026-09-02 09:10:41 -03:00

59 lines
2.5 KiB
JavaScript

/**
* The docs "core set": the source documents every existing locale mirror carries.
*
* The translation pipeline (`scripts/i18n/run-translation.mjs`) knows ~150
* source files, but only a core of them is translated in every locale under
* `docs/i18n/`. A new locale should reach parity with its peers rather than
* translate the full set, so the core is derived from the tree itself — the
* intersection of the relative paths present under every existing,
* non-`docsExcluded` locale directory — instead of being hard-coded.
*
* docsLocaleDirs({ root, config }) → ["ar", "az", …] locale codes whose
* docs/i18n/<code>/ directory exists
* computeDocsCoreSet({ root, config }) → sorted repo-relative source paths
*
* Paths that are not translation targets are dropped from the result: the
* strict-mirror stubs (`llm.txt`, `CHANGELOG.md`), the operator-only
* `docs/guides/I18N.md`, and any mirror whose English source no longer exists
* at `<root>/<path>`. Synchronous and filesystem-only — no network, no writes.
*/
import { existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
export const CORE_SET_EXCLUDED = ["llm.txt", "CHANGELOG.md", "docs/guides/I18N.md"];
function walkFiles(dir, base = dir, out = []) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) walkFiles(abs, base, out);
else if (entry.isFile()) out.push(path.relative(base, abs).split(path.sep).join("/"));
}
return out;
}
function isDirectory(abs) {
return existsSync(abs) && statSync(abs).isDirectory();
}
function isFile(abs) {
return existsSync(abs) && statSync(abs).isFile();
}
export function docsLocaleDirs({ root, config }) {
const excluded = new Set(config.docsExcluded ?? ["en"]);
return config.locales
.map((locale) => locale.code)
.filter((code) => !excluded.has(code) && isDirectory(path.join(root, "docs", "i18n", code)));
}
export function computeDocsCoreSet({ root, config }) {
let shared = null;
for (const code of docsLocaleDirs({ root, config })) {
const files = new Set(walkFiles(path.join(root, "docs", "i18n", code)));
shared = shared === null ? files : new Set([...shared].filter((rel) => files.has(rel)));
}
if (shared === null) return [];
const dropped = new Set(CORE_SET_EXCLUDED);
return [...shared].filter((rel) => !dropped.has(rel) && isFile(path.join(root, rel))).sort();
}