@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
269 lines (236 loc) • 7.49 kB
JavaScript
/**
* 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.");
}