forked from baron/baron-sso
feat: i18n 개선 및 userfront 로그인/로케일 보완
This commit is contained in:
237
tools/i18n-scanner/report.js
Normal file
237
tools/i18n-scanner/report.js
Normal file
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT_DIR = process.cwd();
|
||||
const LOCALES_DIR = path.join(ROOT_DIR, 'locales');
|
||||
const TEMPLATE_PATH = path.join(LOCALES_DIR, 'template.toml');
|
||||
const LANG_FILES = ['ko.toml', 'en.toml'];
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'.dart_tool',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'coverage',
|
||||
'.next',
|
||||
'.cache',
|
||||
'tmp',
|
||||
'logs',
|
||||
]);
|
||||
|
||||
const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.dart']);
|
||||
|
||||
const CODE_PATTERNS = [
|
||||
/\b(?:i18n\.)?t\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/\btr\s*\(\s*['"]([^'"]+)['"]/g,
|
||||
/['"]([^'"]+)['"]\s*\.tr\s*\(/g,
|
||||
];
|
||||
|
||||
function readFileRequired(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { ok: false, error: `파일이 없습니다: ${filePath}` };
|
||||
}
|
||||
return { ok: true, value: fs.readFileSync(filePath, 'utf8') };
|
||||
}
|
||||
|
||||
function parseTomlKeys(filePath) {
|
||||
const result = readFileRequired(filePath);
|
||||
if (!result.ok) {
|
||||
return { ok: false, error: result.error, keys: new Set() };
|
||||
}
|
||||
|
||||
const keys = new Set();
|
||||
const lines = result.value.split(/\r?\n/);
|
||||
let currentSection = [];
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[[') && line.endsWith(']]')) {
|
||||
const sectionName = line.slice(2, -2).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((p) => p.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('[') && line.endsWith(']')) {
|
||||
const sectionName = line.slice(1, -1).trim();
|
||||
currentSection = sectionName
|
||||
? sectionName.split('.').map((p) => p.trim()).filter(Boolean)
|
||||
: [];
|
||||
continue;
|
||||
}
|
||||
|
||||
const eqIndex = line.indexOf('=');
|
||||
if (eqIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, eqIndex).trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullKey = [...currentSection, key].join('.');
|
||||
keys.add(fullKey);
|
||||
}
|
||||
|
||||
return { ok: true, keys };
|
||||
}
|
||||
|
||||
function walkDir(dirPath, files) {
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (SKIP_DIRS.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
walkDir(path.join(dirPath, entry.name), files);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!CODE_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push(path.join(dirPath, entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
function collectCodeKeys() {
|
||||
const files = [];
|
||||
walkDir(ROOT_DIR, files);
|
||||
|
||||
const keys = new Set();
|
||||
for (const filePath of files) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
for (const pattern of CODE_PATTERNS) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
keys.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function difference(aSet, bSet) {
|
||||
const result = [];
|
||||
for (const item of aSet) {
|
||||
if (!bSet.has(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
const report = {
|
||||
generated_at: new Date().toISOString(),
|
||||
errors: [],
|
||||
warnings: [],
|
||||
details: {
|
||||
missing_in_template: [],
|
||||
missing_in_lang: {},
|
||||
unused_in_template: [],
|
||||
},
|
||||
};
|
||||
|
||||
const templateResult = parseTomlKeys(TEMPLATE_PATH);
|
||||
if (!templateResult.ok) {
|
||||
report.errors.push(templateResult.error);
|
||||
return report;
|
||||
}
|
||||
|
||||
const templateKeys = templateResult.keys;
|
||||
const codeKeys = collectCodeKeys();
|
||||
|
||||
const langKeyMap = new Map();
|
||||
for (const fileName of LANG_FILES) {
|
||||
const langPath = path.join(LOCALES_DIR, fileName);
|
||||
const langResult = parseTomlKeys(langPath);
|
||||
if (!langResult.ok) {
|
||||
report.errors.push(langResult.error);
|
||||
continue;
|
||||
}
|
||||
langKeyMap.set(fileName, langResult.keys);
|
||||
}
|
||||
|
||||
for (const [fileName, langKeys] of langKeyMap.entries()) {
|
||||
const missingInLang = difference(templateKeys, langKeys);
|
||||
if (missingInLang.length > 0) {
|
||||
report.errors.push(
|
||||
`[Sync Error] ${fileName} 누락 키 ${missingInLang.length}개`,
|
||||
);
|
||||
report.details.missing_in_lang[fileName] = missingInLang;
|
||||
}
|
||||
}
|
||||
|
||||
const missingInTemplate = difference(codeKeys, templateKeys);
|
||||
if (missingInTemplate.length > 0) {
|
||||
report.errors.push(
|
||||
`[Missing Key] template.toml 누락 키 ${missingInTemplate.length}개`,
|
||||
);
|
||||
report.details.missing_in_template = missingInTemplate;
|
||||
}
|
||||
|
||||
const unusedInTemplate = difference(templateKeys, codeKeys);
|
||||
if (unusedInTemplate.length > 0) {
|
||||
report.warnings.push(
|
||||
`[Unused Key] template.toml 미사용 키 ${unusedInTemplate.length}개`,
|
||||
);
|
||||
report.details.unused_in_template = unusedInTemplate;
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const report = buildReport();
|
||||
const outDir = path.join(ROOT_DIR, 'reports');
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
const outPath = path.join(outDir, 'i18n-report.json');
|
||||
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
||||
const summaryPath = path.join(outDir, 'i18n-report.txt');
|
||||
|
||||
const lines = [];
|
||||
lines.push(`generated_at: ${report.generated_at}`);
|
||||
if (report.errors.length > 0) {
|
||||
lines.push('errors:');
|
||||
report.errors.forEach((err) => lines.push(`- ${err}`));
|
||||
} else {
|
||||
lines.push('errors: none');
|
||||
}
|
||||
if (report.warnings.length > 0) {
|
||||
lines.push('warnings:');
|
||||
report.warnings.forEach((warn) => lines.push(`- ${warn}`));
|
||||
} else {
|
||||
lines.push('warnings: none');
|
||||
}
|
||||
fs.writeFileSync(summaryPath, lines.join('\n'));
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user