mirror of
https://github.com/swc-project/swc.git
synced 2024-12-27 23:56:32 +03:00
473 lines
13 KiB
TypeScript
473 lines
13 KiB
TypeScript
// Loaded from https://unpkg.com/luxon@1.25.0/src/impl/locale.js
|
|
|
|
|
|
import { hasFormatToParts, hasIntl, padStart, roundTo, hasRelative } from "./util.js";
|
|
import * as English from "./english.js";
|
|
import Settings from "../settings.js";
|
|
import DateTime from "../datetime.js";
|
|
import Formatter from "./formatter.js";
|
|
|
|
let intlDTCache = {};
|
|
function getCachedDTF(locString, opts = {}) {
|
|
const key = JSON.stringify([locString, opts]);
|
|
let dtf = intlDTCache[key];
|
|
if (!dtf) {
|
|
dtf = new Intl.DateTimeFormat(locString, opts);
|
|
intlDTCache[key] = dtf;
|
|
}
|
|
return dtf;
|
|
}
|
|
|
|
let intlNumCache = {};
|
|
function getCachedINF(locString, opts = {}) {
|
|
const key = JSON.stringify([locString, opts]);
|
|
let inf = intlNumCache[key];
|
|
if (!inf) {
|
|
inf = new Intl.NumberFormat(locString, opts);
|
|
intlNumCache[key] = inf;
|
|
}
|
|
return inf;
|
|
}
|
|
|
|
let intlRelCache = {};
|
|
function getCachedRTF(locString, opts = {}) {
|
|
const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options
|
|
const key = JSON.stringify([locString, cacheKeyOpts]);
|
|
let inf = intlRelCache[key];
|
|
if (!inf) {
|
|
inf = new Intl.RelativeTimeFormat(locString, opts);
|
|
intlRelCache[key] = inf;
|
|
}
|
|
return inf;
|
|
}
|
|
|
|
let sysLocaleCache = null;
|
|
function systemLocale() {
|
|
if (sysLocaleCache) {
|
|
return sysLocaleCache;
|
|
} else if (hasIntl()) {
|
|
const computedSys = new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
// node sometimes defaults to "und". Override that because that is dumb
|
|
sysLocaleCache = !computedSys || computedSys === "und" ? "en-US" : computedSys;
|
|
return sysLocaleCache;
|
|
} else {
|
|
sysLocaleCache = "en-US";
|
|
return sysLocaleCache;
|
|
}
|
|
}
|
|
|
|
function parseLocaleString(localeStr) {
|
|
// I really want to avoid writing a BCP 47 parser
|
|
// see, e.g. https://github.com/wooorm/bcp-47
|
|
// Instead, we'll do this:
|
|
|
|
// a) if the string has no -u extensions, just leave it alone
|
|
// b) if it does, use Intl to resolve everything
|
|
// c) if Intl fails, try again without the -u
|
|
|
|
const uIndex = localeStr.indexOf("-u-");
|
|
if (uIndex === -1) {
|
|
return [localeStr];
|
|
} else {
|
|
let options;
|
|
const smaller = localeStr.substring(0, uIndex);
|
|
try {
|
|
options = getCachedDTF(localeStr).resolvedOptions();
|
|
} catch (e) {
|
|
options = getCachedDTF(smaller).resolvedOptions();
|
|
}
|
|
|
|
const { numberingSystem, calendar } = options;
|
|
// return the smaller one so that we can append the calendar and numbering overrides to it
|
|
return [smaller, numberingSystem, calendar];
|
|
}
|
|
}
|
|
|
|
function intlConfigString(localeStr, numberingSystem, outputCalendar) {
|
|
if (hasIntl()) {
|
|
if (outputCalendar || numberingSystem) {
|
|
localeStr += "-u";
|
|
|
|
if (outputCalendar) {
|
|
localeStr += `-ca-${outputCalendar}`;
|
|
}
|
|
|
|
if (numberingSystem) {
|
|
localeStr += `-nu-${numberingSystem}`;
|
|
}
|
|
return localeStr;
|
|
} else {
|
|
return localeStr;
|
|
}
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function mapMonths(f) {
|
|
const ms = [];
|
|
for (let i = 1; i <= 12; i++) {
|
|
const dt = DateTime.utc(2016, i, 1);
|
|
ms.push(f(dt));
|
|
}
|
|
return ms;
|
|
}
|
|
|
|
function mapWeekdays(f) {
|
|
const ms = [];
|
|
for (let i = 1; i <= 7; i++) {
|
|
const dt = DateTime.utc(2016, 11, 13 + i);
|
|
ms.push(f(dt));
|
|
}
|
|
return ms;
|
|
}
|
|
|
|
function listStuff(loc, length, defaultOK, englishFn, intlFn) {
|
|
const mode = loc.listingMode(defaultOK);
|
|
|
|
if (mode === "error") {
|
|
return null;
|
|
} else if (mode === "en") {
|
|
return englishFn(length);
|
|
} else {
|
|
return intlFn(length);
|
|
}
|
|
}
|
|
|
|
function supportsFastNumbers(loc) {
|
|
if (loc.numberingSystem && loc.numberingSystem !== "latn") {
|
|
return false;
|
|
} else {
|
|
return (
|
|
loc.numberingSystem === "latn" ||
|
|
!loc.locale ||
|
|
loc.locale.startsWith("en") ||
|
|
(hasIntl() && new Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn")
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
class PolyNumberFormatter {
|
|
constructor(intl, forceSimple, opts) {
|
|
this.padTo = opts.padTo || 0;
|
|
this.floor = opts.floor || false;
|
|
|
|
if (!forceSimple && hasIntl()) {
|
|
const intlOpts = { useGrouping: false };
|
|
if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo;
|
|
this.inf = getCachedINF(intl, intlOpts);
|
|
}
|
|
}
|
|
|
|
format(i) {
|
|
if (this.inf) {
|
|
const fixed = this.floor ? Math.floor(i) : i;
|
|
return this.inf.format(fixed);
|
|
} else {
|
|
// to match the browser's numberformatter defaults
|
|
const fixed = this.floor ? Math.floor(i) : roundTo(i, 3);
|
|
return padStart(fixed, this.padTo);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
class PolyDateFormatter {
|
|
constructor(dt, intl, opts) {
|
|
this.opts = opts;
|
|
this.hasIntl = hasIntl();
|
|
|
|
let z;
|
|
if (dt.zone.universal && this.hasIntl) {
|
|
// Chromium doesn't support fixed-offset zones like Etc/GMT+8 in its formatter,
|
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=364374.
|
|
// So we have to make do. Two cases:
|
|
// 1. The format options tell us to show the zone. We can't do that, so the best
|
|
// we can do is format the date in UTC.
|
|
// 2. The format options don't tell us to show the zone. Then we can adjust them
|
|
// the time and tell the formatter to show it to us in UTC, so that the time is right
|
|
// and the bad zone doesn't show up.
|
|
// We can clean all this up when Chrome fixes this.
|
|
z = "UTC";
|
|
if (opts.timeZoneName) {
|
|
this.dt = dt;
|
|
} else {
|
|
this.dt = dt.offset === 0 ? dt : DateTime.fromMillis(dt.ts + dt.offset * 60 * 1000);
|
|
}
|
|
} else if (dt.zone.type === "local") {
|
|
this.dt = dt;
|
|
} else {
|
|
this.dt = dt;
|
|
z = dt.zone.name;
|
|
}
|
|
|
|
if (this.hasIntl) {
|
|
const intlOpts = Object.assign({}, this.opts);
|
|
if (z) {
|
|
intlOpts.timeZone = z;
|
|
}
|
|
this.dtf = getCachedDTF(intl, intlOpts);
|
|
}
|
|
}
|
|
|
|
format() {
|
|
if (this.hasIntl) {
|
|
return this.dtf.format(this.dt.toJSDate());
|
|
} else {
|
|
const tokenFormat = English.formatString(this.opts),
|
|
loc = Locale.create("en-US");
|
|
return Formatter.create(loc).formatDateTimeFromString(this.dt, tokenFormat);
|
|
}
|
|
}
|
|
|
|
formatToParts() {
|
|
if (this.hasIntl && hasFormatToParts()) {
|
|
return this.dtf.formatToParts(this.dt.toJSDate());
|
|
} else {
|
|
// This is kind of a cop out. We actually could do this for English. However, we couldn't do it for intl strings
|
|
// and IMO it's too weird to have an uncanny valley like that
|
|
return [];
|
|
}
|
|
}
|
|
|
|
resolvedOptions() {
|
|
if (this.hasIntl) {
|
|
return this.dtf.resolvedOptions();
|
|
} else {
|
|
return {
|
|
locale: "en-US",
|
|
numberingSystem: "latn",
|
|
outputCalendar: "gregory"
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
class PolyRelFormatter {
|
|
constructor(intl, isEnglish, opts) {
|
|
this.opts = Object.assign({ style: "long" }, opts);
|
|
if (!isEnglish && hasRelative()) {
|
|
this.rtf = getCachedRTF(intl, opts);
|
|
}
|
|
}
|
|
|
|
format(count, unit) {
|
|
if (this.rtf) {
|
|
return this.rtf.format(count, unit);
|
|
} else {
|
|
return English.formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long");
|
|
}
|
|
}
|
|
|
|
formatToParts(count, unit) {
|
|
if (this.rtf) {
|
|
return this.rtf.formatToParts(count, unit);
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
export default class Locale {
|
|
static fromOpts(opts) {
|
|
return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.defaultToEN);
|
|
}
|
|
|
|
static create(locale, numberingSystem, outputCalendar, defaultToEN = false) {
|
|
const specifiedLocale = locale || Settings.defaultLocale,
|
|
// the system locale is useful for human readable strings but annoying for parsing/formatting known formats
|
|
localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()),
|
|
numberingSystemR = numberingSystem || Settings.defaultNumberingSystem,
|
|
outputCalendarR = outputCalendar || Settings.defaultOutputCalendar;
|
|
return new Locale(localeR, numberingSystemR, outputCalendarR, specifiedLocale);
|
|
}
|
|
|
|
static resetCache() {
|
|
sysLocaleCache = null;
|
|
intlDTCache = {};
|
|
intlNumCache = {};
|
|
intlRelCache = {};
|
|
}
|
|
|
|
static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
|
|
return Locale.create(locale, numberingSystem, outputCalendar);
|
|
}
|
|
|
|
constructor(locale, numbering, outputCalendar, specifiedLocale) {
|
|
const [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale);
|
|
|
|
this.locale = parsedLocale;
|
|
this.numberingSystem = numbering || parsedNumberingSystem || null;
|
|
this.outputCalendar = outputCalendar || parsedOutputCalendar || null;
|
|
this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar);
|
|
|
|
this.weekdaysCache = { format: {}, standalone: {} };
|
|
this.monthsCache = { format: {}, standalone: {} };
|
|
this.meridiemCache = null;
|
|
this.eraCache = {};
|
|
|
|
this.specifiedLocale = specifiedLocale;
|
|
this.fastNumbersCached = null;
|
|
}
|
|
|
|
get fastNumbers() {
|
|
if (this.fastNumbersCached == null) {
|
|
this.fastNumbersCached = supportsFastNumbers(this);
|
|
}
|
|
|
|
return this.fastNumbersCached;
|
|
}
|
|
|
|
listingMode(defaultOK = true) {
|
|
const intl = hasIntl(),
|
|
hasFTP = intl && hasFormatToParts(),
|
|
isActuallyEn = this.isEnglish(),
|
|
hasNoWeirdness =
|
|
(this.numberingSystem === null || this.numberingSystem === "latn") &&
|
|
(this.outputCalendar === null || this.outputCalendar === "gregory");
|
|
|
|
if (!hasFTP && !(isActuallyEn && hasNoWeirdness) && !defaultOK) {
|
|
return "error";
|
|
} else if (!hasFTP || (isActuallyEn && hasNoWeirdness)) {
|
|
return "en";
|
|
} else {
|
|
return "intl";
|
|
}
|
|
}
|
|
|
|
clone(alts) {
|
|
if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
|
|
return this;
|
|
} else {
|
|
return Locale.create(
|
|
alts.locale || this.specifiedLocale,
|
|
alts.numberingSystem || this.numberingSystem,
|
|
alts.outputCalendar || this.outputCalendar,
|
|
alts.defaultToEN || false
|
|
);
|
|
}
|
|
}
|
|
|
|
redefaultToEN(alts = {}) {
|
|
return this.clone(Object.assign({}, alts, { defaultToEN: true }));
|
|
}
|
|
|
|
redefaultToSystem(alts = {}) {
|
|
return this.clone(Object.assign({}, alts, { defaultToEN: false }));
|
|
}
|
|
|
|
months(length, format = false, defaultOK = true) {
|
|
return listStuff(this, length, defaultOK, English.months, () => {
|
|
const intl = format ? { month: length, day: "numeric" } : { month: length },
|
|
formatStr = format ? "format" : "standalone";
|
|
if (!this.monthsCache[formatStr][length]) {
|
|
this.monthsCache[formatStr][length] = mapMonths(dt => this.extract(dt, intl, "month"));
|
|
}
|
|
return this.monthsCache[formatStr][length];
|
|
});
|
|
}
|
|
|
|
weekdays(length, format = false, defaultOK = true) {
|
|
return listStuff(this, length, defaultOK, English.weekdays, () => {
|
|
const intl = format
|
|
? { weekday: length, year: "numeric", month: "long", day: "numeric" }
|
|
: { weekday: length },
|
|
formatStr = format ? "format" : "standalone";
|
|
if (!this.weekdaysCache[formatStr][length]) {
|
|
this.weekdaysCache[formatStr][length] = mapWeekdays(dt =>
|
|
this.extract(dt, intl, "weekday")
|
|
);
|
|
}
|
|
return this.weekdaysCache[formatStr][length];
|
|
});
|
|
}
|
|
|
|
meridiems(defaultOK = true) {
|
|
return listStuff(
|
|
this,
|
|
undefined,
|
|
defaultOK,
|
|
() => English.meridiems,
|
|
() => {
|
|
// In theory there could be aribitrary day periods. We're gonna assume there are exactly two
|
|
// for AM and PM. This is probably wrong, but it's makes parsing way easier.
|
|
if (!this.meridiemCache) {
|
|
const intl = { hour: "numeric", hour12: true };
|
|
this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(
|
|
dt => this.extract(dt, intl, "dayperiod")
|
|
);
|
|
}
|
|
|
|
return this.meridiemCache;
|
|
}
|
|
);
|
|
}
|
|
|
|
eras(length, defaultOK = true) {
|
|
return listStuff(this, length, defaultOK, English.eras, () => {
|
|
const intl = { era: length };
|
|
|
|
// This is utter bullshit. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
|
|
// to definitely enumerate them.
|
|
if (!this.eraCache[length]) {
|
|
this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map(dt =>
|
|
this.extract(dt, intl, "era")
|
|
);
|
|
}
|
|
|
|
return this.eraCache[length];
|
|
});
|
|
}
|
|
|
|
extract(dt, intlOpts, field) {
|
|
const df = this.dtFormatter(dt, intlOpts),
|
|
results = df.formatToParts(),
|
|
matching = results.find(m => m.type.toLowerCase() === field);
|
|
return matching ? matching.value : null;
|
|
}
|
|
|
|
numberFormatter(opts = {}) {
|
|
// this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave)
|
|
// (in contrast, the rest of the condition is used heavily)
|
|
return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts);
|
|
}
|
|
|
|
dtFormatter(dt, intlOpts = {}) {
|
|
return new PolyDateFormatter(dt, this.intl, intlOpts);
|
|
}
|
|
|
|
relFormatter(opts = {}) {
|
|
return new PolyRelFormatter(this.intl, this.isEnglish(), opts);
|
|
}
|
|
|
|
isEnglish() {
|
|
return (
|
|
this.locale === "en" ||
|
|
this.locale.toLowerCase() === "en-us" ||
|
|
(hasIntl() && new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us"))
|
|
);
|
|
}
|
|
|
|
equals(other) {
|
|
return (
|
|
this.locale === other.locale &&
|
|
this.numberingSystem === other.numberingSystem &&
|
|
this.outputCalendar === other.outputCalendar
|
|
);
|
|
}
|
|
}
|