/** * UI label loader utility * * Loads user-facing display strings from language-specific YAML files * (labels_en.yaml / labels_ja.yaml) and provides * key-based access with template variable substitution. * * This module handles UI labels only — AI prompts live in ../prompts/. */ import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import type { Language } from '../../core/models/types.js'; import { DEFAULT_LANGUAGE } from '../constants.js'; /** Cached YAML data per language */ const labelCache = new Map>(); function loadLabels(lang: Language): Record { const cached = labelCache.get(lang); if (cached) return cached; const __dirname = dirname(fileURLToPath(import.meta.url)); const yamlPath = join(__dirname, `labels_${lang}.yaml`); const content = readFileSync(yamlPath, 'utf-8'); const data = parseYaml(content) as Record; labelCache.set(lang, data); return data; } /** * Resolve a dot-separated key path to a value in a nested object. * Returns undefined if the path does not exist. */ function resolveKey(obj: Record, keyPath: string): unknown { const parts = keyPath.split('.'); let current: unknown = obj; for (const part of parts) { if (current === null || current === undefined || typeof current !== 'object') { return undefined; } current = (current as Record)[part]; } return current; } /** * Replace {key} placeholders in a template string with values from vars. * Unmatched placeholders are left as-is. */ function applyVars(template: string, vars: Record): string { return template.replace(/\{(\w+)\}/g, (match, key: string) => { if (key in vars) { const value: string = vars[key] as string; return value; } return match; }); } /** * Get a UI label string from the language-specific YAML by dot-separated key. * * When `lang` is provided, loads the corresponding language file. * When `lang` is omitted, uses DEFAULT_LANGUAGE. * * Template variables in `{name}` format are replaced when `vars` is given. */ export function getLabel( key: string, lang?: Language, vars?: Record, ): string { const effectiveLang = lang ?? DEFAULT_LANGUAGE; const data = loadLabels(effectiveLang); const value = resolveKey(data, key); if (typeof value !== 'string') { throw new Error(`Label key not found: ${key}${lang ? ` (lang: ${lang})` : ''}`); } if (vars) { return applyVars(value, vars); } return value; } /** * Get a nested object from the language-specific YAML by dot-separated key. * * When `lang` is provided, loads the corresponding language file. * When `lang` is omitted, uses DEFAULT_LANGUAGE. * * Useful for structured label groups (e.g. UI text objects). */ export function getLabelObject(key: string, lang?: Language): T { const effectiveLang = lang ?? DEFAULT_LANGUAGE; const data = loadLabels(effectiveLang); const value = resolveKey(data, key); if (value === undefined || value === null) { throw new Error(`Label key not found: ${key}${lang ? ` (lang: ${lang})` : ''}`); } return value as T; } /** Reset cached data (for testing) */ export function _resetLabelCache(): void { labelCache.clear(); }