/** * Copyright schukai GmbH and contributors 2023. All Rights Reserved. * Node module: @schukai/monster * This file is licensed under the AGPLv3 License. * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html */ import {instanceSymbol} from "../constants.mjs"; import { Base } from "../types/base.mjs"; import { validateString } from "../types/validate.mjs"; import { clone } from "../util/clone.mjs"; export { Locale, parseLocale }; /** * @memberOf Monster.I18n * @type {symbol} */ const propertiesSymbol = Symbol("properties"); /** * @type {symbol} * @memberOf Monster.I18n */ const localeStringSymbol = Symbol("localeString"); /** * The Locale class is a base class for the language classes. * * RFC * * ``` * A Language-Tag consists of: * langtag ; generated tag * -or- private-use ; a private use tag * * langtag = (language * ["-" script] * ["-" region] * *("-" variant) * *("-" extension) * ["-" privateuse]) * * language = "en", "ale", or a registered value * * script = "Latn", "Cyrl", "Hant" ISO 15924 codes * * region = "US", "CS", "FR" ISO 3166 codes * "419", "019", or UN M.49 codes * * variant = "rozaj", "nedis", "1996", multiple subtags can be used in a tag * * extension = single letter followed by additional subtags; more than one extension * may be used in a language tag * * private-use = "x-" followed by additional subtags, as many as are required * Note that these can start a tag or appear at the end (but not * in the middle) * ``` * * @license AGPLv3 * @since 1.13.0 * @copyright schukai GmbH * @memberOf Monster.I18n * @see https://datatracker.ietf.org/doc/html/rfc3066 */ class Locale extends Base { /** * @param {string} language * @param {string} [region] * @param {string} [script] * @param {string} [variants] * @param {string} [extlang] * @param {string} [privateUse] * @throws {Error} unsupported locale */ constructor(language, region, script, variants, extlang, privateUse) { super(); this[propertiesSymbol] = { language: language === undefined ? undefined : validateString(language), script: script === undefined ? undefined : validateString(script), region: region === undefined ? undefined : validateString(region), variants: variants === undefined ? undefined : validateString(variants), extlang: extlang === undefined ? undefined : validateString(extlang), privateUse: privateUse === undefined ? undefined : validateString(privateUse), }; let s = []; if (language !== undefined) s.push(language); if (script !== undefined) s.push(script); if (region !== undefined) s.push(region); if (variants !== undefined) s.push(variants); if (extlang !== undefined) s.push(extlang); if (privateUse !== undefined) s.push(privateUse); if (s.length === 0) { throw new Error("unsupported locale"); } this[localeStringSymbol] = s.join("-"); } /** * This method is called by the `instanceof` operator. * @returns {symbol} * @since 3.27.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/i18n/locale@@instance"); } /** * @return {string} */ get localeString() { return this[localeStringSymbol]; } /** * @return {string|undefined} */ get language() { return this[propertiesSymbol].language; } /** * @return {string|undefined} */ get region() { return this[propertiesSymbol].region; } /** * @return {string|undefined} */ get script() { return this[propertiesSymbol].script; } /** * @return {string|undefined} */ get variants() { return this[propertiesSymbol].variants; } /** * @return {string|undefined} */ get extlang() { return this[propertiesSymbol].extlang; } /** * @return {string|undefined} */ get privateUse() { return this[propertiesSymbol].privateValue; } /** * @return {string} */ toString() { return `${this.localeString}`; } /** * The structure has the following: language, script, region, variants, extlang, privateUse * * @return {Monster.I18n.LocaleMap} */ getMap() { return clone(this[propertiesSymbol]); } } /** * @typedef {Object} LocaleMap * @property {string} language * @property {string} script * @property {string} region * @property {string} variants * @property {string} extlang * @property {string} privateUse * @memberOf Monster.I18n */ /** * Parse local according to rfc4646 standard * * Limitations: The regex cannot handle multiple variants or private. * * You can call the method via this function individually: * * ```javascript * import {createLocale} from '@schukai/monster/source/i18n/locale.mjs'; * createLocale() * ``` * * RFC * * ``` * The syntax of the language tag in ABNF [RFC4234] is: * * Language-Tag = langtag * / privateuse ; private use tag * / grandfathered ; grandfathered registrations * * langtag = (language * ["-" script] * ["-" region] * *("-" variant) * *("-" extension) * ["-" privateuse]) * * language = (2*3ALPHA [ extlang ]) ; shortest ISO 639 code * / 4ALPHA ; reserved for future use * / 5*8ALPHA ; registered language subtag * * extlang = *3("-" 3ALPHA) ; reserved for future use * * script = 4ALPHA ; ISO 15924 code * * region = 2ALPHA ; ISO 3166 code * / 3DIGIT ; UN M.49 code * * variant = 5*8alphanum ; registered variants * / (DIGIT 3alphanum) * * extension = singleton 1*("-" (2*8alphanum)) * * singleton = %x41-57 / %x59-5A / %x61-77 / %x79-7A / DIGIT * ; "a"-"w" / "y"-"z" / "A"-"W" / "Y"-"Z" / "0"-"9" * ; Single letters: x/X is reserved for private use * * privateuse = ("x"/"X") 1*("-" (1*8alphanum)) * * grandfathered = 1*3ALPHA 1*2("-" (2*8alphanum)) * ; grandfathered registration * ; Note: i is the only singleton * ; that starts a grandfathered tag * * alphanum = (ALPHA / DIGIT) ; letters and numbers * * Figure 1: Language Tag ABNF * ``` * * @param {string} locale * @returns {Locale} * @license AGPLv3 * @since 1.14.0 * @copyright schukai GmbH * @memberOf Monster.I18n * @throws {TypeError} value is not a string * @throws {Error} unsupported locale */ function parseLocale(locale) { locale = validateString(locale).replace(/_/g, "-"); let language; let region; let variants; let parts; let script; let extlang; let regexRegular = "(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)"; let regexIrregular = "(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)"; let regexGrandfathered = `(${regexIrregular}|${regexRegular})`; let regexPrivateUse = "(x(-[A-Za-z0-9]{1,8})+)"; let regexSingleton = "[0-9A-WY-Za-wy-z]"; let regexExtension = `(${regexSingleton}(-[A-Za-z0-9]{2,8})+)`; let regexVariant = "([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})"; let regexRegion = "([A-Za-z]{2}|[0-9]{3})"; let regexScript = "([A-Za-z]{4})"; let regexExtlang = "([A-Za-z]{3}(-[A-Za-z]{3}){0,2})"; let regexLanguage = `(([A-Za-z]{2,3}(-${regexExtlang})?)|[A-Za-z]{4}|[A-Za-z]{5,8})`; let regexLangtag = `(${regexLanguage}(-${regexScript})?(-${regexRegion})?(-${regexVariant})*(-${regexExtension})*(-${regexPrivateUse})?)`; let regexLanguageTag = `^(${regexGrandfathered}|${regexLangtag}|${regexPrivateUse})$`; let regex = new RegExp(regexLanguageTag); let match; if ((match = regex.exec(locale)) !== null) { if (match.index === regex.lastIndex) { regex.lastIndex++; } } if (match === undefined || match === null) { throw new Error("unsupported locale"); } if (match[6] !== undefined) { language = match[6]; parts = language.split("-"); if (parts.length > 1) { language = parts[0]; extlang = parts[1]; } } if (match[14] !== undefined) { region = match[14]; } if (match[12] !== undefined) { script = match[12]; } if (match[16] !== undefined) { variants = match[16]; } return new Locale(language, region, script, variants, extlang); }