diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/mozintl/mozIntl.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/mozintl/mozIntl.sys.mjs')
-rw-r--r-- | toolkit/components/mozintl/mozIntl.sys.mjs | 1108 |
1 files changed, 1108 insertions, 0 deletions
diff --git a/toolkit/components/mozintl/mozIntl.sys.mjs b/toolkit/components/mozintl/mozIntl.sys.mjs new file mode 100644 index 0000000000..50633e365c --- /dev/null +++ b/toolkit/components/mozintl/mozIntl.sys.mjs @@ -0,0 +1,1108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const mozIntlHelper = Cc["@mozilla.org/mozintlhelper;1"].getService( + Ci.mozIMozIntlHelper +); +const osPrefs = Cc["@mozilla.org/intl/ospreferences;1"].getService( + Ci.mozIOSPreferences +); + +/** + * RegExp used to parse variant subtags from a BCP47 language tag. + * For example: ca-valencia + */ +const variantSubtagsMatch = /(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))+$/; + +function getDateTimePatternStyle(option) { + switch (option) { + case "full": + return osPrefs.dateTimeFormatStyleFull; + case "long": + return osPrefs.dateTimeFormatStyleLong; + case "medium": + return osPrefs.dateTimeFormatStyleMedium; + case "short": + return osPrefs.dateTimeFormatStyleShort; + default: + return osPrefs.dateTimeFormatStyleNone; + } +} + +/** + * Number of milliseconds in other time units. + * + * This is used by relative time format best unit + * calculations. + */ +const second = 1e3; +const minute = 6e4; +const hour = 36e5; +const day = 864e5; + +/** + * Use by RelativeTimeFormat. + * + * Allows for defining a cached getter to perform + * calculations only once. + * + * @param {Object} obj - Object to place the getter on. + * @param {String} prop - Name of the property. + * @param {Function} get - Function that will be used as a getter. + */ +function defineCachedGetter(obj, prop, get) { + defineGetter(obj, prop, function () { + if (!this._[prop]) { + this._[prop] = get.call(this); + } + return this._[prop]; + }); +} + +/** + * Used by RelativeTimeFormat. + * + * Defines a getter on an object + * + * @param {Object} obj - Object to place the getter on. + * @param {String} prop - Name of the property. + * @param {Function} get - Function that will be used as a getter. + */ +function defineGetter(obj, prop, get) { + Object.defineProperty(obj, prop, { get, enumerable: true }); +} + +/** + * Used by RelativeTimeFormat. + * + * Allows for calculation of the beginning of + * a period for discrete distances. + * + * @param {Date} date - Date of which we're looking to find a start of. + * @param {String} unit - Period to calculate the start of. + * + * @returns {Date} + */ +function startOf(date, unit) { + date = new Date(date.getTime()); + switch (unit) { + case "year": + date.setMonth(0); + // falls through + case "month": + date.setDate(1); + // falls through + case "day": + date.setHours(0); + // falls through + case "hour": + date.setMinutes(0); + // falls through + case "minute": + date.setSeconds(0); + // falls through + case "second": + date.setMilliseconds(0); + } + return date; +} + +/** + * Used by RelativeTimeFormat. + * + * Calculates the best fit unit to use for an absolute diff distance based + * on thresholds. + * + * @param {Object} absDiff - Object with absolute diff per unit calculated. + * + * @returns {String} + */ +function bestFit(absDiff) { + switch (true) { + case absDiff.years > 0 && absDiff.months > threshold.month: + return "year"; + case absDiff.months > 0 && absDiff.weeks > threshold.week: + return "month"; + case absDiff.weeks > 0 && absDiff.days > threshold.day: + return "week"; + case absDiff.days > 0 && absDiff.hours > threshold.hour: + return "day"; + case absDiff.hours > 0 && absDiff.minutes > threshold.minute: + return "hour"; + case absDiff.minutes > 0 && absDiff.seconds > threshold.second: + return "minute"; + default: + return "second"; + } +} + +/** + * Used by RelativeTimeFormat. + * + * Thresholds to use for calculating the best unit for relative time fromatting. + */ +const threshold = { + month: 2, // at least 2 months before using year. + week: 3, // at least 3 weeks before using month. + day: 6, // at least 6 days before using week. + hour: 6, // at least 6 hours before using day. + minute: 59, // at least 59 minutes before using hour. + second: 59, // at least 59 seconds before using minute. +}; + +/** + * Notice: If you're updating this list, you should also + * update the list in + * languageNames.ftl and regionNames.ftl. + */ +const availableLocaleDisplayNames = { + region: new Set([ + "ad", + "ae", + "af", + "ag", + "ai", + "al", + "am", + "ao", + "aq", + "ar", + "as", + "at", + "au", + "aw", + "az", + "ba", + "bb", + "bd", + "be", + "bf", + "bg", + "bh", + "bi", + "bj", + "bl", + "bm", + "bn", + "bo", + "bq", + "br", + "bs", + "bt", + "bv", + "bw", + "by", + "bz", + "ca", + "cc", + "cd", + "cf", + "cg", + "ch", + "ci", + "ck", + "cl", + "cm", + "cn", + "co", + "cp", + "cr", + "cu", + "cv", + "cw", + "cx", + "cy", + "cz", + "de", + "dg", + "dj", + "dk", + "dm", + "do", + "dz", + "ec", + "ee", + "eg", + "eh", + "er", + "es", + "et", + "fi", + "fj", + "fk", + "fm", + "fo", + "fr", + "ga", + "gb", + "gd", + "ge", + "gf", + "gg", + "gh", + "gi", + "gl", + "gm", + "gn", + "gp", + "gq", + "gr", + "gs", + "gt", + "gu", + "gw", + "gy", + "hk", + "hm", + "hn", + "hr", + "ht", + "hu", + "id", + "ie", + "il", + "im", + "in", + "io", + "iq", + "ir", + "is", + "it", + "je", + "jm", + "jo", + "jp", + "ke", + "kg", + "kh", + "ki", + "km", + "kn", + "kp", + "kr", + "kw", + "ky", + "kz", + "la", + "lb", + "lc", + "li", + "lk", + "lr", + "ls", + "lt", + "lu", + "lv", + "ly", + "ma", + "mc", + "md", + "me", + "mf", + "mg", + "mh", + "mk", + "ml", + "mm", + "mn", + "mo", + "mp", + "mq", + "mr", + "ms", + "mt", + "mu", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nc", + "ne", + "nf", + "ng", + "ni", + "nl", + "no", + "np", + "nr", + "nu", + "nz", + "om", + "pa", + "pe", + "pf", + "pg", + "ph", + "pk", + "pl", + "pm", + "pn", + "pr", + "pt", + "pw", + "py", + "qa", + "qm", + "qs", + "qu", + "qw", + "qx", + "qz", + "re", + "ro", + "rs", + "ru", + "rw", + "sa", + "sb", + "sc", + "sd", + "se", + "sg", + "sh", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "sr", + "ss", + "st", + "sv", + "sx", + "sy", + "sz", + "tc", + "td", + "tf", + "tg", + "th", + "tj", + "tk", + "tl", + "tm", + "tn", + "to", + "tr", + "tt", + "tv", + "tw", + "tz", + "ua", + "ug", + "us", + "uy", + "uz", + "va", + "vc", + "ve", + "vg", + "vi", + "vn", + "vu", + "wf", + "ws", + "xa", + "xb", + "xc", + "xd", + "xe", + "xg", + "xh", + "xj", + "xk", + "xl", + "xm", + "xp", + "xq", + "xr", + "xs", + "xt", + "xu", + "xv", + "xw", + "ye", + "yt", + "za", + "zm", + "zw", + ]), + language: new Set([ + "aa", + "ab", + "ach", + "ae", + "af", + "ak", + "am", + "an", + "ar", + "as", + "ast", + "av", + "ay", + "az", + "ba", + "be", + "bg", + "bh", + "bi", + "bm", + "bn", + "bo", + "br", + "bs", + "ca", + "cak", + "ce", + "ch", + "co", + "cr", + "crh", + "cs", + "csb", + "cu", + "cv", + "cy", + "da", + "de", + "dsb", + "dv", + "dz", + "ee", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "ff", + "fi", + "fj", + "fo", + "fr", + "fur", + "fy", + "ga", + "gd", + "gl", + "gn", + "gu", + "gv", + "ha", + "haw", + "he", + "hi", + "hil", + "ho", + "hr", + "hsb", + "ht", + "hu", + "hy", + "hz", + "ia", + "id", + "ie", + "ig", + "ii", + "ik", + "io", + "is", + "it", + "iu", + "ja", + "jv", + "ka", + "kab", + "kg", + "ki", + "kj", + "kk", + "kl", + "km", + "kn", + "ko", + "kok", + "kr", + "ks", + "ku", + "kv", + "kw", + "ky", + "la", + "lb", + "lg", + "li", + "lij", + "ln", + "lo", + "lt", + "ltg", + "lu", + "lv", + "mai", + "meh", + "mg", + "mh", + "mi", + "mix", + "mk", + "ml", + "mn", + "mr", + "ms", + "mt", + "my", + "na", + "nb", + "nd", + "ne", + "ng", + "nl", + "nn", + "no", + "nr", + "nso", + "nv", + "ny", + "oc", + "oj", + "om", + "or", + "os", + "pa", + "pi", + "pl", + "ps", + "pt", + "qu", + "rm", + "rn", + "ro", + "ru", + "rw", + "sa", + "sat", + "sc", + "sco", + "sd", + "se", + "sg", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "son", + "sq", + "sr", + "ss", + "st", + "su", + "sv", + "sw", + "szl", + "ta", + "te", + "tg", + "th", + "ti", + "tig", + "tk", + "tl", + "tlh", + "tn", + "to", + "tr", + "trs", + "ts", + "tt", + "tw", + "ty", + "ug", + "uk", + "ur", + "uz", + "ve", + "vi", + "vo", + "wa", + "wen", + "wo", + "xh", + "yi", + "yo", + "za", + "zam", + "zh", + "zu", + ]), +}; + +/** + * Notice: If you're updating these names, you should also update the data + * in python/mozbuild/mozbuild/action/langpack_localeNames.json. + */ +const nativeLocaleNames = new Map( + Object.entries({ + ach: "Acholi", + af: "Afrikaans", + an: "Aragonés", + ar: "العربية", + ast: "Asturianu", + az: "Azərbaycanca", + be: "Беларуская", + bg: "Български", + bn: "বাংলা", + bo: "བོད་སྐད", + br: "Brezhoneg", + brx: "बड़ो", + bs: "Bosanski", + ca: "Català", + "ca-valencia": "Català (Valencià)", + cak: "Kaqchikel", + cs: "Čeština", + cy: "Cymraeg", + da: "Dansk", + de: "Deutsch", + dsb: "Dolnoserbšćina", + el: "Ελληνικά", + "en-CA": "English (CA)", + "en-GB": "English (GB)", + "en-US": "English (US)", + eo: "Esperanto", + "es-AR": "Español (AR)", + "es-CL": "Español (CL)", + "es-ES": "Español (ES)", + "es-MX": "Español (MX)", + et: "Eesti", + eu: "Euskara", + fa: "فارسی", + ff: "Pulaar", + fi: "Suomi", + fr: "Français", + fur: "Furlan", + "fy-NL": "Frysk", + "ga-IE": "Gaeilge", + gd: "Gàidhlig", + gl: "Galego", + gn: "Guarani", + "gu-IN": "ગુજરાતી", + he: "עברית", + "hi-IN": "हिन्दी", + hr: "Hrvatski", + hsb: "Hornjoserbšćina", + hu: "Magyar", + "hy-AM": "հայերեն", + ia: "Interlingua", + id: "Indonesia", + is: "Islenska", + it: "Italiano", + ja: "日本語", + "ja-JP-mac": "日本語", + ka: "ქართული", + kab: "Taqbaylit", + kk: "қазақ тілі", + km: "ខ្មែរ", + kn: "ಕನ್ನಡ", + ko: "한국어", + lij: "Ligure", + lo: "ລາວ", + lt: "Lietuvių", + ltg: "Latgalīšu", + lv: "Latviešu", + mk: "македонски", + ml: "മലയാളം", + mr: "मराठी", + ms: "Melayu", + my: "မြန်မာ", + "nb-NO": "Norsk Bokmål", + "ne-NP": "नेपाली", + nl: "Nederlands", + "nn-NO": "Nynorsk", + oc: "Occitan", + or: "ଓଡ଼ିଆ", + "pa-IN": "ਪੰਜਾਬੀ", + pl: "Polski", + "pt-BR": "Português (BR)", + "pt-PT": "Português (PT)", + rm: "Rumantsch", + ro: "Română", + ru: "Русский", + sat: "ᱥᱟᱱᱛᱟᱲᱤ", + sc: "Sardu", + sco: "Scots", + si: "සිංහල", + sk: "Slovenčina", + sl: "Slovenščina", + son: "Soŋay", + sq: "Shqip", + sr: "Cрпски", + "sv-SE": "Svenska", + szl: "Ślōnsko", + ta: "தமிழ்", + te: "తెలుగు", + tg: "Тоҷикӣ", + th: "ไทย", + tl: "Tagalog", + tr: "Türkçe", + trs: "Triqui", + uk: "Українська", + ur: "اردو", + uz: "O‘zbek", + vi: "Tiếng Việt", + wo: "Wolof", + xh: "IsiXhosa", + "zh-CN": "简体中文", + "zh-TW": "正體中文", + }) +); + +class MozRelativeTimeFormat extends Intl.RelativeTimeFormat { + constructor(locales, options = {}, ...args) { + // If someone is asking for MozRelativeTimeFormat, it's likely they'll want + // to use `formatBestUnit` which works better with `auto` + if (options.numeric === undefined) { + options.numeric = "auto"; + } + super(locales, options, ...args); + } + + formatBestUnit(date, { now = new Date() } = {}) { + const diff = { + _: {}, + ms: date.getTime() - now.getTime(), + years: date.getFullYear() - now.getFullYear(), + }; + + defineCachedGetter(diff, "months", function () { + return this.years * 12 + date.getMonth() - now.getMonth(); + }); + defineCachedGetter(diff, "weeks", function () { + return Math.trunc(this.days / 7); + }); + defineCachedGetter(diff, "days", function () { + return Math.trunc((startOf(date, "day") - startOf(now, "day")) / day); + }); + defineCachedGetter(diff, "hours", function () { + return Math.trunc((startOf(date, "hour") - startOf(now, "hour")) / hour); + }); + defineCachedGetter(diff, "minutes", function () { + return Math.trunc( + (startOf(date, "minute") - startOf(now, "minute")) / minute + ); + }); + defineCachedGetter(diff, "seconds", function () { + return Math.trunc( + (startOf(date, "second") - startOf(now, "second")) / second + ); + }); + + const absDiff = { + _: {}, + }; + + for (let prop of Object.keys(diff)) { + defineGetter(absDiff, prop, function () { + return Math.abs(diff[prop]); + }); + } + const unit = bestFit(absDiff); + + switch (unit) { + case "year": + return this.format(diff.years, unit); + case "month": + return this.format(diff.months, unit); + case "week": + return this.format(diff.weeks, unit); + case "day": + return this.format(diff.days, unit); + case "hour": + return this.format(diff.hours, unit); + case "minute": + return this.format(diff.minutes, unit); + default: + if (unit !== "second") { + throw new TypeError(`Unsupported unit "${unit}"`); + } + return this.format(diff.seconds, unit); + } + } +} + +export class MozIntl { + Collator = Intl.Collator; + ListFormat = Intl.ListFormat; + Locale = Intl.Locale; + NumberFormat = Intl.NumberFormat; + PluralRules = Intl.PluralRules; + RelativeTimeFormat = MozRelativeTimeFormat; + + constructor() { + this._cache = {}; + Services.obs.addObserver(this, "intl:app-locales-changed", true); + } + + observe() { + // Clear cache when things change. + this._cache = {}; + } + + getCalendarInfo(locales, ...args) { + if (!this._cache.hasOwnProperty("getCalendarInfo")) { + mozIntlHelper.addGetCalendarInfo(this._cache); + } + + return this._cache.getCalendarInfo(locales, ...args); + } + + getDisplayNamesDeprecated(locales, options = {}) { + // Helper for IntlUtils.webidl, will be removed once Intl.DisplayNames is + // available in non-privileged code. + + let { type, style, calendar, keys = [] } = options; + + let dn = new this.DisplayNames(locales, { type, style, calendar }); + let { + locale: resolvedLocale, + type: resolvedType, + style: resolvedStyle, + calendar: resolvedCalendar, + } = dn.resolvedOptions(); + let values = keys.map(key => dn.of(key)); + + return { + locale: resolvedLocale, + type: resolvedType, + style: resolvedStyle, + calendar: resolvedCalendar, + values, + }; + } + + getAvailableLocaleDisplayNames(type) { + if (availableLocaleDisplayNames.hasOwnProperty(type)) { + return Array.from(availableLocaleDisplayNames[type]); + } + + return new Error("Unimplemented!"); + } + + getLanguageDisplayNames(locales, langCodes) { + if (locales !== undefined) { + throw new Error("First argument support not implemented yet"); + } + + if (!this._cache.hasOwnProperty("languageLocalization")) { + const loc = new Localization(["toolkit/intl/languageNames.ftl"], true); + this._cache.languageLocalization = loc; + } + + const loc = this._cache.languageLocalization; + + return langCodes.map(langCode => { + if (typeof langCode !== "string") { + throw new TypeError("All language codes must be strings."); + } + let lcLangCode = langCode.toLowerCase(); + if (availableLocaleDisplayNames.language.has(lcLangCode)) { + const value = loc.formatValueSync(`language-name-${lcLangCode}`); + if (value !== null) { + return value; + } + } + return lcLangCode; + }); + } + + getRegionDisplayNames(locales, regionCodes) { + if (locales !== undefined) { + throw new Error("First argument support not implemented yet"); + } + + if (!this._cache.hasOwnProperty("regionLocalization")) { + const loc = new Localization(["toolkit/intl/regionNames.ftl"], true); + this._cache.regionLocalization = loc; + } + + const loc = this._cache.regionLocalization; + + return regionCodes.map(regionCode => { + if (typeof regionCode !== "string") { + throw new TypeError("All region codes must be strings."); + } + let lcRegionCode = regionCode.toLowerCase(); + if (availableLocaleDisplayNames.region.has(lcRegionCode)) { + let regionID; + // Allow changing names over time for specific regions + switch (lcRegionCode) { + case "bq": + regionID = "region-name-bq-2018"; + break; + case "cv": + regionID = "region-name-cv-2020"; + break; + case "cz": + regionID = "region-name-cz-2019"; + break; + case "mk": + regionID = "region-name-mk-2019"; + break; + case "sz": + regionID = "region-name-sz-2019"; + break; + default: + regionID = `region-name-${lcRegionCode}`; + } + + const value = loc.formatValueSync(regionID); + if (value !== null) { + return value; + } + } + return regionCode.toUpperCase(); + }); + } + + getLocaleDisplayNames(locales, localeCodes, options = {}) { + const { preferNative = false } = options; + + if (locales !== undefined) { + throw new Error("First argument support not implemented yet"); + } + // Patterns hardcoded from CLDR 33 english. + // We can later look into fetching them from CLDR directly. + const localePattern = "{0} ({1})"; + const localeSeparator = ", "; + + return localeCodes.map(localeCode => { + if (typeof localeCode !== "string") { + throw new TypeError("All locale codes must be strings."); + } + + if (preferNative && nativeLocaleNames.has(localeCode)) { + return nativeLocaleNames.get(localeCode); + } + + let locale; + try { + locale = new Intl.Locale(localeCode.replaceAll("_", "-")); + } catch { + return localeCode; + } + + const { + language: languageSubtag, + script: scriptSubtag, + region: regionSubtag, + } = locale; + + const variantSubtags = locale.baseName.match(variantSubtagsMatch); + + const displayName = [ + this.getLanguageDisplayNames(locales, [languageSubtag])[0], + ]; + + if (scriptSubtag) { + displayName.push(scriptSubtag); + } + + if (regionSubtag) { + displayName.push( + this.getRegionDisplayNames(locales, [regionSubtag])[0] + ); + } + + if (variantSubtags) { + displayName.push(...variantSubtags[0].substr(1).split("-")); // Collapse multiple variants. + } + + let modifiers; + if (displayName.length === 1) { + return displayName[0]; + } else if (displayName.length > 2) { + modifiers = displayName.slice(1).join(localeSeparator); + } else { + modifiers = displayName[1]; + } + return localePattern + .replace("{0}", displayName[0]) + .replace("{1}", modifiers); + }); + } + + getScriptDirection(locale) { + // This is a crude implementation until Bug 1693576 lands. + // See justification in toolkit/components/mozintl/mozIMozIntl.idl + const { language } = new Intl.Locale(locale); + if ( + language == "ar" || + language == "ckb" || + language == "fa" || + language == "he" || + language == "ur" + ) { + return "rtl"; + } + return "ltr"; + } + + stringHasRTLChars(str) { + return mozIntlHelper.stringHasRTLChars(str); + } + + get DateTimeFormat() { + if (!this._cache.hasOwnProperty("DateTimeFormat")) { + mozIntlHelper.addDateTimeFormatConstructor(this._cache); + + const DateTimeFormat = this._cache.DateTimeFormat; + + class MozDateTimeFormat extends DateTimeFormat { + constructor(locales, options, ...args) { + let resolvedLocales = DateTimeFormat.supportedLocalesOf(locales); + if (options) { + if (options.dateStyle || options.timeStyle) { + options.pattern = osPrefs.getDateTimePattern( + getDateTimePatternStyle(options.dateStyle), + getDateTimePatternStyle(options.timeStyle), + resolvedLocales[0] + ); + } else { + // make sure that user doesn't pass a pattern explicitly + options.pattern = undefined; + } + } + super(resolvedLocales, options, ...args); + } + } + this._cache.MozDateTimeFormat = MozDateTimeFormat; + } + + return this._cache.MozDateTimeFormat; + } + + get DisplayNames() { + if (!this._cache.hasOwnProperty("DisplayNames")) { + mozIntlHelper.addDisplayNamesConstructor(this._cache); + } + + return this._cache.DisplayNames; + } +} + +MozIntl.prototype.classID = Components.ID( + "{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}" +); +MozIntl.prototype.QueryInterface = ChromeUtils.generateQI([ + "mozIMozIntl", + "nsIObserver", + "nsISupportsWeakReference", +]); |