src/impl/locale.js
import { Util } from './util';
import { English } from './english';
import { DateTime } from '../datetime';
const localeCache = new Map();
function intlConfigString(locale, numberingSystem, outputCalendar) {
let loc = locale || new Intl.DateTimeFormat().resolvedOptions().locale;
loc = Array.isArray(locale) ? locale : [locale];
if (outputCalendar || numberingSystem) {
loc = loc.map(l => {
l += '-u';
if (outputCalendar) {
l += '-ca-' + outputCalendar;
}
if (numberingSystem) {
l += '-nu-' + numberingSystem;
}
return l;
});
}
return loc;
}
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;
}
/**
* @private
*/
class PolyFormatter {
constructor(opts) {
this.padTo = opts.padTo || 0;
this.round = opts.round || false;
}
format(i) {
const maybeRounded = this.round ? Math.round(i) : i;
return maybeRounded.toString().padStart(this.padTo, '0');
}
}
/**
* @private
*/
export class Locale {
static fromOpts(opts) {
return Locale.create(
opts.locale,
opts.numberingSystem,
opts.outputCalendar
);
}
static create(locale, numberingSystem, outputCalendar) {
const localeR = locale || 'en-US',
numberingSystemR = numberingSystem || null,
outputCalendarR = outputCalendar || null,
cacheKey = `${localeR}|${numberingSystemR}|${outputCalendarR}`,
cached = localeCache.get(cacheKey);
if (cached) {
return cached;
} else {
const fresh = new Locale(localeR, numberingSystemR, outputCalendarR);
localeCache.set(cacheKey, fresh);
return fresh;
}
}
static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
return Locale.create(locale, numberingSystem, outputCalendar);
}
constructor(locale, numbering, outputCalendar) {
Object.defineProperty(this, 'locale', { value: locale, enumerable: true });
Object.defineProperty(this, 'numberingSystem', {
value: numbering || null,
enumerable: true
});
Object.defineProperty(this, 'outputCalendar', {
value: outputCalendar || null,
enumerable: true
});
Object.defineProperty(this, 'intl', {
value: intlConfigString(
this.locale,
this.numberingSystem,
this.outputCalendar
),
enumerable: false
});
// cached usefulness
Object.defineProperty(this, 'weekdaysCache', {
value: { format: {}, standalone: {} },
enumerable: false
});
Object.defineProperty(this, 'monthsCache', {
value: { format: {}, standalone: {} },
enumerable: false
});
Object.defineProperty(this, 'meridiemCache', {
value: null,
enumerable: false,
writable: true
});
Object.defineProperty(this, 'eraCache', {
value: {},
enumerable: false,
writable: true
});
}
knownEnglish() {
return (this.locale === 'en' ||
this.locale.toLowerCase() === 'en-us' ||
Intl.DateTimeFormat(this.intl)
.resolvedOptions()
.locale.startsWith('en-US')) &&
(this.numberingSystem === null || this.numberingSystem === 'latn') &&
(this.outputCalendar === null || this.outputCalendar === 'gregory');
}
clone(alts) {
if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
return this;
} else {
return Locale.create(
alts.locale || this.locale,
alts.numberingSystem || this.numberingSystem,
alts.outputCalendar || this.outputCalendar
);
}
}
months(length, format = false) {
if (this.knownEnglish()) {
const english = English.months(length);
if (english) {
return english;
}
}
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) {
if (this.knownEnglish()) {
const english = English.weekdays(length);
if (english) {
return english;
}
}
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() {
if (this.knownEnglish()) {
return 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) {
if (this.knownEnglish()) {
return English.eras(length);
}
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, d] = this.dtFormatter(dt, intlOpts),
results = df.formatToParts(d),
matching = results.find(m => m.type.toLowerCase() === field);
return matching ? matching.value : null;
}
numberFormatter(opts = {}, intlOpts = {}) {
if (Intl && Intl.NumberFormat) {
const realIntlOpts = Object.assign({ useGrouping: false }, intlOpts);
if (opts.padTo > 0) {
realIntlOpts.minimumIntegerDigits = opts.padTo;
}
if (opts.round) {
realIntlOpts.maximumFractionDigits = 0;
}
return new Intl.NumberFormat(this.intl, realIntlOpts);
} else {
return new PolyFormatter(opts);
}
}
dtFormatter(dt, intlOpts = {}) {
let d, z;
if (dt.zone.universal) {
// if we have a fixed-offset zone that isn't actually UTC,
// (like UTC+8), we need to make do with just displaying
// the time in UTC; the formatter doesn't know how to handle UTC+8
d = Util.asIfUTC(dt);
z = 'UTC';
} else if (dt.zone.type === 'local') {
d = dt.toJSDate();
} else {
d = dt.toJSDate();
z = dt.zone.name;
}
const realIntlOpts = Object.assign({}, intlOpts);
if (z) {
realIntlOpts.timeZone = z;
}
return [new Intl.DateTimeFormat(this.intl, realIntlOpts), d];
}
equals(other) {
return this.locale === other.locale &&
this.numberingSystem === other.numberingSystem &&
this.outputCalendar === other.outputCalendar;
}
}