Source code

Revision control

Copy as Markdown

Other Tools

/* 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: 11, // at least 11 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",
]);