UNPKG

@kwaeri/i18n

Version:

The internationalization component module of the @kwaeri platform.

246 lines 13.1 kB
/** * 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