VulpesOS/gaia/shared/js/moz_intl.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

582 lines
17 KiB
JavaScript
Executable file

(function(global) {
'use strict';
global.mozIntl = {
/**
* Format an Array of strings using locale specific separators.
* Currently it only uses middle separator resulting in a local
* equivalent of 'X, Y, Z'.
*
* In the Intl API it will support start/end separators allowing for
* things like 'X, Y and Z'.
*
* @param {Array} An array of strings to be formatted
* @returns {Promise} A promise of a string
*/
formatList: function(list) {
return navigator.mozL10n.formatValue('listSeparator_middle').then(
sep => list.join(sep)
);
},
/**
* Overload of Intl.DateTimeFormat that extends with future features.
*
* 1) dayperiod token
*
* `dayperiod` token allows to decide whether the formatted time should
* contain dayperiod token. The value may be true or false.
*
* Example:
*
* mozIntl.DateTimeFormat(navigator.languages, {
* dayperiod: true,
* hour12: navigator.mozHour12,
* hour: 'numeric',
* minute: 'numeric'
* });
*
* 2) Token formatting
*
* This feature allows for formatting of tokens in the result string.
*
* Example:
*
* var f = mozIntl.DateTimeFormat(navigator.languages, {
* weekday: 'long',
* day: 'numeric',
* month: 'long'
* });
*
* f.format(date, {
* day: '<strong>$&</strong>',
* });
*
* Warning: Current implementation is very fragile. Be very careful when
* choosing tokens to be formatted. The formatter will replace the
* first occurrence of the returned string, so if you try to format
* dayperiod ('am') in a string that also contains weekday that has this
* string, you may end up with bad formatting. Example:
*
* French for Saturday is "samedi". So formatting weekday in the same string
* as dayperiod will result in "s<strong>am</strong>edi, 8:53 am".
*
* It's best to make sure that your formatted token is the only number or
* the only string. Examples:
*
* * dayperiod in Hour:Minute am/pm
* * day in "Saturday, July 23"
*
*/
DateTimeFormat: function(locales, options) {
const resolvedOptions = Object.assign({}, options);
if (resolvedOptions.dayperiod) {
if (resolvedOptions.hour === undefined) {
resolvedOptions.hour = 'numeric';
}
}
if (resolvedOptions.hour !== undefined &&
resolvedOptions.hour12 === undefined) {
resolvedOptions.hour12 = navigator.mozHour12;
}
if (resolvedOptions.dayperiod === undefined &&
resolvedOptions.hour12 === true) {
resolvedOptions.dayperiod = true;
}
var intlFormat = new Intl.DateTimeFormat(locales, resolvedOptions);
resolvedOptions.locale = intlFormat.resolvedOptions().locale;
resolvedOptions.hour12 = intlFormat.resolvedOptions().hour12;
// This is needed for a workaround for bug 1208808
// Remove when that bug is fixed
var hourFormatter;
if (resolvedOptions.dayperiod !== undefined &&
resolvedOptions.hour12 === true) {
hourFormatter = Intl.DateTimeFormat(locales, {
hour: 'numeric',
hour12: false
});
}
return {
resolvedOptions() { return resolvedOptions; },
format: function(date, tokenFormats) {
var dayPeriod;
var string = intlFormat.format(date);
if (resolvedOptions.dayperiod === false &&
resolvedOptions.hour12 === true) {
dayPeriod = getDayPeriodTokenForDate(date, hourFormatter);
string = string.replace(dayPeriod, '').trim();
} else if (resolvedOptions.dayperiod === true &&
options.hour === undefined) {
dayPeriod = getDayPeriodTokenForDate(date, hourFormatter);
const hour = date.toLocaleString(navigator.languages, {
hour12: true,
hour: 'numeric'
}).replace(dayPeriod, '').trim();
string = string.replace(hour, '').trim();
}
for (var token in tokenFormats) {
if (token === 'dayperiod' &&
resolvedOptions.hour12 === false) {
continue;
}
const localOptions = {
[token]: resolvedOptions[token],
};
var formatter = global.mozIntl.DateTimeFormat(
navigator.languages, localOptions);
var tokenString = formatter.format(date);
string = string.replace(tokenString, tokenFormats[token]);
}
return string;
},
};
},
/**
* Return locale specific infromation about calendar system.
*
* Currently supports:
* * firstDayOfTheWeek: 0 - Sunday, 1 - Monday, etc.
*
* @param {String} Identifier of a token to be retrieved
* @returns {Promise} A promise of value corresponding to a token
*/
calendarInfo: function(token) {
switch (token) {
case 'firstDayOfTheWeek':
return navigator.mozL10n.formatValue('firstDayOfTheWeek').then(
firstDayOfTheWeek => parseInt(firstDayOfTheWeek) % 7);
default:
throw new Error('Unknown token: ' + token);
}
},
/**
* Duration formatter.
* Formats an integer with milliseconds into locale specific duration string.
*
* The shim differs from Intl API formatters in that it returns a Promise
* because it relies on L20n so it has to be asynchronous.
* Intl API will probably be synchronous.
*
* Currently accepted options:
* - maxUnit
* - minUnit
* both can take values hour | minute | second | millisecond
*
* Examples:
*
* mozIntl.DurationFormat(navigator.languages, {
* minUnit: 'second',
* maxUnit: 'hour'
* }).then(formatter =>
* formatter.format(milliseconds); // 02:12:34 in en-US
* );
*
* mozIntl.DurationFormat(navigator.languages, {
* minUnit: 'millisecond',
* maxUnit: 'minute'
* }).then(formatter =>
* formatter.format(milliseconds); // 12:34.80 in en-US
* );
*
* @param {Array} An array of languages
* @param {Array} Options object with `minUnit` and `maxUnit`
* @returns {Promise} A promise of a formatter
*/
DurationFormat: function(locales = navigator.languages, options = {}) {
const resolvedOptions = Object.assign({
locale: locales[0],
maxUnit: 'hour',
minUnit: 'second',
}, options);
const numFormatter = Intl.NumberFormat(locales, {
style: 'decimal',
useGrouping: false,
minimumIntegerDigits: 2
});
const maxUnitIdx = getDurationUnitIdx(resolvedOptions.maxUnit, 0);
const minUnitIdx = getDurationUnitIdx(resolvedOptions.minUnit,
durationFormatOrder.length - 1);
return navigator.mozL10n.formatValue('durationPattern').then(fmt => ({
resolvedOptions: function() { return resolvedOptions; },
format: function(input) {
// Rounding minUnit to closest visible unit
const minValue = durationFormatElements[resolvedOptions.minUnit].value;
input = Math.round(input / minValue) * minValue;
const duration = splitIntoTimeUnits(input, maxUnitIdx, minUnitIdx);
var string = trimDurationPattern(fmt,
resolvedOptions.maxUnit, resolvedOptions.minUnit);
for (var unit in duration) {
const token = durationFormatElements[unit].token;
string = string.replace(token,
numFormatter.format(duration[unit]));
}
if (input < 0) {
return '-' + string;
}
return string;
}
}));
},
/**
* RelativeTime formatter.
* Formats an integer with milliseconds into locale specific relative time
* string.
*
* Currently accepted options:
*
* * style - long | short
*
* Defines whether the string will be long "1 minute ago" or short "1 min.
* ago"
*
* * minUnit - (default) millisecond
* * maxUnit - (default) year
* * unit - second | minute | hour | day | week | month | year
* (default) bestFit
*
* Example:
*
* const formatter = new Intl.RelativeTimeFormat(navigator.languages, {
* unit: 'bestFit',
* style: long'
* });
*
* var ms = Date.now() - 2 * 1000;
*
* formatter.format(ms); // "2 seconds ago"
*
* @param {Array} An array of languages
* @param {Array} Options object
* @returns An object with a formatter that returns Promises of strings
*/
RelativeTimeFormat: function(locales, options) {
return {
resolvedOptions: function() { return options; },
/*
* ECMA 402 rev 3., 1.3.4, FormatRelativeTime
*
* Notes: This is a modified version of the function to use mozL10n
* and simplified to match current data set in data.properties
*/
format: function(x) {
const {unit, value} = relativeTimeFormatId(x, options);
return navigator.mozL10n.formatValue(unit, {
value
});
},
};
},
UnitFormat: function(locales, options) {
const unitGroup = getUnitFormatGroupName(options.unit);
if (unitGroup === undefined) {
throw new RangeError(`invalid value ${options.unit} for option unit`);
}
if (!unitFormatGroups[unitGroup].styles.includes(options.style)) {
throw new RangeError(`invalid value ${options.style} for option style`);
}
const unit = `${unitGroup}-${options.unit}-${options.style}`;
return {
format: function(x) {
return document.l10n.formatValue(unit, {
value: x
});
},
};
},
_gaia: {
// This is an internal Firefox OS function, not part of the future standard
relativePart: function(milliseconds) {
const units = computeTimeUnits(milliseconds);
const unit = getBestMatchUnit(units);
return {
unit: unit + 's',
value: Math.abs(units[unit])
};
},
// This is an internal Firefox OS function, not part of the future standard
RelativeDate: function(locales, options) {
const style = options && options.style || 'long';
const maxFormatter = Intl.DateTimeFormat(locales, {
year: 'numeric',
month: 'numeric',
day: 'numeric'
});
const relativeFmtOptions = {
unit: 'bestFit',
style: style,
minUnit: 'minute',
};
return {
format: function(time, maxDiff) {
maxDiff = maxDiff || 86400 * 10; // default = 10 days
const secDiff = (Date.now() - time) / 1000;
if (isNaN(secDiff)) {
return navigator.mozL10n.formatValue('incorrectDate');
}
if (secDiff > maxDiff) {
return Promise.resolve(maxFormatter.format(time));
}
const {unit, value} = relativeTimeFormatId(time, relativeFmtOptions);
return navigator.mozL10n.formatValue(unit, {
value
});
},
formatElement: function(element, time, maxDiff) {
maxDiff = maxDiff || 86400 * 10; // default = 10 days
const secDiff = (Date.now() - time) / 1000;
if (isNaN(secDiff)) {
element.setAttribute('data-l10n-id', 'incorrectDate');
}
element.removeAttribute('data-l10n-id');
if (secDiff > maxDiff) {
element.textContent = maxFormatter.format(time);
}
const {unit, value} = relativeTimeFormatId(time, relativeFmtOptions);
navigator.mozL10n.setAttributes(element, unit, {
value
});
},
};
},
getFormattedUnit: function(type, style, v) {
if (!unitFormatData.hasOwnProperty(type)) {
throw new RangeError(`invalid type ${type}`);
}
if (!unitFormatGroups[type].styles.includes(style)) {
throw new RangeError(`invalid style ${style} for type ${type}`);
}
var units = unitFormatData[type];
var scale = 0;
for (let i = 1; i < units.length; i++) {
if (v < units[i].value * unitFormatGroups[type].rounding) {
scale = i - 1;
break;
} else if (i === units.length - 1) {
scale = i;
}
}
var value = Math.round(v / units[scale].value * 100) / 100;
return global.mozIntl.UnitFormat(navigator.languages, {
unit: units[scale].name,
style: style
}).format(value);
},
}
};
/*
* This data is used by DurationFormat
*/
/*jshint unused:false*/
const durationFormatOrder = ['hour', 'minute', 'second', 'millisecond'];
const durationFormatElements = {
'hour': {value: 3600000, token: 'hh'},
'minute': {value: 60000, token: 'mm'},
'second': {value: 1000, token: 'ss'},
// rounding milliseconds to tens
'millisecond': {value: 10, token: 'SS'}
};
const unitFormatData = {
'duration': [
{'name': 'second', 'value': 1},
{'name': 'minute', 'value': 60},
{'name': 'hour', 'value': 60 * 60},
{'name': 'day', 'value': 24 * 60 * 60},
{'name': 'month', 'value': 30 * 24 * 60 * 60},
],
'digital': [
{'name': 'byte', 'value': 1},
{'name': 'kilobyte', 'value': 1024},
{'name': 'megabyte', 'value': 1024 * 1024},
{'name': 'gigabyte', 'value': 1024 * 1024 * 1024},
{'name': 'terabyte', 'value': 1024 * 1024 * 1024 * 1024},
],
};
const unitFormatGroups = {
'duration': {
'units': ['second', 'minute', 'hour', 'day', 'month'],
'styles': ['narrow'],
'rounding': 1
},
'digital': {
'units': ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'],
'styles': ['short'],
'rounding': 0.8
}
};
/*
* This helper function is used by splitIntoTimeUnits
*/
function getDurationUnitIdx(name, defaultValue) {
if (!name) {
return defaultValue;
}
const pos = durationFormatOrder.indexOf(name);
if (pos === -1) {
throw new Error('Unknown unit type: ' + name);
}
return pos;
}
/*
* This helper function is used by DurationFormat
*/
function splitIntoTimeUnits(v, maxUnitIdx, minUnitIdx) {
const units = {};
var input = Math.abs(v);
for (var i = maxUnitIdx; i <= minUnitIdx; i++) {
const key = durationFormatOrder[i];
const {value} = durationFormatElements[key];
units[key] = i == minUnitIdx ?
Math.round(input / value) :
Math.floor(input / value);
input -= units[key] * value;
}
return units;
}
function trimDurationPattern(string, maxUnit, minUnit) {
const maxToken = durationFormatElements[maxUnit].token;
const minToken = durationFormatElements[minUnit].token;
// We currently know of no format that would require reverse order
// Even RTL languages use LTR duration formatting, so all we care
// are separators.
string = string.substring(
string.indexOf(maxToken),
string.indexOf(minToken) + minToken.length);
return string;
}
/**
* This helper function is used by mozIntl.DateTimeFormat
*
* This is necessary because sometimes toLocaleFormat
* uses different timezone than Intl API
* which leads to it resolving %p to 'PM' while Intl is in 'AM'
*
* So what we do here, is we force the same hour in toLocaleFormat API
* as we use in Intl API, to enforce the same dayperiod to remove it.
* Remove once bug 1208808 is fixed
*/
function getDayPeriodTokenForDate(date, hourFormatter) {
const hourToken = hourFormatter.format(date);
const newDate = new Date(date);
newDate.setHours(parseInt(hourToken));
return new Intl.DateTimeFormat(navigator.language, {hour:'numeric', hour12:true}).formatToParts(newDate).find(part => part.type === 'dayPeriod').value;
}
/*
* ECMA 402 rev 3., 1.3.4, ComputeTimeUnits
*/
function computeTimeUnits(v) {
const units = {};
const millisecond = Math.round(v);
const second = Math.round(millisecond / 1000);
const minute = Math.round(second / 60);
const hour = Math.round(minute / 60);
const day = Math.round(hour / 24);
const rawYear = day * 400 / 146097;
units.millisecond = millisecond;
units.second = second;
units.minute = minute;
units.hour = hour;
units.day = day;
units.week = Math.round(day / 7);
units.month = Math.round(rawYear * 12);
units.quarter = Math.round(rawYear * 4);
units.year = Math.round(rawYear);
return units;
}
/*
* ECMA 402 rev 3., 1.3.4, GetBestMatchUnit
*/
function getBestMatchUnit(units) {
//if (Math.abs(units.second) < 45) { return 'second'; }
if (Math.abs(units.minute) < 45) { return 'minute'; }
if (Math.abs(units.hour) < 22) { return 'hour'; }
// Intl uses 26 days here
if (Math.abs(units.day) < 7) { return 'day'; }
if (Math.abs(units.week) < 4) { return 'week'; }
if (Math.abs(units.month) < 11) { return 'month'; }
//if (Math.abs(units.quarter) < 4) { return 'quarter'; }
return 'year';
}
function relativeTimeFormatId(x, options) {
const ms = x - Date.now();
const units = computeTimeUnits(ms);
const unit = options.unit === 'bestFit' ?
getBestMatchUnit(units) : options.unit;
const v = units[unit];
// CLDR uses past || future
const tl = v < 0 ? '-ago' : '-until';
const style = options.style || 'long';
const entry = unit + 's' + tl + '-' + style;
return {
unit: entry,
value: Math.abs(v)
};
}
function getUnitFormatGroupName(unitName) {
for (let groupName in unitFormatGroups) {
if (unitFormatGroups[groupName].units.includes(unitName)) {
return groupName;
}
}
return undefined;
}
})(this);