multi-lang-lib
Version:
A simple multilingual utility for JS/TS apps
236 lines (233 loc) • 7.96 kB
JavaScript
'use strict';
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}`;
}
}
}
exports.MultiLang = MultiLang;