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
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`);
|
|
}
|
|
|