forked from baron/baron-sso
common/locales 기반 i18n 스캐너와 문서 정리
This commit is contained in:
@@ -5,11 +5,27 @@ 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 FAIL_UNUSED = process.argv.includes('--fail-unused');
|
||||
|
||||
const LOCALE_SPECS = [
|
||||
{
|
||||
name: 'root',
|
||||
label: 'root locales',
|
||||
dir: path.join(ROOT_DIR, 'locales'),
|
||||
template: 'template.toml',
|
||||
langs: ['ko.toml', 'en.toml'],
|
||||
ownsKey: (key) => !key.startsWith('ui.common.') && !key.startsWith('msg.common.'),
|
||||
},
|
||||
{
|
||||
name: 'common',
|
||||
label: 'common locales',
|
||||
dir: path.join(ROOT_DIR, 'common', 'locales'),
|
||||
template: 'template.toml',
|
||||
langs: ['ko.toml', 'en.toml'],
|
||||
ownsKey: (key) => key.startsWith('ui.common.') || key.startsWith('msg.common.'),
|
||||
},
|
||||
];
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
'node_modules',
|
||||
@@ -78,7 +94,6 @@ function parseTomlKeys(filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip quotes if present
|
||||
if (key.startsWith('"') && key.endsWith('"')) {
|
||||
key = key.slice(1, -1);
|
||||
}
|
||||
@@ -138,6 +153,23 @@ function collectCodeKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
function shouldIgnoreCodeKey(key) {
|
||||
return (
|
||||
key.includes('.msg.') ||
|
||||
key.includes('.ui.') ||
|
||||
key.includes('.err.') ||
|
||||
key.includes('.test.') ||
|
||||
key.includes('.non.') ||
|
||||
key.startsWith('ui.admin.users.list.table.') ||
|
||||
key.startsWith('msg.admin.users.detail.') ||
|
||||
key.startsWith('msg.dev.clients.') ||
|
||||
key.startsWith('ui.admin.users.create.') ||
|
||||
key.startsWith('ui.admin.users.detail.') ||
|
||||
key.startsWith('ui.dev.clients.') ||
|
||||
key.startsWith('ui.dev.session.')
|
||||
);
|
||||
}
|
||||
|
||||
function difference(aSet, bSet) {
|
||||
const result = [];
|
||||
for (const item of aSet) {
|
||||
@@ -158,72 +190,75 @@ function printList(title, items) {
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const templateResult = parseTomlKeys(TEMPLATE_PATH);
|
||||
function collectSpecResources(spec) {
|
||||
const templatePath = path.join(spec.dir, spec.template);
|
||||
const templateResult = parseTomlKeys(templatePath);
|
||||
if (!templateResult.ok) {
|
||||
errors.push(templateResult.error);
|
||||
return { ok: false, error: templateResult.error };
|
||||
}
|
||||
|
||||
const langKeyMap = new Map();
|
||||
for (const fileName of LANG_FILES) {
|
||||
const langPath = path.join(LOCALES_DIR, fileName);
|
||||
for (const fileName of spec.langs) {
|
||||
const langPath = path.join(spec.dir, fileName);
|
||||
const langResult = parseTomlKeys(langPath);
|
||||
if (!langResult.ok) {
|
||||
errors.push(langResult.error);
|
||||
continue;
|
||||
return { ok: false, error: langResult.error };
|
||||
}
|
||||
langKeyMap.set(fileName, langResult.keys);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('i18n 검증 실패: 필수 리소스 파일을 찾지 못했습니다.');
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
templateKeys: templateResult.keys,
|
||||
langKeyMap,
|
||||
};
|
||||
}
|
||||
|
||||
const templateKeys = templateResult.keys;
|
||||
const rawCodeKeys = Array.from(collectCodeKeys());
|
||||
const codeKeysArray = rawCodeKeys.filter(k =>
|
||||
!k.includes('.msg.') &&
|
||||
!k.includes('.ui.') &&
|
||||
!k.includes('.err.') &&
|
||||
!k.includes('.test.') &&
|
||||
!k.includes('.non.') &&
|
||||
!k.startsWith("ui.admin.users.list.table.") &&
|
||||
!k.startsWith("msg.admin.users.detail.") &&
|
||||
!k.startsWith("msg.common.") &&
|
||||
!k.startsWith("msg.dev.clients.") &&
|
||||
!k.startsWith("ui.admin.users.create.") &&
|
||||
!k.startsWith("ui.admin.users.detail.") &&
|
||||
!k.startsWith("ui.common.") &&
|
||||
!k.startsWith("ui.dev.clients.") &&
|
||||
!k.startsWith("ui.dev.session.")
|
||||
function main() {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const rawCodeKeys = Array.from(collectCodeKeys()).filter(
|
||||
(key) => !shouldIgnoreCodeKey(key),
|
||||
);
|
||||
const codeKeys = new Set(codeKeysArray);
|
||||
const codeKeys = new Set(rawCodeKeys);
|
||||
|
||||
for (const [fileName, langKeys] of langKeyMap.entries()) {
|
||||
const missingInLang = difference(templateKeys, langKeys);
|
||||
if (missingInLang.length > 0) {
|
||||
errors.push(`[Sync Error] ${fileName} 누락 키 ${missingInLang.length}개`);
|
||||
printList(`${fileName}에 없는 키`, missingInLang);
|
||||
for (const spec of LOCALE_SPECS) {
|
||||
const resources = collectSpecResources(spec);
|
||||
if (!resources.ok) {
|
||||
errors.push(resources.error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const missingInTemplate = difference(codeKeys, templateKeys);
|
||||
if (missingInTemplate.length > 0) {
|
||||
errors.push(`[Missing Key] template.toml 누락 키 ${missingInTemplate.length}개`);
|
||||
printList('template.toml에 없는 코드 사용 키', missingInTemplate);
|
||||
}
|
||||
for (const [fileName, langKeys] of resources.langKeyMap.entries()) {
|
||||
const missingInLang = difference(resources.templateKeys, langKeys);
|
||||
if (missingInLang.length > 0) {
|
||||
errors.push(
|
||||
`[Sync Error] ${spec.label} ${fileName} 누락 키 ${missingInLang.length}개`,
|
||||
);
|
||||
printList(`${spec.label} ${fileName}에 없는 키`, missingInLang);
|
||||
}
|
||||
}
|
||||
|
||||
const unusedInTemplate = difference(templateKeys, codeKeys);
|
||||
if (unusedInTemplate.length > 0) {
|
||||
warnings.push(`[Unused Key] template.toml 미사용 키 ${unusedInTemplate.length}개`);
|
||||
printList('코드에서 사용되지 않는 키', unusedInTemplate);
|
||||
const ownedCodeKeys = new Set(
|
||||
rawCodeKeys.filter((key) => spec.ownsKey(key)),
|
||||
);
|
||||
|
||||
const missingInTemplate = difference(ownedCodeKeys, resources.templateKeys);
|
||||
if (missingInTemplate.length > 0) {
|
||||
errors.push(
|
||||
`[Missing Key] ${spec.label} template.toml 누락 키 ${missingInTemplate.length}개`,
|
||||
);
|
||||
printList(`${spec.label} template.toml에 없는 코드 사용 키`, missingInTemplate);
|
||||
}
|
||||
|
||||
const unusedInTemplate = difference(resources.templateKeys, codeKeys);
|
||||
if (unusedInTemplate.length > 0) {
|
||||
warnings.push(
|
||||
`[Unused Key] ${spec.label} template.toml 미사용 키 ${unusedInTemplate.length}개`,
|
||||
);
|
||||
printList(`${spec.label} 코드에서 사용되지 않는 키`, unusedInTemplate);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
@@ -5,9 +5,25 @@ 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 LOCALE_SPECS = [
|
||||
{
|
||||
name: 'root',
|
||||
label: 'root locales',
|
||||
dir: path.join(ROOT_DIR, 'locales'),
|
||||
template: 'template.toml',
|
||||
langs: ['ko.toml', 'en.toml'],
|
||||
ownsKey: (key) => !key.startsWith('ui.common.') && !key.startsWith('msg.common.'),
|
||||
},
|
||||
{
|
||||
name: 'common',
|
||||
label: 'common locales',
|
||||
dir: path.join(ROOT_DIR, 'common', 'locales'),
|
||||
template: 'template.toml',
|
||||
langs: ['ko.toml', 'en.toml'],
|
||||
ownsKey: (key) => key.startsWith('ui.common.') || key.startsWith('msg.common.'),
|
||||
},
|
||||
];
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
'.git',
|
||||
@@ -81,7 +97,6 @@ function parseTomlKeys(filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip quotes if present
|
||||
if (key.startsWith('"') && key.endsWith('"')) {
|
||||
key = key.slice(1, -1);
|
||||
}
|
||||
@@ -141,22 +156,20 @@ function collectCodeKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
function filterCodeKeys(rawKeys) {
|
||||
return Array.from(rawKeys).filter((k) =>
|
||||
!k.includes('.msg.') &&
|
||||
!k.includes('.ui.') &&
|
||||
!k.includes('.err.') &&
|
||||
!k.includes('.test.') &&
|
||||
!k.includes('.non.') &&
|
||||
!k.startsWith('ui.admin.users.list.table.') &&
|
||||
!k.startsWith('msg.admin.users.detail.') &&
|
||||
!k.startsWith('msg.common.') &&
|
||||
!k.startsWith('msg.dev.clients.') &&
|
||||
!k.startsWith('ui.admin.users.create.') &&
|
||||
!k.startsWith('ui.admin.users.detail.') &&
|
||||
!k.startsWith('ui.common.') &&
|
||||
!k.startsWith('ui.dev.clients.') &&
|
||||
!k.startsWith('ui.dev.session.')
|
||||
function shouldIgnoreCodeKey(key) {
|
||||
return (
|
||||
key.includes('.msg.') ||
|
||||
key.includes('.ui.') ||
|
||||
key.includes('.err.') ||
|
||||
key.includes('.test.') ||
|
||||
key.includes('.non.') ||
|
||||
key.startsWith('ui.admin.users.list.table.') ||
|
||||
key.startsWith('msg.admin.users.detail.') ||
|
||||
key.startsWith('msg.dev.clients.') ||
|
||||
key.startsWith('ui.admin.users.create.') ||
|
||||
key.startsWith('ui.admin.users.detail.') ||
|
||||
key.startsWith('ui.dev.clients.') ||
|
||||
key.startsWith('ui.dev.session.')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,62 +183,82 @@ function difference(aSet, bSet) {
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
function collectSpecResources(spec) {
|
||||
const templatePath = path.join(spec.dir, spec.template);
|
||||
const templateResult = parseTomlKeys(templatePath);
|
||||
if (!templateResult.ok) {
|
||||
return { ok: false, error: templateResult.error };
|
||||
}
|
||||
|
||||
const langKeyMap = new Map();
|
||||
for (const fileName of spec.langs) {
|
||||
const langPath = path.join(spec.dir, fileName);
|
||||
const langResult = parseTomlKeys(langPath);
|
||||
if (!langResult.ok) {
|
||||
return { ok: false, error: langResult.error };
|
||||
}
|
||||
langKeyMap.set(fileName, langResult.keys);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
templateKeys: templateResult.keys,
|
||||
langKeyMap,
|
||||
};
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
const report = {
|
||||
generated_at: new Date().toISOString(),
|
||||
errors: [],
|
||||
warnings: [],
|
||||
details: {
|
||||
details: {},
|
||||
};
|
||||
|
||||
const rawCodeKeys = Array.from(collectCodeKeys()).filter(
|
||||
(key) => !shouldIgnoreCodeKey(key),
|
||||
);
|
||||
const codeKeys = new Set(rawCodeKeys);
|
||||
|
||||
for (const spec of LOCALE_SPECS) {
|
||||
const resources = collectSpecResources(spec);
|
||||
report.details[spec.name] = {
|
||||
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 = new Set(filterCodeKeys(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);
|
||||
if (!resources.ok) {
|
||||
report.errors.push(resources.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;
|
||||
for (const [fileName, langKeys] of resources.langKeyMap.entries()) {
|
||||
const missingInLang = difference(resources.templateKeys, langKeys);
|
||||
if (missingInLang.length > 0) {
|
||||
report.errors.push(
|
||||
`[Sync Error] ${spec.label} ${fileName} 누락 키 ${missingInLang.length}개`,
|
||||
);
|
||||
report.details[spec.name].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 ownedCodeKeys = new Set(rawCodeKeys.filter((key) => spec.ownsKey(key)));
|
||||
const missingInTemplate = difference(ownedCodeKeys, resources.templateKeys);
|
||||
if (missingInTemplate.length > 0) {
|
||||
report.errors.push(
|
||||
`[Missing Key] ${spec.label} template.toml 누락 키 ${missingInTemplate.length}개`,
|
||||
);
|
||||
report.details[spec.name].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;
|
||||
const unusedInTemplate = difference(resources.templateKeys, codeKeys);
|
||||
if (unusedInTemplate.length > 0) {
|
||||
report.warnings.push(
|
||||
`[Unused Key] ${spec.label} template.toml 미사용 키 ${unusedInTemplate.length}개`,
|
||||
);
|
||||
report.details[spec.name].unused_in_template = unusedInTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
return report;
|
||||
@@ -258,8 +291,12 @@ function main() {
|
||||
fs.writeFileSync(summaryPath, lines.join('\n'));
|
||||
|
||||
if (report.errors.length > 0) {
|
||||
console.error('❌ i18n report generated with errors');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ i18n report written to ${outPath}`);
|
||||
console.log(`✅ i18n summary written to ${summaryPath}`);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
Reference in New Issue
Block a user