@itwin/core-i18n
Version:
iTwin.js localization code
214 lines • 9.94 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Localization
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ITwinLocalization = void 0;
const i18next_1 = __importDefault(require("i18next"));
const i18next_browser_languagedetector_1 = __importDefault(require("i18next-browser-languagedetector"));
const i18next_http_backend_1 = __importDefault(require("i18next-http-backend"));
const core_bentley_1 = require("@itwin/core-bentley");
const DEFAULT_MAX_RETRIES = 1; // a low number prevents wasted time and potential timeouts when requesting localization files throws an error
/** Supplies localizations for iTwin.js
* @note this class uses the [i18next](https://www.i18next.com/) package.
* @public
*/
class ITwinLocalization {
i18next;
_initOptions;
_backendOptions;
_detectionOptions;
_namespaces = new Map();
constructor(options) {
this.i18next = i18next_1.default.createInstance();
this._backendOptions = {
loadPath: options?.urlTemplate ?? "locales/{{lng}}/{{ns}}.json",
crossDomain: true,
...options?.backendHttpOptions,
};
this._detectionOptions = {
order: ["querystring", "navigator", "htmlTag"],
lookupQuerystring: "lng",
caches: [],
...options?.detectorOptions,
};
this._initOptions = {
interpolation: { escapeValue: true },
fallbackLng: "en",
maxRetries: DEFAULT_MAX_RETRIES,
backend: this._backendOptions,
detection: this._detectionOptions,
...options?.initOptions,
};
this.i18next
.use(options?.detectorPlugin ?? i18next_browser_languagedetector_1.default)
.use(options?.backendPlugin ?? i18next_http_backend_1.default)
.use(TranslationLogger);
}
async initialize(namespaces) {
// Also consider namespaces passed into constructor
const initNamespaces = [this._initOptions.ns || []].flat();
const combinedNamespaces = new Set([...namespaces, ...initNamespaces]); // without duplicates
const defaultNamespace = this._initOptions.defaultNS ?? namespaces[0];
if (defaultNamespace)
combinedNamespaces.add(defaultNamespace); // Make sure default namespace is in namespaces list
const initOptions = {
...this._initOptions,
defaultNS: defaultNamespace,
ns: [...combinedNamespaces],
};
// if in a development environment, set debugging
if (process.env.NODE_ENV === "development")
initOptions.debug = true;
const initPromise = this.i18next.init(initOptions);
for (const ns of namespaces)
this._namespaces.set(ns, initPromise);
return initPromise;
}
/** Replace all instances of `%{key}` within a string with the translations of those keys.
* For example:
* ``` ts
* "MyKeys": {
* "Key1": "First value",
* "Key2": "Second value"
* }
* ```
*
* ``` ts
* i18.translateKeys("string with %{MyKeys.Key1} followed by %{MyKeys.Key2}!"") // returns "string with First Value followed by Second Value!"
* ```
* @param line The input line, potentially containing %{keys}.
* @returns The line with all %{keys} translated
* @public
*/
getLocalizedKeys(line) {
return line.replace(/\%\{(.+?)\}/g, (_match, tag) => this.getLocalizedString(tag));
}
/** Return the translated value of a key.
* @param key - the key that matches a property in the JSON localization file.
* @note The key includes the namespace, which identifies the particular localization file that contains the property,
* followed by a colon, followed by the property in the JSON file.
* For example:
* ``` ts
* const dataString: string = IModelApp.localization.getLocalizedString("iModelJs:BackgroundMap.BingDataAttribution");
* ```
* assigns to dataString the string with property BackgroundMap.BingDataAttribution from the iModelJs.json localization file.
* @returns The string corresponding to the first key that resolves.
* @throws Error if no keys resolve to a string.
* @public
*/
getLocalizedString(key, options) {
if (options?.returnDetails || options?.returnObjects) {
throw new Error("Translation key must map to a string, but the given options will result in an object");
}
const value = this.i18next.t(key, options);
if (typeof value !== "string") {
throw new Error("Translation key(s) string not found");
}
return value;
}
/** Gets the English translation.
* @param namespace - the namespace that identifies the particular localization file that contains the property.
* @param key - the key that matches a property in the JSON localization file.
* @returns The string corresponding to the first key that resolves.
* @throws Error if no keys resolve to a string.
* @internal
*/
getEnglishString(namespace, key, options) {
if (options?.returnDetails || options?.returnObjects) {
throw new Error("Translation key must map to a string, but the given options will result in an object");
}
options = {
...options,
ns: namespace, // ensure namespace argument is used
};
const en = this.i18next.getFixedT("en", namespace);
const str = en(key, options);
if (typeof str !== "string")
throw new Error("Translation key(s) not found");
return str;
}
/** Get the promise for an already registered Namespace.
* @param name - the name of the namespace
* @public
*/
getNamespacePromise(name) {
return this._namespaces.get(name);
}
/** @internal */
getLanguageList() {
return this.i18next.languages;
}
/** override the language detected in the browser */
async changeLanguage(language) {
return this.i18next.changeLanguage(language);
}
/** Register a new Namespace and return it. If the namespace is already registered, it will be returned.
* @param name - the name of the namespace, which is the base name of the JSON file that contains the localization properties.
* @note - The registerNamespace method starts fetching the appropriate version of the JSON localization file from the server,
* based on the current locale. To make sure that fetch is complete before performing translations from this namespace, await
* fulfillment of the readPromise Promise property of the returned LocalizationNamespace.
* @see [Localization in iTwin.js]($docs/learning/frontend/Localization.md)
* @public
*/
async registerNamespace(name) {
const existing = this._namespaces.get(name);
if (existing !== undefined)
return existing;
const theReadPromise = new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.i18next.loadNamespaces(name, (err) => {
if (!err)
return resolve();
// Here we got a non-null err object.
// This method is called when the system has attempted to load the resources for the namespaces for each possible locale.
// For example 'fr-ca' might be the most specific locale, in which case 'fr' and 'en' are fallback locales.
// Using Backend from i18next-http-backend, err will be an array of strings of each namespace it tried to read and its locale.
// There might be errs for some other namespaces as well as this one. We resolve the promise unless there's an error for each possible locale.
let locales = this.getLanguageList().map((thisLocale) => `/${thisLocale}/`);
try {
for (const thisError of err) {
if (typeof thisError === "string")
locales = locales.filter((thisLocale) => !thisError.includes(thisLocale));
}
}
catch {
locales = [];
}
// if we removed every locale from the array, it wasn't loaded.
if (locales.length === 0)
core_bentley_1.Logger.logError("i18n", `No resources for namespace ${name} could be loaded`);
resolve();
});
});
this._namespaces.set(name, theReadPromise);
return theReadPromise;
}
/** @internal */
unregisterNamespace(name) {
this._namespaces.delete(name);
}
}
exports.ITwinLocalization = ITwinLocalization;
class TranslationLogger {
static type = "logger";
log(args) { core_bentley_1.Logger.logInfo("i18n", this.createLogMessage(args)); }
warn(args) { core_bentley_1.Logger.logWarning("i18n", this.createLogMessage(args)); }
error(args) { core_bentley_1.Logger.logError("i18n", this.createLogMessage(args)); }
createLogMessage(args) {
let message = args[0];
for (let i = 1; i < args.length; ++i) {
if (typeof args[i] === "string")
message += `\n ${args[i]}`;
}
return message;
}
}
//# sourceMappingURL=ITwinLocalization.js.map