126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { ko } from '../src/i18n/locales/ko';
|
|
import { en } from '../src/i18n/locales/en';
|
|
|
|
/**
|
|
* 전역 설정 및 상수
|
|
*/
|
|
const TARGET_DIR = path.join(__dirname, '../tests');
|
|
const IGNORE_FILES = ['node_modules', '.git'];
|
|
const LOCALES = { ko, en };
|
|
|
|
type I18nEntry = { key: string; locales: string[] };
|
|
const i18nValueToKey = new Map<string, I18nEntry>();
|
|
|
|
/**
|
|
* i18n 객체를 평탄화하여 '값 -> 키' 매핑을 생성합니다.
|
|
*/
|
|
function walk(obj: any, prefix = '', locale = '') {
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
if (typeof value === 'string') {
|
|
const entry = i18nValueToKey.get(value);
|
|
if (entry) {
|
|
if (!entry.locales.includes(locale)) entry.locales.push(locale);
|
|
} else {
|
|
i18nValueToKey.set(value, { key: fullKey, locales: [locale] });
|
|
}
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
walk(value, fullKey, locale);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 로딩
|
|
for (const [locale, data] of Object.entries(LOCALES)) {
|
|
walk(data, '', locale);
|
|
}
|
|
|
|
/**
|
|
* 테스트 파일을 재귀적으로 탐색합니다.
|
|
*/
|
|
function getFiles(dir: string): string[] {
|
|
const results: string[] = [];
|
|
if (!fs.existsSync(dir)) return results;
|
|
|
|
const list = fs.readdirSync(dir);
|
|
for (const file of list) {
|
|
if (IGNORE_FILES.some(ignore => file.includes(ignore))) continue;
|
|
const fullPath = path.join(dir, file);
|
|
const stat = fs.statSync(fullPath);
|
|
if (stat.isDirectory()) {
|
|
results.push(...getFiles(fullPath));
|
|
} else if (file.endsWith('.test.ts') || file.endsWith('.spec.ts') || (dir.includes('tests') && file.endsWith('.ts'))) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* 파일 내에서 하드코딩된 i18n 값을 찾습니다.
|
|
*/
|
|
function checkFile(filePath: string) {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const lines = content.split('\n');
|
|
let matchCount = 0;
|
|
|
|
lines.forEach((line, index) => {
|
|
// 0. 무시 주석 체크
|
|
if (line.includes('i18n-ignore')) return;
|
|
|
|
// 따옴표로 둘러싸인 모든 문자열을 찾습니다.
|
|
// 인덱스를 추적하기 위해 수동으로 문자열을 찾습니다.
|
|
const regex = /(['"`])(.*?)\1/g;
|
|
let match;
|
|
while ((match = regex.exec(line)) !== null) {
|
|
const fullMatch = match[0];
|
|
const val = match[2];
|
|
|
|
if (i18nValueToKey.has(val)) {
|
|
const info = i18nValueToKey.get(val)!;
|
|
|
|
// 1. 만약 문자열이 i18n 키 자체라면 (점 포함) 무시합니다.
|
|
if (val === info.key || (val.includes('.') && !val.includes(' '))) continue;
|
|
|
|
// 2. t(..., 'key') 에서 'key'가 값과 같은 경우 무시 (매우 드문 경우)
|
|
// t() 호출 안에 있는지 대략적으로 체크
|
|
const linePrefix = line.substring(0, match.index);
|
|
if (linePrefix.trim().endsWith('t(') || linePrefix.includes('t(')) {
|
|
// 호출 인자로 보인다면 패스 (단순화된 로직)
|
|
if (val === info.key) continue;
|
|
}
|
|
|
|
console.log(`[FOUND] ${path.relative(process.cwd(), filePath)}:${index + 1}`);
|
|
console.log(` - Hardcoded: ${fullMatch}`);
|
|
console.log(` - Suggested: t(locale, '${info.key}')`);
|
|
console.log(` - Values: "${val}"`);
|
|
console.log(` - Locales: ${info.locales.join(', ')}`);
|
|
console.log('');
|
|
matchCount++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return matchCount;
|
|
}
|
|
|
|
// 실행
|
|
console.log('--- i18n Reference Check in Tests ---');
|
|
console.log(`Total i18n values loaded: ${i18nValueToKey.size}`);
|
|
|
|
const files = getFiles(TARGET_DIR);
|
|
console.log(`Checking ${files.length} test files...`);
|
|
|
|
let totalMatch = 0;
|
|
files.forEach(file => {
|
|
totalMatch += checkFile(file);
|
|
});
|
|
|
|
if (totalMatch === 0) {
|
|
console.log('✅ No hardcoded i18n values found in tests.');
|
|
} else {
|
|
console.log(`❌ Found ${totalMatch} violations.`);
|
|
}
|