mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-02 18:42:51 +01:00
refactor(i18n): harden key checker per review
- Validate static t() keys even under a dynamic prefix (dynamic prefixes now only affect the unused calc, not missing) — Copilot - Strip JS/TS/JSX comments before scanning so keys in comments aren't counted as usage — CodeRabbit - Run i18n:check as part of \un run test\ so local mirrors CI — CodeRabbit - Fix stale 'all locale files' comment (fixer is source-only) — Copilot
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
"doctor": "expo-doctor",
|
"doctor": "expo-doctor",
|
||||||
"i18n:check": "bun scripts/check-i18n-keys.mjs",
|
"i18n:check": "bun scripts/check-i18n-keys.mjs",
|
||||||
"i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused",
|
"i18n:fix-unused": "bun scripts/check-i18n-keys.mjs --fix-unused",
|
||||||
"test": "bun run typecheck && bun run lint && bun run format && bun run doctor",
|
"test": "bun run typecheck && bun run lint && bun run format && bun run i18n:check && bun run doctor",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -132,22 +132,32 @@ const KEY_SHAPE = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)+$/; // dotted key, e.g. home.
|
|||||||
const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection
|
const usedStatic = new Set(); // keys passed to t(...) / i18nKey — used for MISSING detection
|
||||||
const dynamicPrefixes = new Set();
|
const dynamicPrefixes = new Set();
|
||||||
const fullyDynamic = []; // { file, line }
|
const fullyDynamic = []; // { file, line }
|
||||||
let codeBlob = ""; // all source text — searched for delimited key literals
|
let codeBlob = ""; // all (comment-stripped) source text — searched for delimited key literals
|
||||||
|
|
||||||
|
// Strip comments so keys mentioned in comments (e.g. `// t("old.key")`) are not counted as
|
||||||
|
// usage. Block comments and JSX {/* */} are blanked (preserving newlines for line numbers);
|
||||||
|
// line comments are only stripped when `//` follows start/whitespace/punctuation, which keeps
|
||||||
|
// `://` inside string URLs intact.
|
||||||
|
const stripComments = (src) =>
|
||||||
|
src
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
||||||
|
.replace(/(^|[\s;{}()[\],=>])\/\/[^\n]*/g, (_m, p) => p);
|
||||||
|
|
||||||
const files = config.srcDirs.flatMap((d) =>
|
const files = config.srcDirs.flatMap((d) =>
|
||||||
walk(join(ROOT, d === "." ? "" : d) || ROOT),
|
walk(join(ROOT, d === "." ? "" : d) || ROOT),
|
||||||
);
|
);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const text = readFileSync(file, "utf8");
|
const text = readFileSync(file, "utf8");
|
||||||
codeBlob += `\n${text}`;
|
const clean = stripComments(text);
|
||||||
for (const m of text.matchAll(STATIC_RE)) usedStatic.add(m[2]);
|
codeBlob += `\n${clean}`;
|
||||||
for (const m of text.matchAll(TPL_STATIC_RE)) usedStatic.add(m[1]);
|
for (const m of clean.matchAll(STATIC_RE)) usedStatic.add(m[2]);
|
||||||
for (const m of text.matchAll(I18NKEY_RE)) usedStatic.add(m[2]);
|
for (const m of clean.matchAll(TPL_STATIC_RE)) usedStatic.add(m[1]);
|
||||||
for (const m of text.matchAll(TPL_DYN_RE)) {
|
for (const m of clean.matchAll(I18NKEY_RE)) usedStatic.add(m[2]);
|
||||||
|
for (const m of clean.matchAll(TPL_DYN_RE)) {
|
||||||
const prefix = m[1];
|
const prefix = m[1];
|
||||||
if (prefix?.includes(".")) dynamicPrefixes.add(prefix);
|
if (prefix?.includes(".")) dynamicPrefixes.add(prefix);
|
||||||
else {
|
else {
|
||||||
const idx = text.slice(0, m.index).split("\n").length;
|
const idx = clean.slice(0, m.index).split("\n").length;
|
||||||
fullyDynamic.push({ file: relative(ROOT, file), line: idx });
|
fullyDynamic.push({ file: relative(ROOT, file), line: idx });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,16 +179,13 @@ const isUsed = (key) =>
|
|||||||
|
|
||||||
// ---- compute ----
|
// ---- compute ----
|
||||||
const unused = sourceKeys.filter((k) => !isUsed(k)).sort();
|
const unused = sourceKeys.filter((k) => !isUsed(k)).sort();
|
||||||
|
// Static references are always validated, even under a dynamic prefix: a dynamic prefix only
|
||||||
|
// affects the UNUSED calculation, never MISSING.
|
||||||
const missing = [...usedStatic]
|
const missing = [...usedStatic]
|
||||||
.filter(
|
.filter((k) => KEY_SHAPE.test(k) && !sourceKeySet.has(k))
|
||||||
(k) =>
|
|
||||||
KEY_SHAPE.test(k) &&
|
|
||||||
!sourceKeySet.has(k) &&
|
|
||||||
!prefixList.some((p) => k.startsWith(p)),
|
|
||||||
)
|
|
||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
// ---- optional fix: strip dead keys from all locale files ----
|
// ---- optional fix: strip dead keys from the source locale (en.json) ----
|
||||||
const removeKey = (obj, parts) => {
|
const removeKey = (obj, parts) => {
|
||||||
const [head, ...rest] = parts;
|
const [head, ...rest] = parts;
|
||||||
if (!(head in obj)) return;
|
if (!(head in obj)) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user