Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
28 lines
1.8 KiB
JavaScript
28 lines
1.8 KiB
JavaScript
/**
|
|
* lib/i18n.mjs — 화면 문구 사전. 문구를 코드에 박지 않고 lang/ko.json 에서 읽는다(§9 규약).
|
|
* t('auth.login.failed') → 문자열. {name} 자리표시자는 vars 로 치환. 없는 키는 키 그대로 돌려줘 누락이 화면에 드러난다.
|
|
*/
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
let dict = null; let loadedAt = 0;
|
|
/** lang/ko/*.json 을 모두 읽어 깊게 합친다(영역마다 파일을 나눠 충돌 없이 관리). 운영 중 수정은 5초 안에 반영. */
|
|
function load() {
|
|
if (dict && Date.now() - loadedAt < 5000) return dict;
|
|
const dir = join(ROOT, 'lang', 'ko'); const merged = {};
|
|
const deepMerge = (a, b) => { for (const [k, v] of Object.entries(b)) { if (v && typeof v === 'object' && !Array.isArray(v) && a[k] && typeof a[k] === 'object') deepMerge(a[k], v); else a[k] = v; } return a; };
|
|
try { for (const f of readdirSync(dir).filter((x) => x.endsWith('.json')).sort()) { try { deepMerge(merged, JSON.parse(readFileSync(join(dir, f), 'utf8'))); } catch (e) { console.error(`[i18n] ${f} 읽기 실패: ${e.message}`); } } } catch (_) {}
|
|
dict = merged; loadedAt = Date.now(); return dict;
|
|
}
|
|
/** 점(.)으로 중첩 키를 찾는다 */
|
|
function lookup(obj, key) { return key.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), obj); }
|
|
export function t(key, vars = {}) {
|
|
const v = lookup(load(), key);
|
|
const s = typeof v === 'string' ? v : key;
|
|
return s.replace(/\{(\w+)\}/g, (_, k) => (vars[k] !== undefined ? String(vars[k]) : `{${k}}`));
|
|
}
|
|
/** 사전 전체(화면 JS 에 내려보낼 때) */
|
|
export function all() { return load(); }
|