// 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 ); } }