Skip to content
Snippets Groups Projects
Select Git revision
  • 3035f4c1789b96fc2a385fcc3dc6e5cfe08855ba
  • master default protected
  • 1.31
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
  • 4.28.0
  • 4.27.0
  • 4.26.0
23 results

locale.mjs

Blame
  • 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);
    }