import { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, type Locale, SUPPORTED_LOCALES, type TomlObject, type TomlValue, type TranslatorInput, type TranslatorOptions, } from "./types"; function mergeTomlObjects(base: TomlObject, override: TomlObject): TomlObject { const result: TomlObject = { ...base }; for (const [key, value] of Object.entries(override)) { const currentValue = result[key]; if ( typeof currentValue === "object" && currentValue !== null && typeof value === "object" && value !== null ) { result[key] = mergeTomlObjects(currentValue as TomlObject, value); continue; } result[key] = value; } return result; } function isSupportedLocale(value: string): value is Locale { return (SUPPORTED_LOCALES as readonly string[]).includes(value); } function parseToml(raw: string): TomlObject { const lines = raw.split(/\r?\n/); const root: TomlObject = {}; let currentPath: string[] = []; for (const rawLine of lines) { const line = rawLine.trim(); if (!line || line.startsWith("#")) { continue; } if (line.startsWith("[") && line.endsWith("]")) { const sectionName = line.slice(1, -1).trim(); currentPath = sectionName ? sectionName .split(".") .map((part) => part.trim()) .filter(Boolean) : []; continue; } const eqIndex = line.indexOf("="); if (eqIndex === -1) { continue; } const key = line.slice(0, eqIndex).trim(); const valueRaw = line.slice(eqIndex + 1).trim(); if (!key) { continue; } let value = valueRaw; if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } let cursor: TomlObject = root; for (const section of currentPath) { if (!cursor[section] || typeof cursor[section] === "string") { cursor[section] = {}; } cursor = cursor[section] as TomlObject; } cursor[key] = value; } return root; } function getValue(target: TomlObject, key: string): string | undefined { const parts = key.split("."); let cursor: TomlValue = target; for (const part of parts) { if (typeof cursor !== "object" || cursor === null) { return undefined; } cursor = (cursor as TomlObject)[part]; if (cursor === undefined) { return undefined; } } return typeof cursor === "string" ? cursor : undefined; } function detectLocale(): Locale { if (typeof window === "undefined") { return DEFAULT_LOCALE; } const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY); if (stored && isSupportedLocale(stored)) { return stored; } const pathLocale = window.location.pathname.split("/")[1]; if (pathLocale && isSupportedLocale(pathLocale)) { return pathLocale; } const browserLang = window.navigator.language.toLowerCase(); if (browserLang.startsWith("ko")) { return "ko"; } return DEFAULT_LOCALE; } function formatTemplate( template: string, vars?: Record, options?: TranslatorOptions, ): string { const normalizedTemplate = options?.normalizeEscapedNewlines ? template.replace(/\\n/g, "\n") : template; if (!vars) { return normalizedTemplate; } return normalizedTemplate.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => { const value = vars[key]; if (value === undefined || value === null) { return match; } return String(value); }); } export function createTomlTranslator( input: TranslatorInput, options?: TranslatorOptions, ) { const translations: Record = { ko: input.ko .map((raw) => parseToml(raw)) .reduce( (merged, current) => mergeTomlObjects(merged, current), {}, ), en: input.en .map((raw) => parseToml(raw)) .reduce( (merged, current) => mergeTomlObjects(merged, current), {}, ), }; return function t( key: string, fallback?: string, vars?: Record, ): string { const locale = detectLocale(); const value = getValue(translations[locale], key); if (value && value.length > 0) { return formatTemplate(value, vars, options); } return formatTemplate(fallback ?? key, vars, options); }; }