gecko-dev/toolkit/components/mozintl/mozIntl.js

446 строки
14 KiB
JavaScript

/* 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/. */
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
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 a BCP47 language tag (ex: en-US, sr-Cyrl-RU etc.)
*/
const languageTagMatch = /^([a-z]{2,3}|[a-z]{4}|[a-z]{5,8})(?:[-_]([a-z]{4}))?(?:[-_]([A-Z]{2}|[0-9]{3}))?((?:[-_](?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)(?:[-_][a-wy-z0-9](?:[-_][a-z0-9]{2,8})+)*(?:[-_]x(?:[-_][a-z0-9]{1,8})+)?$/i;
/**
* This helper function retrives currently used app locales, allowing
* all mozIntl APIs to use the current regional prefs locales unless
* called with explicitly listed locales.
*/
function getLocales(locales) {
if (!locales) {
return Services.locale.regionalPrefsLocales;
}
return locales;
}
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});
}
/**
* 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.days > threshold.day: return "month";
// 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: 4, // at least 4 weeks before using month.
day: 6, // at least 6 days before using month.
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.
};
class MozIntl {
constructor() {
this._cache = {};
}
getCalendarInfo(locales, ...args) {
if (!this._cache.hasOwnProperty("getCalendarInfo")) {
mozIntlHelper.addGetCalendarInfo(this._cache);
}
return this._cache.getCalendarInfo(getLocales(locales), ...args);
}
getDisplayNames(locales, ...args) {
if (!this._cache.hasOwnProperty("getDisplayNames")) {
mozIntlHelper.addGetDisplayNames(this._cache);
}
return this._cache.getDisplayNames(getLocales(locales), ...args);
}
getLocaleInfo(locales, ...args) {
if (!this._cache.hasOwnProperty("getLocaleInfo")) {
mozIntlHelper.addGetLocaleInfo(this._cache);
}
return this._cache.getLocaleInfo(getLocales(locales), ...args);
}
getLanguageDisplayNames(locales, langCodes) {
if (locales !== undefined) {
throw new Error("First argument support not implemented yet");
}
const languageBundle = Services.strings.createBundle(
"chrome://global/locale/languageNames.properties");
return langCodes.map(langCode => {
if (typeof langCode !== "string") {
throw new TypeError("All language codes must be strings.");
}
try {
return languageBundle.GetStringFromName(langCode.toLowerCase());
} catch (e) {
return langCode.toLowerCase(); // Fall back to raw language subtag.
}
});
}
getRegions(locales) {
if (locales !== undefined) {
throw new Error("First argument support not implemented yet");
}
if (!this._cache.hasOwnProperty("regionBundle")) {
const regionBundle = Services.strings.createBundle(
"chrome://global/locale/regionNames.properties");
this._cache.regionBundle = regionBundle;
}
if (!this._cache.hasOwnProperty("regionMap")) {
this._cache.regionMap = new Map([...this._cache.regionBundle.getSimpleEnumeration()].map(prop => {
prop.QueryInterface(Ci.nsIPropertyElement);
return [prop.key.toUpperCase(), prop.value];
}));
}
return this._cache.regionMap;
}
getRegionDisplayNames(locales, regionCodes) {
if (locales !== undefined) {
throw new Error("First argument support not implemented yet");
}
if (!this._cache.hasOwnProperty("regionBundle")) {
const regionBundle = Services.strings.createBundle(
"chrome://global/locale/regionNames.properties");
this._cache.regionBundle = regionBundle;
}
return regionCodes.map(regionCode => {
if (typeof regionCode !== "string") {
throw new TypeError("All region codes must be strings.");
}
try {
return this._cache.regionBundle.GetStringFromName(regionCode.toLowerCase());
} catch (e) {
return regionCode.toUpperCase(); // Fall back to raw region subtag.
}
});
}
getLocaleDisplayNames(locales, localeCodes) {
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.");
}
// Get the display name for this dictionary.
// XXX: To be replaced with Intl.Locale once it lands - bug 1433303.
const match = localeCode.match(languageTagMatch);
if (match === null) {
return localeCode;
}
const [
/* languageTag */,
languageSubtag,
scriptSubtag,
regionSubtag,
variantSubtags,
] = match;
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.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);
});
}
get DateTimeFormat() {
if (!this._cache.hasOwnProperty("DateTimeFormat")) {
mozIntlHelper.addDateTimeFormatConstructor(this._cache);
}
let DateTimeFormat = this._cache.DateTimeFormat;
class MozDateTimeFormat extends DateTimeFormat {
constructor(locales, options, ...args) {
let resolvedLocales = DateTimeFormat.supportedLocalesOf(getLocales(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);
}
}
return MozDateTimeFormat;
}
get NumberFormat() {
class MozNumberFormat extends Intl.NumberFormat {
constructor(locales, options, ...args) {
super(getLocales(locales), options, ...args);
}
}
return MozNumberFormat;
}
get Collator() {
class MozCollator extends Intl.Collator {
constructor(locales, options, ...args) {
super(getLocales(locales), options, ...args);
}
}
return MozCollator;
}
get PluralRules() {
class MozPluralRules extends Intl.PluralRules {
constructor(locales, options, ...args) {
super(getLocales(locales), options, ...args);
}
}
return MozPluralRules;
}
get RelativeTimeFormat() {
if (!this._cache.hasOwnProperty("RelativeTimeFormat")) {
mozIntlHelper.addRelativeTimeFormatConstructor(this._cache);
}
const RelativeTimeFormat = this._cache.RelativeTimeFormat;
class MozRelativeTimeFormat extends 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(getLocales(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, "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 = {
_: {},
};
defineGetter(absDiff, "years", function() {
return Math.abs(diff.years);
});
defineGetter(absDiff, "months", function() {
return Math.abs(diff.months);
});
defineGetter(absDiff, "days", function() {
return Math.abs(diff.days);
});
defineGetter(absDiff, "hours", function() {
return Math.abs(diff.hours);
});
defineGetter(absDiff, "minutes", function() {
return Math.abs(diff.minutes);
});
defineGetter(absDiff, "seconds", function() {
return Math.abs(diff.seconds);
});
const unit = bestFit(absDiff);
switch (unit) {
case "year": return this.format(diff.years, unit);
case "month": return this.format(diff.months, 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);
}
}
}
return MozRelativeTimeFormat;
}
}
MozIntl.prototype.classID = Components.ID("{35ec195a-e8d0-4300-83af-c8a2cc84b4a3}");
MozIntl.prototype.QueryInterface = ChromeUtils.generateQI([Ci.mozIMozIntl]);
var components = [MozIntl];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);