UNPKG

multi-lang-lib

Version:

A simple multilingual utility for JS/TS apps

234 lines (232 loc) 7.94 kB
class MultiLang { constructor(settings = {}, languages) { this.langs = []; this.data = {}; if (Object.keys(languages).length === 0) { throw new Error("Language list cannot be empty"); } this.debug = settings.debug ?? false; this.initial = settings.initial; Object.entries(languages).forEach(([lang, langData]) => { const code = lang; this.langs.push(code); this.data[code] = langData; }); if (this.initial && this.langs.includes(this.initial)) { this.current = this.initial; } else { this.current = this.langs[0]; } } /** * Sets the current language. Custom or regional codes are allowed, * as long as at least one language from the fallback chain is loaded. * * @param lang ISO language code (e.g., 'ru_RU') */ setLang(lang) { const fallbackChain = this.getLangChain(lang); const hasLoadedLang = fallbackChain.some(code => this.langs.includes(code)); if (!hasLoadedLang) { throw new Error(`Language ${lang} and its fallbacks are not loaded`); } this.current = lang; } /** * Returns the currently active language code. */ getLang() { return this.current; } /** * Returns a list of all loaded languages. */ getAvailableLangs() { return [...this.langs]; } /** * Adds a new language or merges data into an existing one. * * @param lang ISO language code. * @param data Language dictionary. */ addLang(lang, data) { if (this.langs.includes(lang)) { this.data[lang] = { ...this.data[lang], ...data }; } else { this.langs.push(lang); this.data[lang] = data; } } /** * Checks if a translation key exists in the specified or current language. * * @param key Dot-notated key (e.g., 'menu.settings.title'). * @param lang Optional language code to check against. */ has(key, lang = this.current) { return this.get(key, lang) !== undefined; } /** * Lists all translation keys in the specified or current language. * * @param lang Language code (defaults to current). * @returns Array of flattened keys. */ listKeys(lang = this.current) { const flatten = (obj, path = '') => { return Object.entries(obj).reduce((acc, [k, v]) => { const newPath = `${path}${k}`; if (typeof v === 'object' && v !== null) { acc.push(...flatten(v, `${newPath}.`)); } else { acc.push(newPath); } return acc; }, []); }; const langData = this.data[lang]; return langData ? flatten(langData) : []; } /** * Retrieves a translated string by key with fallback support and optional variable substitution. * * @param key Dot-notated translation key (e.g., 'menu.settings.title'). * @param lang Language code (defaults to current). * @param vars Optional map of variables to substitute in the string. * @returns Translated string or undefined if not found. */ get(key, lang = this.current, vars) { const fallbackChain = this.getLangChain(lang); for (const langCode of fallbackChain) { const result = this.deepGet(this.data[langCode], key); if (typeof result === 'string') { return vars ? this.format(result, vars) : result; } } if (this.debug) { console.warn(`[MultiLang] Missing key "${key}" in "${lang}"`); } return undefined; } /** * Internal utility to deeply access a property via dot notation. * * @param obj Language data object. * @param path Dot-notated key path (e.g., 'a.b.c'). * @returns Value at the specified path, if found. */ deepGet(obj, path) { if (!obj) return undefined; const parts = path.split('.'); let current = obj; for (const part of parts) { if (typeof current === 'object' && current !== null && part in current) { current = current[part]; } else { return undefined; } } return typeof current === 'string' ? current : undefined; } /** * Builds the fallback language chain: lang → base → initial. * * @param lang The requested language. * @returns Array of fallback language codes in priority order. */ getLangChain(lang) { const chain = []; if (this.langs.includes(lang)) chain.push(lang); const base = lang.split('_')[0]; if (base !== lang && this.langs.includes(base)) chain.push(base); if (this.initial && !chain.includes(this.initial)) { chain.push(this.initial); } return chain; } /** * Replaces variables in a string with their values. * * @param template The template string (e.g., 'Hello, {name}'). * @param vars A key-value map of variables. * @returns Formatted string. */ format(template, vars) { return template.replace(/{(\w+)}/g, (_, key) => { return vars[key] ?? `{${key}}`; }); } /** * Formats a number using locale-aware options (e.g. currency, percent). * * @param value The numeric value to format. * @param options Intl.NumberFormat options. * @param lang Language code (defaults to current). * @returns Formatted number string. */ formatNumber(value, options = {}, lang = this.current) { try { const locale = lang.replace('_', '-'); const formatter = new Intl.NumberFormat(locale, options); return formatter.format(value); } catch (e) { if (this.debug) { console.warn(`[MultiLang] Failed to format number for "${lang}":`, e); } return value.toString(); } } /** * Formats a date or time value using locale-aware options. * * @param date Date object, timestamp or ISO string. * @param options Intl.DateTimeFormat options. * @param lang Language code (defaults to current). * @returns Formatted date string. */ formatDate(date, options = {}, lang = this.current) { try { const locale = lang.replace('_', '-'); const formatter = new Intl.DateTimeFormat(locale, options); return formatter.format(new Date(date)); } catch (e) { if (this.debug) { console.warn(`[MultiLang] Failed to format date for "${lang}":`, e); } return new Date(date).toLocaleString(); } } /** * Formats a relative time string (e.g. "yesterday", "in 2 days"). * * @param value Numeric offset (e.g. -1 = "yesterday", 1 = "tomorrow"). * @param unit Time unit (e.g. 'day', 'month', 'year'). * @param options Intl.RelativeTimeFormat options. * @param lang Language code (defaults to current). * @returns Formatted relative time string. */ formatRelative(value, unit, options = {}, lang = this.current) { try { const locale = lang.replace('_', '-'); const formatter = new Intl.RelativeTimeFormat(locale, options); return formatter.format(value, unit); } catch (e) { if (this.debug) { console.warn(`[MultiLang] Failed to format relative time for "${lang}":`, e); } return `${value} ${unit}`; } } } export { MultiLang };