chore(i18n): add missing i18n keys
This commit is contained in:
@@ -5,6 +5,8 @@ import path from "path";
|
||||
import process from "process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import ts from "typescript";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -46,10 +48,44 @@ const SUPPORTED_EXTENSIONS = new Set([
|
||||
".mjs",
|
||||
".cjs",
|
||||
".vue",
|
||||
".rs",
|
||||
".json",
|
||||
]);
|
||||
|
||||
const TS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
||||
|
||||
const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+$/;
|
||||
const TEMPLATE_PREFIX_PATTERN =
|
||||
/^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)*\.$/;
|
||||
|
||||
const IGNORED_KEY_PREFIXES = new Set([
|
||||
"text",
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"warning",
|
||||
"success",
|
||||
"info",
|
||||
"background",
|
||||
"grey",
|
||||
"option",
|
||||
"action",
|
||||
"example",
|
||||
"chrome",
|
||||
"localhost",
|
||||
"www",
|
||||
"pac",
|
||||
"V2",
|
||||
"v2",
|
||||
"v1",
|
||||
]);
|
||||
|
||||
const NOTICE_METHOD_NAMES = new Set(["success", "error", "info", "warning"]);
|
||||
const NOTICE_SERVICE_IDENTIFIERS = new Set([
|
||||
"@/services/noticeService",
|
||||
"./noticeService",
|
||||
"../services/noticeService",
|
||||
]);
|
||||
|
||||
const WHITELIST_KEYS = new Set([
|
||||
"theme.light",
|
||||
"theme.dark",
|
||||
@@ -58,6 +94,8 @@ const WHITELIST_KEYS = new Set([
|
||||
]);
|
||||
|
||||
const MAX_PREVIEW_ENTRIES = 40;
|
||||
const dynamicKeyCache = new Map();
|
||||
const fileUsageCache = new Map();
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage: pnpm node scripts/cleanup-unused-i18n.mjs [options]
|
||||
@@ -177,22 +215,37 @@ function getAllFiles(start, predicate) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function loadSourceContents(sourceDirs) {
|
||||
const sourceFiles = sourceDirs.flatMap((dir) =>
|
||||
getAllFiles(
|
||||
dir,
|
||||
(filePath) =>
|
||||
SUPPORTED_EXTENSIONS.has(path.extname(filePath)) &&
|
||||
!EXCLUDE_USAGE_DIRS.some((excluded) =>
|
||||
filePath.startsWith(`${excluded}${path.sep}`),
|
||||
) &&
|
||||
!EXCLUDE_USAGE_DIRS.includes(filePath),
|
||||
),
|
||||
);
|
||||
function collectSourceFiles(sourceDirs) {
|
||||
const seen = new Set();
|
||||
const files = [];
|
||||
|
||||
return sourceFiles
|
||||
.map((filePath) => fs.readFileSync(filePath, "utf8"))
|
||||
.join("\n");
|
||||
for (const dir of sourceDirs) {
|
||||
const resolved = getAllFiles(dir, (filePath) => {
|
||||
if (seen.has(filePath)) return false;
|
||||
if (!SUPPORTED_EXTENSIONS.has(path.extname(filePath))) return false;
|
||||
if (
|
||||
EXCLUDE_USAGE_DIRS.some((excluded) =>
|
||||
filePath.startsWith(`${excluded}${path.sep}`),
|
||||
) ||
|
||||
EXCLUDE_USAGE_DIRS.includes(filePath)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const filePath of resolved) {
|
||||
seen.add(filePath);
|
||||
files.push({
|
||||
path: filePath,
|
||||
extension: path.extname(filePath).toLowerCase(),
|
||||
content: fs.readFileSync(filePath, "utf8"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
files.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return files;
|
||||
}
|
||||
|
||||
function flattenLocale(obj, parent = "") {
|
||||
@@ -239,6 +292,411 @@ function diffLocaleKeys(baselineEntries, localeEntries) {
|
||||
return { missing, extra };
|
||||
}
|
||||
|
||||
function determineScriptKind(extension) {
|
||||
switch (extension) {
|
||||
case ".ts":
|
||||
return ts.ScriptKind.TS;
|
||||
case ".tsx":
|
||||
return ts.ScriptKind.TSX;
|
||||
case ".jsx":
|
||||
return ts.ScriptKind.JSX;
|
||||
case ".js":
|
||||
case ".mjs":
|
||||
case ".cjs":
|
||||
return ts.ScriptKind.JS;
|
||||
default:
|
||||
return ts.ScriptKind.TS;
|
||||
}
|
||||
}
|
||||
|
||||
function getNamespaceFromKey(key) {
|
||||
if (!key || typeof key !== "string") return null;
|
||||
const [namespace] = key.split(".");
|
||||
return namespace ?? null;
|
||||
}
|
||||
|
||||
function addTemplatePrefixCandidate(
|
||||
prefix,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
) {
|
||||
if (!prefix || typeof prefix !== "string") return;
|
||||
const normalized = prefix.trim();
|
||||
if (!normalized) return;
|
||||
let candidate = normalized;
|
||||
if (!candidate.endsWith(".")) {
|
||||
const lastDotIndex = candidate.lastIndexOf(".");
|
||||
if (lastDotIndex === -1) {
|
||||
return;
|
||||
}
|
||||
candidate = candidate.slice(0, lastDotIndex + 1);
|
||||
}
|
||||
if (!TEMPLATE_PREFIX_PATTERN.test(candidate)) return;
|
||||
const namespace = getNamespaceFromKey(candidate);
|
||||
if (!namespace || IGNORED_KEY_PREFIXES.has(namespace)) return;
|
||||
if (!baselineNamespaces.has(namespace)) return;
|
||||
dynamicPrefixes.add(candidate);
|
||||
}
|
||||
|
||||
function addKeyIfValid(key, usedKeys, baselineNamespaces, options = {}) {
|
||||
if (!key || typeof key !== "string") return false;
|
||||
if (!KEY_PATTERN.test(key)) return false;
|
||||
const namespace = getNamespaceFromKey(key);
|
||||
if (!namespace || IGNORED_KEY_PREFIXES.has(namespace)) return false;
|
||||
if (!options.forceNamespace && !baselineNamespaces.has(namespace)) {
|
||||
return false;
|
||||
}
|
||||
usedKeys.add(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
function collectImportSpecifiers(sourceFile) {
|
||||
const specifiers = new Map();
|
||||
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (!ts.isImportDeclaration(node)) return;
|
||||
const moduleText =
|
||||
ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text;
|
||||
if (!moduleText || !node.importClause) return;
|
||||
|
||||
if (node.importClause.name) {
|
||||
specifiers.set(node.importClause.name.text, moduleText);
|
||||
}
|
||||
|
||||
const { namedBindings } = node.importClause;
|
||||
if (!namedBindings) return;
|
||||
|
||||
if (ts.isNamespaceImport(namedBindings)) {
|
||||
specifiers.set(namedBindings.name.text, `${moduleText}.*`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isNamedImports(namedBindings)) {
|
||||
for (const element of namedBindings.elements) {
|
||||
specifiers.set(element.name.text, moduleText);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function getCallExpressionChain(expression) {
|
||||
const chain = [];
|
||||
let current = expression;
|
||||
while (current) {
|
||||
if (ts.isIdentifier(current)) {
|
||||
chain.unshift(current.text);
|
||||
break;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(current)) {
|
||||
chain.unshift(current.name.text);
|
||||
current = current.expression;
|
||||
continue;
|
||||
}
|
||||
if (ts.isElementAccessExpression(current)) {
|
||||
const argument = current.argumentExpression;
|
||||
if (ts.isStringLiteralLike(argument)) {
|
||||
chain.unshift(argument.text);
|
||||
current = current.expression;
|
||||
continue;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function classifyCallExpression(expression, importSpecifiers) {
|
||||
if (!expression) return null;
|
||||
const chain = getCallExpressionChain(expression);
|
||||
if (chain.length === 0) return null;
|
||||
|
||||
const last = chain[chain.length - 1];
|
||||
const root = chain[0];
|
||||
|
||||
if (last === "t") {
|
||||
return { type: "translation", forceNamespace: true };
|
||||
}
|
||||
|
||||
if (
|
||||
NOTICE_METHOD_NAMES.has(last) &&
|
||||
root === "showNotice" &&
|
||||
(!importSpecifiers ||
|
||||
NOTICE_SERVICE_IDENTIFIERS.has(importSpecifiers.get("showNotice") ?? ""))
|
||||
) {
|
||||
return { type: "notice", forceNamespace: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveBindingValue(name, scopeStack) {
|
||||
if (!name) return null;
|
||||
for (let index = scopeStack.length - 1; index >= 0; index -= 1) {
|
||||
const scope = scopeStack[index];
|
||||
if (scope && scope.has(name)) {
|
||||
return scope.get(name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveKeyFromExpression(
|
||||
node,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
) {
|
||||
if (!node) return null;
|
||||
|
||||
if (
|
||||
ts.isStringLiteralLike(node) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(node)
|
||||
) {
|
||||
return node.text;
|
||||
}
|
||||
|
||||
if (ts.isTemplateExpression(node)) {
|
||||
addTemplatePrefixCandidate(
|
||||
node.head?.text ?? "",
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
);
|
||||
for (const span of node.templateSpans) {
|
||||
const literalText = span.literal?.text ?? "";
|
||||
const combined = (node.head?.text ?? "") + literalText;
|
||||
addTemplatePrefixCandidate(combined, dynamicPrefixes, baselineNamespaces);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
const left = resolveKeyFromExpression(
|
||||
node.left,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
const right = resolveKeyFromExpression(
|
||||
node.right,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
if (left && right) {
|
||||
return `${left}${right}`;
|
||||
}
|
||||
if (left) {
|
||||
addTemplatePrefixCandidate(left, dynamicPrefixes, baselineNamespaces);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ts.isParenthesizedExpression(node)) {
|
||||
return resolveKeyFromExpression(
|
||||
node.expression,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
||||
return resolveKeyFromExpression(
|
||||
node.expression,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(node)) {
|
||||
return resolveBindingValue(node.text, scopeStack);
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(node)) {
|
||||
const classification = classifyCallExpression(
|
||||
node.expression,
|
||||
importSpecifiers,
|
||||
);
|
||||
if (!classification) return null;
|
||||
const firstArg = node.arguments[0];
|
||||
if (!firstArg) return null;
|
||||
return resolveKeyFromExpression(
|
||||
firstArg,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectUsedKeysFromTsFile(
|
||||
file,
|
||||
baselineNamespaces,
|
||||
usedKeys,
|
||||
dynamicPrefixes,
|
||||
) {
|
||||
let sourceFile;
|
||||
|
||||
try {
|
||||
sourceFile = ts.createSourceFile(
|
||||
file.path,
|
||||
file.content,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
determineScriptKind(file.extension),
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(`Warning: failed to parse ${file.path}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const importSpecifiers = collectImportSpecifiers(sourceFile);
|
||||
const scopeStack = [new Map()];
|
||||
|
||||
const visit = (node) => {
|
||||
let scopePushed = false;
|
||||
if (
|
||||
ts.isBlock(node) ||
|
||||
ts.isModuleBlock(node) ||
|
||||
node.kind === ts.SyntaxKind.CaseBlock ||
|
||||
ts.isCatchClause(node)
|
||||
) {
|
||||
scopeStack.push(new Map());
|
||||
scopePushed = true;
|
||||
}
|
||||
|
||||
if (ts.isTemplateExpression(node)) {
|
||||
resolveKeyFromExpression(
|
||||
node,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
}
|
||||
|
||||
if (ts.isVariableDeclaration(node) && node.initializer) {
|
||||
const key = resolveKeyFromExpression(
|
||||
node.initializer,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
if (key && KEY_PATTERN.test(key)) {
|
||||
if (ts.isIdentifier(node.name)) {
|
||||
scopeStack[scopeStack.length - 1].set(node.name.text, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(node)) {
|
||||
const classification = classifyCallExpression(
|
||||
node.expression,
|
||||
importSpecifiers,
|
||||
);
|
||||
if (classification) {
|
||||
const [firstArg] = node.arguments;
|
||||
if (firstArg) {
|
||||
const key = resolveKeyFromExpression(
|
||||
firstArg,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
if (
|
||||
!addKeyIfValid(key, usedKeys, baselineNamespaces, classification)
|
||||
) {
|
||||
addTemplatePrefixCandidate(
|
||||
key ?? "",
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isJsxAttribute(node) && node.name?.text === "i18nKey") {
|
||||
const initializer = node.initializer;
|
||||
if (initializer && ts.isStringLiteralLike(initializer)) {
|
||||
addKeyIfValid(initializer.text, usedKeys, baselineNamespaces, {
|
||||
forceNamespace: false,
|
||||
});
|
||||
} else if (
|
||||
initializer &&
|
||||
ts.isJsxExpression(initializer) &&
|
||||
initializer.expression
|
||||
) {
|
||||
const key = resolveKeyFromExpression(
|
||||
initializer.expression,
|
||||
scopeStack,
|
||||
dynamicPrefixes,
|
||||
baselineNamespaces,
|
||||
importSpecifiers,
|
||||
);
|
||||
addKeyIfValid(key, usedKeys, baselineNamespaces, {
|
||||
forceNamespace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
node.forEachChild(visit);
|
||||
|
||||
if (scopePushed) {
|
||||
scopeStack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
function collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys) {
|
||||
const regex = /['"`]([A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_-]+)+)['"`]/g;
|
||||
let match;
|
||||
while ((match = regex.exec(file.content))) {
|
||||
addKeyIfValid(match[1], usedKeys, baselineNamespaces, {
|
||||
forceNamespace: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function collectUsedI18nKeys(sourceFiles, baselineNamespaces) {
|
||||
const usedKeys = new Set();
|
||||
const dynamicPrefixes = new Set();
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
if (TS_EXTENSIONS.has(file.extension)) {
|
||||
collectUsedKeysFromTsFile(
|
||||
file,
|
||||
baselineNamespaces,
|
||||
usedKeys,
|
||||
dynamicPrefixes,
|
||||
);
|
||||
} else {
|
||||
collectUsedKeysFromTextFile(file, baselineNamespaces, usedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
return { usedKeys, dynamicPrefixes };
|
||||
}
|
||||
|
||||
function alignToBaseline(baselineNode, localeNode, options) {
|
||||
const shouldCopyLocale =
|
||||
localeNode && typeof localeNode === "object" && !Array.isArray(localeNode);
|
||||
@@ -355,12 +813,54 @@ function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function isKeyUsed(content, key) {
|
||||
if (WHITELIST_KEYS.has(key)) return true;
|
||||
if (!key) return false;
|
||||
function findKeyInSources(key, sourceFiles) {
|
||||
if (fileUsageCache.has(key)) {
|
||||
return fileUsageCache.get(key);
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`(['"\`])${escapeRegExp(key)}\\1`);
|
||||
return pattern.test(content);
|
||||
let found = false;
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
if (pattern.test(file.content)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fileUsageCache.set(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
function isKeyUsed(key, usage, sourceFiles) {
|
||||
if (WHITELIST_KEYS.has(key)) return true;
|
||||
if (!key) return false;
|
||||
if (usage.usedKeys.has(key)) return true;
|
||||
|
||||
if (dynamicKeyCache.has(key)) {
|
||||
return dynamicKeyCache.get(key);
|
||||
}
|
||||
|
||||
const prefixes = getCandidatePrefixes(key);
|
||||
let used = prefixes.some((prefix) => usage.dynamicPrefixes.has(prefix));
|
||||
|
||||
if (!used) {
|
||||
used = findKeyInSources(key, sourceFiles);
|
||||
}
|
||||
|
||||
dynamicKeyCache.set(key, used);
|
||||
return used;
|
||||
}
|
||||
|
||||
function getCandidatePrefixes(key) {
|
||||
if (!key.includes(".")) return [];
|
||||
const parts = key.split(".");
|
||||
const prefixes = [];
|
||||
for (let index = 0; index < parts.length - 1; index += 1) {
|
||||
const prefix = `${parts.slice(0, index + 1).join(".")}.`;
|
||||
prefixes.push(prefix);
|
||||
}
|
||||
return prefixes;
|
||||
}
|
||||
|
||||
function writeReport(reportPath, data) {
|
||||
@@ -403,7 +903,9 @@ function processLocale(
|
||||
locale,
|
||||
baselineData,
|
||||
baselineEntries,
|
||||
allSourceContent,
|
||||
usage,
|
||||
sourceFiles,
|
||||
missingFromSource,
|
||||
options,
|
||||
) {
|
||||
const raw = fs.readFileSync(locale.path, "utf8");
|
||||
@@ -415,15 +917,21 @@ function processLocale(
|
||||
|
||||
const unused = [];
|
||||
for (const key of flattened.keys()) {
|
||||
if (!isKeyUsed(allSourceContent, key)) {
|
||||
if (!isKeyUsed(key, usage, sourceFiles)) {
|
||||
unused.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const sourceMissing =
|
||||
locale.name === options.baseline
|
||||
? missingFromSource.filter((key) => !flattened.has(key))
|
||||
: [];
|
||||
|
||||
if (
|
||||
unused.length === 0 &&
|
||||
missing.length === 0 &&
|
||||
extra.length === 0 &&
|
||||
sourceMissing.length === 0 &&
|
||||
!options.align
|
||||
) {
|
||||
console.log(`[${locale.name}] No issues detected 🎉`);
|
||||
@@ -432,6 +940,10 @@ function processLocale(
|
||||
console.log(
|
||||
` unused: ${unused.length}, missing vs baseline: ${missing.length}, extra: ${extra.length}`,
|
||||
);
|
||||
if (sourceMissing.length > 0) {
|
||||
console.log(` missing in source: ${sourceMissing.length}`);
|
||||
logPreviewEntries("missing-source", sourceMissing);
|
||||
}
|
||||
logPreviewEntries("unused", unused);
|
||||
logPreviewEntries("missing", missing);
|
||||
logPreviewEntries("extra", extra);
|
||||
@@ -479,6 +991,7 @@ function processLocale(
|
||||
removed,
|
||||
missingKeys: missing,
|
||||
extraKeys: extra,
|
||||
missingSourceKeys: sourceMissing,
|
||||
aligned: aligned && options.apply,
|
||||
};
|
||||
}
|
||||
@@ -505,7 +1018,7 @@ function main() {
|
||||
console.log(` - ${dir}`);
|
||||
}
|
||||
|
||||
const allSourceContent = loadSourceContents(sourceDirs);
|
||||
const sourceFiles = collectSourceFiles(sourceDirs);
|
||||
const locales = loadLocales();
|
||||
|
||||
if (locales.length === 0) {
|
||||
@@ -526,6 +1039,13 @@ function main() {
|
||||
|
||||
const baselineData = JSON.parse(fs.readFileSync(baselineLocale.path, "utf8"));
|
||||
const baselineEntries = flattenLocale(baselineData);
|
||||
const baselineNamespaces = new Set(Object.keys(baselineData));
|
||||
const usage = collectUsedI18nKeys(sourceFiles, baselineNamespaces);
|
||||
const baselineKeys = new Set(baselineEntries.keys());
|
||||
const missingFromSource = Array.from(usage.usedKeys).filter(
|
||||
(key) => !baselineKeys.has(key),
|
||||
);
|
||||
missingFromSource.sort();
|
||||
|
||||
locales.sort((a, b) => {
|
||||
if (a.name === baselineLocale.name) return -1;
|
||||
@@ -540,7 +1060,9 @@ function main() {
|
||||
locale,
|
||||
baselineData,
|
||||
baselineEntries,
|
||||
allSourceContent,
|
||||
usage,
|
||||
sourceFiles,
|
||||
missingFromSource,
|
||||
options,
|
||||
),
|
||||
);
|
||||
@@ -557,15 +1079,19 @@ function main() {
|
||||
(count, result) => count + result.extraKeys.length,
|
||||
0,
|
||||
);
|
||||
const totalSourceMissing = results.reduce(
|
||||
(count, result) => count + result.missingSourceKeys.length,
|
||||
0,
|
||||
);
|
||||
|
||||
console.log("\nSummary:");
|
||||
for (const result of results) {
|
||||
console.log(
|
||||
` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`,
|
||||
` • ${result.locale}: unused=${result.unusedKeys.length}, missing=${result.missingKeys.length}, extra=${result.extraKeys.length}, missingSource=${result.missingSourceKeys.length}, total=${result.totalKeys}, expected=${result.expectedKeys}`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}`,
|
||||
`\nTotals → unused: ${totalUnused}, missing: ${totalMissing}, extra: ${totalExtra}, missingSource: ${totalSourceMissing}`,
|
||||
);
|
||||
if (options.apply) {
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user