You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

82 lines
3.1 KiB

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const frontendRoot = path.resolve(scriptDirectory, "..");
const i18nPath = path.join(frontendRoot, "web", "static", "js", "i18n.js");
const i18nSource = fs.readFileSync(i18nPath, "utf8");
const definitionsStart = i18nSource.indexOf("const exact =");
const definitionsEnd = i18nSource.indexOf("function readLanguage");
if (definitionsStart < 0 || definitionsEnd < 0) {
throw new Error("could not locate i18n definitions");
}
const definitions = i18nSource.slice(definitionsStart, definitionsEnd);
const dictionaries = new Function(`${definitions}\nreturn { exact, patterns, fragments };`)();
function translate(value) {
const source = String(value == null ? "" : value);
if (!/[가-힣]/.test(source)) { return source; }
const leading = source.match(/^\s*/)?.[0] || "";
const trailing = source.match(/\s*$/)?.[0] || "";
const body = source.slice(leading.length, source.length - trailing.length);
if (Object.prototype.hasOwnProperty.call(dictionaries.exact, body)) {
return leading + dictionaries.exact[body] + trailing;
}
for (const pattern of dictionaries.patterns) {
if (pattern[0].test(body)) {
return leading + body.replace(pattern[0], pattern[1]) + trailing;
}
}
let result = body;
dictionaries.fragments.forEach(function (entry) {
result = result.split(entry[0]).join(entry[1]);
});
return leading + result + trailing;
}
function filesBelow(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap(function (entry) {
const item = path.join(directory, entry.name);
return entry.isDirectory() ? filesBelow(item) : [item];
});
}
const sourcePaths = [
path.join(frontendRoot, "internal", "mock", "mock_data.go"),
path.join(frontendRoot, "web", "static", "js", "integration.js"),
...filesBelow(path.join(frontendRoot, "web", "templates"))
];
const candidates = new Set();
sourcePaths.forEach(function (sourcePath) {
const source = fs.readFileSync(sourcePath, "utf8");
for (const match of source.matchAll(/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/gs)) {
const value = match[0].slice(1, -1);
if (/[가-힣]/.test(value) && value.length < 220) { candidates.add(value); }
}
if (sourcePath.endsWith(".html")) {
const visibleSource = source
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
for (const match of visibleSource.matchAll(/>([^<]*[가-힣][^<]*)</gs)) {
const value = match[1].replace(/{{[^}]+}}/g, "SAMPLE").trim();
if (value) { candidates.add(value); }
}
}
});
const missing = [...candidates].filter(function (value) {
return /[가-힣]/.test(translate(value));
}).sort(function (left, right) {
return left.localeCompare(right, "ko");
});
if (missing.length) {
console.error(`i18n coverage missing ${missing.length} of ${candidates.size} Korean UI strings:`);
missing.forEach(function (value) { console.error(`- ${value}`); });
process.exitCode = 1;
} else {
console.log(`i18n coverage OK: ${candidates.size} Korean UI strings`);
}