@j2inn/i18n
Version:
J2 Innovations internationalization library
230 lines (229 loc) • 9.05 kB
JavaScript
/*
* Copyright (c) 2025, J2 Innovations. All Rights Reserved
*/
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _I18n_localeMap;
/**
* Process the locale data into a map.
*/
function processLocaleDataIntoMap(baseKey, localeMap, localeData) {
for (const key of Object.keys(localeData)) {
const mapKey = baseKey ? `${baseKey}.${key}` : key;
const value = localeData[key];
if (typeof value === 'string') {
localeMap[mapKey] = value;
}
else if (value && typeof value === 'object') {
processLocaleDataIntoMap(mapKey, localeMap, value);
}
}
}
/**
* Loads, caches and queries localization data.
*/
export class I18n {
constructor(localeData = {}, antdLocaleData, locale = antdLocaleData?.locale) {
/**
* Cached locale strings.
*/
_I18n_localeMap.set(this, {}
/**
* Cached locale data for ant design.
*/
);
/**
* The loading flag.
*/
this.loading = false;
/**
* Get a localized string value.
*
* @param key The language key.
* @param values The handlebar values to be substitued.
* @returns The localized string value.
*/
this.get = (key, values) => {
if (typeof __classPrivateFieldGet(this, _I18n_localeMap, "f")[key] === 'string') {
// If we have a value for this key return it.
return I18n.replaceDynamicValues(String(__classPrivateFieldGet(this, _I18n_localeMap, "f")[key]), values);
}
else if (typeof __classPrivateFieldGet(this, _I18n_localeMap, "f")[key.toLowerCase()] === 'string') {
// If we have a value for this key (but lowercased) return it.
return I18n.replaceDynamicValues(String(__classPrivateFieldGet(this, _I18n_localeMap, "f")[key.toLowerCase()]), values);
}
// Otherwise, return the key but print a warning about missing key.
console.warn(`Missing translation key for: "${key}"`);
return I18n.replaceDynamicValues(key, values);
};
/**
* Get a localized string value.
*
* ```typescript
* const {t} = useI18n()
* const localizedStr = t('foo.bar')
* ```
*
* @param key The language key.
* @param values The handlebar values to be substitued.
* @returns The localized string value.
*/
this.t = (key, values) => {
return this.get(key, values);
};
/**
* @returns All the keys used for localization.
*/
this.keys = () => {
return Object.keys(__classPrivateFieldGet(this, _I18n_localeMap, "f"));
};
/**
* Return true key is available.
*
* @param key The key to test.
* @returns true if the specified key is available.
*/
this.has = (key) => {
return (typeof __classPrivateFieldGet(this, _I18n_localeMap, "f")[key] === 'string' ||
typeof __classPrivateFieldGet(this, _I18n_localeMap, "f")[key.toLowerCase()] === 'string');
};
/**
* Import the locale data from the server for the given path.
*
* @param locale The language locale to use.
* @param path The path URI.
* @param fetch An optional alternative fetch function to use for
* the network request.
*/
this.import = async (locale, path, fetchFunc = fetch) => {
try {
this.loading = true;
this.locale = I18n.toLocaleIdentifier(locale);
this.intl = new Intl.Locale(locale);
__classPrivateFieldSet(this, _I18n_localeMap, {}, "f");
this.antdLocaleData = undefined;
path = I18n.replaceDynamicValues(path, {
locale: path.includes('fin5Lang')
? I18n.getLanguageCode(locale)
: locale,
});
const res = await fetchFunc(path);
let localeData;
// Use the response's content type to decide how to decode the data.
if (res.headers
.get('content-type')
?.toLowerCase()
.includes('text/plain')) {
localeData = I18n.parseFin5LanguageFile(await res.text());
}
else {
localeData = await res.json();
}
processLocaleDataIntoMap('', __classPrivateFieldGet(this, _I18n_localeMap, "f"), localeData);
try {
const antLocaleCode = I18n.toLocaleProvider(locale);
const data = await import(`antd/es/locale-provider/${antLocaleCode}.js`);
this.antdLocaleData = data.default;
}
catch (ignore) { }
}
finally {
this.loading = false;
}
};
processLocaleDataIntoMap('', __classPrivateFieldGet(this, _I18n_localeMap, "f"), localeData);
this.antdLocaleData = antdLocaleData;
this.locale = locale;
this.intl = locale === undefined ? undefined : new Intl.Locale(locale);
}
/**
* @returns A copy of the internal locale string map.
*/
get internalMap() {
return { ...__classPrivateFieldGet(this, _I18n_localeMap, "f") };
}
/**
* Replace the values in the string with the supplied values.
*
* The string should use handlebars style syntax to define the
* variables to be replaced. For example, `Hello {{world}} will have
* `world` replaced by an object that has a `world` property.
*
* @param str The string to update.
* @param values The values to be used in the update process.
* @returns The updated string.
*/
static replaceDynamicValues(str, values) {
let res = str;
if (values) {
for (const key of Object.keys(values)) {
res = res.replace(`{{${key}}}`, values[key]);
}
}
return res;
}
/**
* Parse a FIN5 language file and return the locale data.
*
* @param input The input text.
* @returns The locale data.
*/
static parseFin5LanguageFile(input) {
const localeData = {};
const lines = input.split('\n');
for (let i = 0, len = lines.length; i < len; i++) {
const line = lines[i].trim();
if (line.length < 1 || line.startsWith('//')) {
continue;
}
const equalIndex = line.indexOf('=');
const key = line.slice(0, equalIndex).trim();
const value = line.slice(equalIndex + 1).trim();
localeData[key] = value;
}
return localeData;
}
/**
* Extracts the ISO 639-1 langage code from the locale.
*
* @param locale The locale.
* @returns The language code.
*/
static getLanguageCode(locale) {
return locale.replace(/^(\w+)[_-]\w+$/, '$1');
}
/**
* Convert the locale to a valid unicode locale identifier.
*
* @param locale The locale tag to convert.
* @returns The language tag.
*/
static toLocaleIdentifier(locale) {
return locale.replace(/_/g, '-');
}
/**
* Convert the locale into an ant design compatible locale provider.
*
* @param locale The locale tag to convert.
* @returns The locale provider.
*/
static toLocaleProvider(locale) {
const parts = locale.split('-');
// If no region is specified, add a default region.
if (parts.length === 1) {
parts.push(parts[0]);
}
parts[1] = parts[1].toUpperCase();
return parts.join('_');
}
}
_I18n_localeMap = new WeakMap();