@kwaeri/i18n
Version:
The internationalization component module of the @kwaeri platform.
246 lines • 13.1 kB
JavaScript
/**
* SPDX-PackageName: kwaeri/i18n
* SPDX-PackageVersion: 0.5.0
* SPDX-FileCopyrightText: © 2014 - 2022 Richard Winters <kirvedx@gmail.com> and contributors
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception OR MIT
*/
'use strict';
import { kdt } from '@kwaeri/developer-tools';
import * as fs from 'fs/promises';
import * as path from 'path';
import debug from 'debug';
// DEFINES
const _ = new kdt(), DEBUG = debug('kwaeri:i18n');
export var kwaeri;
(function (kwaeri) {
let Internationalization;
(function (Internationalization) {
class i18n {
/**
* The default locale
*/
defaultLocale;
/**
* The default locale's base locale
*/
defaultLocaleBase;
/**
* The currently configured locale
*/
locale;
/**
* The currently configured locale's base locale. It may seem redundant, but
* for a logical message resolution process, we search for a message from
* the configured locale - should it not be found we'd then check the base
* locale for a given locale (i.e. "en" for "en_US"), if it is still not found
* (either because it was already a base locale, messages for a base locale
* aren't provided for a locale), we'd finally check the default locale.
* before we decide that a message translation just doesn't exist.
*/
localeBase;
/**
* A [list of] provided locale(s)
*/
locales;
/**
* A map of loaded messages per locale that has been provided
*/
map = {};
/**
* A convenience flag that denotes whether translations have been
* mapped or not.
*/
mapped = false;
/**
* IL8N constructor
*
* @param { string[] } locales A provided locale, or list of locales.Can be provided as individual arguments or as an array of strings.
*/
constructor(...locales) {
this.locales = locales;
}
/**
* Initializes IL8N; Sets default and configured locales and loads translations
*
* @returns { Promise<Locale.ResultMap|null> } A promise of a {@link Locale.ResultMap } or null
*/
async init() {
try {
// Fetch the default locale
this.defaultLocale = JSON.parse((await fs.readFile(path.join('./', 'package.json'), { encoding: "utf8" }))).default_locale || "en";
if (this.defaultLocale?.includes("_"))
this.defaultLocaleBase = this.defaultLocale.split("_")[0];
else
this.defaultLocaleBase = this.defaultLocale;
// Set the assigned locale; defer to the default and if it does
// not exist set it to en.
this.locale = (this.locales && this.locales.length) ? this.locales[0] : this.defaultLocale;
if (this.locale?.includes("_"))
this.localeBase = this.locale.split("_")[0];
else
this.localeBase = this.locale;
// Compile a list of all locales provides, and load them all
const locales = (this.locales && this.locales.length) ? [this.defaultLocale, ...this.locales] : [this.defaultLocale];
if (locales)
return Promise.resolve(await this.loadLocales(locales));
else
return Promise.resolve(null);
}
catch (exception) {
DEBUG(`Error: ${exception.message}`);
return Promise.reject(exception);
}
}
/**
* Method to fetch translations per locale provided
*
* @param { string[] } locales An array of strings, each a locale requested
*
* @returns {}
*/
async loadLocales(locales) {
return new Promise(async (resolve, reject) => {
// Indicate if all passed locales were found and loaded
const block = await Promise.all(locales.map(async (locale) => {
const localePath = path.join("_locales", locale, "messages.json");
DEBUG(`Read '${locale} translations from '${localePath.toString()};`);
try {
const localeMessages = await fs.readFile(localePath, { encoding: 'utf8' });
this.map[locale] = JSON.parse(localeMessages);
return true;
}
catch (exception) {
return false;
}
}));
// Translations have been mapped
this.mapped = true;
// Return a map of locales and whether or not they were
// found and loaded
resolve(Object.fromEntries(locales.map(($, i) => [locales[i], block[i]])));
});
}
/**
* Returns the translation of a message for a configured locale. Up to 8 placeholder/template arguments may be passed in after the message name.
*
* @param { string } name The name of the internationalized message
* @params Up to 8 placeholder/template values
*
* @returns { string } The requested message translated for a configured locale
*/
getMessage(name, ...placeholders) {
// All available translations have been loaded
if (this.mapped)
// Check first the configured locale
if (this.map.hasOwnProperty(this.locale) &&
this.map[this.locale].hasOwnProperty(name)) {
return this.rasterizeString(this.map[this.locale][name], placeholders);
}
else {
// If message translation wasnt found check the base locale
if (this.map.hasOwnProperty(this.localeBase) &&
this.map[this.localeBase].hasOwnProperty(name)) {
return this.rasterizeString(this.map[this.localeBase][name], placeholders);
}
else {
// If message translation still unfound check the default locale
if (this.map.hasOwnProperty(this.defaultLocale) &&
this.map[this.defaultLocale].hasOwnProperty(name)) {
return this.rasterizeString(this.map[this.defaultLocale][name], placeholders);
}
else {
// Finally check the default locale's base locale
if (this.map.hasOwnProperty(this.defaultLocaleBase) &&
this.map[this.defaultLocaleBase].hasOwnProperty(name)) {
return this.rasterizeString(this.map[this.defaultLocaleBase][name], placeholders);
}
else {
return "";
}
}
}
}
else
return "";
}
/**
* Similar to {@link getMessage}, returns the translation of a message for a provided locale, allowing up to 8 placeholder/template arguments.
*
* Unlike {@link getMessage}, should a message not be found for the provided locale no additional searhing will be done.
*
* @param { string } name The name of the internationalized message
* @param { string } locale The localization for the provided message
* @params Up to 8 placeholder/template values
*
* @returns { string } The requested message for a provided locale.
*/
getLocalisedMessage(name, locale, ...placeholders) {
if (this.mapped)
if (this.map.hasOwnProperty(locale) &&
this.map[locale].hasOwnProperty(name)) {
return this.rasterizeString(this.map[locale][name], placeholders);
}
else {
return "";
}
else
return "";
}
/**
* Strips placeholders from a message where placeholder content is provided to replace them,
*
* @param { Locale.MessageBits } bits
* @params placeholders
*/
rasterizeString(bits, ...placeholders) {
// Check whether there are placeholders for this message
if (bits.hasOwnProperty("placeholders") && placeholders) {
// Not sure why placeholders acts so wierd in typescript - whatevs:
const _placeholders = (placeholders && placeholders.length) ? placeholders[0] : [];
// Get placeholder keys so we can identify them in string
const replaceables = Object.keys(bits.placeholders);
// Get the template message so it remains unmodified in the map
let returnable = bits.message;
// If placeholders are defined for the message and placeholder values are provided
if (replaceables.length && _placeholders.length) {
DEBUG(`Replacing message placeholders with supplied values`);
// Track how many placeholders we've replaced to reduce loops
let countReplaced = 0;
// While we havent replaced as many values as has been provided to replace,
// and haven't replaced more than we can replace
while ((countReplaced < _placeholders.length) && (countReplaced < replaceables.length)) {
// For each placeholder defined
for (let _placeholder in bits.placeholders) {
// Get the placeholders position in the message (and convert it to 0-based)
const position = (+(bits.placeholders[_placeholder].content.charAt(1))) - 1;
// So long as its position in the template is a 0-based index value that's equal to one that's
// of the placeholder arguments supplied - indicating that its value has been supplied under
// the presumption that values are given in the order that they appear in the message; replace
// said placeholder with the respective placeholder value:supplied
if (position < _placeholders.length) {
DEBUG(`Substitute '${_placeholder}' with '${_placeholders[position]}'`);
returnable = returnable.replace(/*/\$(.*)\$??/*/ `$${_placeholder}$`, _placeholders[position]);
countReplaced++;
}
}
}
// OLD IMPLEMENTATION - ASSUMED ORDER DEFINED AND ORDER PROVIDED MUST MATCH, WHICH WAS UNSAFE
//
// If any exist, and any were provided, substitute the placeholders
// for the template content
//for( let i = 0; ( ( i < _placeholders.length ) && ( i < templateables.length ) && ( i < 8 ) ); i++ ) {
// DEBUG( `Substitute '${templateables[i]}' with '${_placeholders[i]}'` );
// //if( i < templateables.length )
// returnable = returnable.replace( /*/\$(.*)\$??/*/`$${templateables[i]}$`, _placeholders[i] );
//}
}
return returnable;
}
else
return bits.message;
}
}
Internationalization.i18n = i18n;
})(Internationalization = kwaeri.Internationalization || (kwaeri.Internationalization = {}));
})(kwaeri || (kwaeri = {}));
//# sourceMappingURL=i18n.mjs.map