Something went wrong on our end
Select Git revision
-
Volker Schukai authoredVolker Schukai authored
locale.mjs 9.08 KiB
/**
* 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);
}