UNPKG

@itwin/core-i18n

Version:
214 lines • 9.94 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * 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