UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

269 lines (236 loc) 7.49 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { instanceSymbol } from "../constants.mjs"; import { getLinkedObjects, hasObjectLink } from "../dom/attributes.mjs"; import { ATTRIBUTE_OBJECTLINK } from "../dom/constants.mjs"; import { getDocument } from "../dom/util.mjs"; import { Base } from "../types/base.mjs"; import { isObject, isString } from "../types/is.mjs"; import { validateInteger, validateObject, validateString, } from "../types/validate.mjs"; import { Locale, parseLocale } from "./locale.mjs"; import { translationsLinkSymbol } from "./provider.mjs"; import { getLocaleOfDocument } from "../dom/locale.mjs"; export { Translations, getDocumentTranslations }; /** * With this class you can manage translations and access the keys. * * @fragments /fragments/libraries/i18n/translations/ * * @externalExample ../../example/i18n/translations.mjs * @license AGPLv3 * @since 1.13.0 * @copyright Volker Schukai * @see https://datatracker.ietf.org/doc/html/rfc3066 */ class Translations extends Base { /** * * @param {Locale} locale */ constructor(locale) { super(); try { if (locale instanceof Locale) { this.locale = locale; } else if (isString(locale)) { this.locale = parseLocale(validateString(locale)); } else { this.locale = getLocaleOfDocument(); } } catch (e) { } finally { if (!(this.locale instanceof Locale)) { this.locale = new Locale("en"); } } this.storage = new Map(); } /** * This method is called by the `instanceof` operator. * @return {symbol} * @since 3.27.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/i18n/translations@@instance"); } /** * Fetches a text using the specified key. * If no suitable key is found, `defaultText` is taken. * * @param {string} key * @param {string|undefined} defaultText * @return {string} * @throws {Error} key not found */ getText(key, defaultText) { if (!this.storage.has(key)) { if (defaultText === undefined) { throw new Error(`key ${key} not found`); } return validateString(defaultText); } const r = this.storage.get(key); if (isObject(r)) { return this.getPluralRuleText(key, "other", defaultText); } return this.storage.get(key); } /** * A number `count` can be passed to this method. In addition to a number, one of the keywords can also be passed directly. * "zero", "one", "two", "few", "many" and "other". Remember: not every language has all rules. * * The appropriate text for this number is then selected. If no suitable key is found, `defaultText` is taken. * * @param {string} key * @param {integer|string} count * @param {string|undefined} defaultText * @return {string} */ getPluralRuleText(key, count, defaultText) { if (!this.storage.has(key)) { return validateString(defaultText); } const r = validateObject(this.storage.get(key)); let keyword; if (isString(count)) { keyword = count.toLocaleString(); } else { count = validateInteger(count); if (count === 0) { // special handling for zero count if (r.hasOwnProperty("zero")) { return validateString(r?.zero); } } keyword = new Intl.PluralRules(this.locale.toString()).select( validateInteger(count), ); } if (r.hasOwnProperty(keyword)) { return validateString(r[keyword]); } // @deprecated since 2023-03-14 // DEFAULT_KEY is undefined // if (r.hasOwnProperty(DEFAULT_KEY)) { // return validateString(r[DEFAULT_KEY]); // } return validateString(defaultText); } /** * Set a text for a key * * ``` * translations.setText("text1", "Make my day!"); * // plural rules * translations.setText("text6", { * "zero": "There are no files on Disk.", * "one": "There is one file on Disk.", * "other": "There are files on Disk." * "default": "There are files on Disk." * }); * ``` * * @param {string} key * @param {string|object} text * @return {Translations} * @throws {TypeError} value is not a string or object */ setText(key, text) { if (isString(text) || isObject(text)) { this.storage.set(validateString(key), text); return this; } throw new TypeError("value is not a string or object"); } /** * This method can be used to transfer overlays from an object. The keys are transferred, and the values are entered * as text. * * The values can either be character strings or, in the case of texts with plural forms, objects. The plural forms * must be stored as text via a standard key "zero", "one", "two", "few", "many" and "other". * * Additionally, the key default can be specified, which will be used if no other key fits. * * In some languages, like for example in German, there is no own more number at the value 0. In these languages, * the function applies additionally zero. * * ``` * translations.assignTranslations({ * "text1": "Make my day!", * "text2": "I'll be back!", * "text6": { * "zero": "There are no files on Disk.", * "one": "There is one file on Disk.", * "other": "There are files on Disk." * "default": "There are files on Disk." * }); * ``` * * @param {object} translations * @return {Translations} */ assignTranslations(translations) { validateObject(translations); if (translations instanceof Translations) { translations.storage.forEach((v, k) => { this.setText(k, v); }); return this; } for (const [k, v] of Object.entries(translations)) { this.setText(k, v); } return this; } } /** * Returns the translations for the current document. * * @param {HTMLElement|undefined} [element] - Element to search for translations. Default: element with objectlink @schukai/monster/i18n/translations@@link. * @return {Translations} * @throws {Error} Element is not an HTMLElement. * @throws {Error} Cannot find the element with translations. Add the translation object to the document. * @throws {Error} This element has no translations. * @throws {Error} Missing translations. */ function getDocumentTranslations(element) { const d = getDocument(); if (!(element instanceof HTMLElement)) { element = d.querySelector( `[${ATTRIBUTE_OBJECTLINK}~="${translationsLinkSymbol.toString()}"]`, ); if (element === null) { throw new Error( "Cannot find the element with translations. Add the translation object to the document.", ); } } if (!(element instanceof HTMLElement)) { throw new Error("Element is not an HTMLElement."); } if (!hasObjectLink(element, translationsLinkSymbol)) { throw new Error("This element has no translations."); } const obj = getLinkedObjects(element, translationsLinkSymbol); for (const t of obj) { if (t instanceof Translations) { return t; } } throw new Error("Missing translations."); }