messageformat
Version:
Intl.MessageFormat / Unicode MessageFormat 2 parser, runtime and polyfill
186 lines (185 loc) • 6.5 kB
JavaScript
import { getLocaleDir } from "../dir-utils.js";
import { MessageFunctionError } from "../errors.js";
import { asBoolean, asString } from "./utils.js";
const dateFieldsValues = new Set([
'weekday',
'day-weekday',
'month-day',
'month-day-weekday',
'year-month-day',
'year-month-day-weekday'
]);
const dateLengthValues = new Set(['long', 'medium', 'short']);
const timePrecisionValues = new Set(['hour', 'minute', 'second']);
const timeZoneStyleValues = new Set(['long', 'short']);
/**
* The function `:datetime` is used to format a date/time value.
* Its formatted result will always include both the date and the time, and optionally a timezone.
*
* @beta
*/
export const datetime = (ctx, options, operand) => dateTimeImplementation('datetime', ctx, options, operand);
/**
* The function `:date` is used to format the date portion of date/time values.
*
* @beta
*/
export const date = (ctx, options, operand) => dateTimeImplementation('date', ctx, options, operand);
/**
* The function `:time` is used to format the time portion of date/time values.
* Its formatted result will always include the time, and optionally a timezone.
*
* @beta
*/
export const time = (ctx, options, operand) => dateTimeImplementation('time', ctx, options, operand);
function dateTimeImplementation(functionName, ctx, exprOpt, operand) {
const options = {
localeMatcher: ctx.localeMatcher
};
let value = operand;
if (typeof value === 'object' && value !== null) {
const opt = value.options;
if (opt) {
options.calendar = opt.calendar;
if (functionName !== 'date')
options.hour12 = opt.hour12;
options.timeZone = opt.timeZone;
}
if (typeof value.valueOf === 'function')
value = value.valueOf();
}
switch (typeof value) {
case 'number':
case 'string':
value = new Date(value);
}
if (!(value instanceof Date) || isNaN(value.getTime())) {
throw new MessageFunctionError('bad-operand', 'Input is not a valid date');
}
// Override options
if (exprOpt.calendar !== undefined) {
try {
options.calendar = asString(exprOpt.calendar);
}
catch {
ctx.onError('bad-option', `Invalid :${functionName} calendar option value`);
}
}
if (exprOpt.hour12 !== undefined && functionName !== 'date') {
try {
options.hour12 = asBoolean(exprOpt.hour12);
}
catch {
ctx.onError('bad-option', `Invalid :${functionName} hour12 option value`);
}
}
if (exprOpt.timeZone !== undefined) {
let tz;
try {
tz = asString(exprOpt.timeZone);
}
catch {
ctx.onError('bad-option', `Invalid :${functionName} timeZone option value`);
}
if (tz === 'input') {
if (options.timeZone === undefined) {
ctx.onError('bad-operand', `Missing input timeZone value for :${functionName}`);
}
}
else if (tz !== undefined) {
if (options.timeZone !== undefined && tz !== options.timeZone) {
// Use fallback value for expression
throw new MessageFunctionError('bad-option', 'Time zone conversion is not supported');
}
options.timeZone = tz;
}
}
// Date formatting options
if (functionName !== 'time') {
const dfName = functionName === 'date' ? 'fields' : 'dateFields';
const dlName = functionName === 'date' ? 'length' : 'dateLength';
const dateFieldsValue = readStringOption(ctx, exprOpt, dfName, dateFieldsValues) ??
'year-month-day';
const dateLength = readStringOption(ctx, exprOpt, dlName, dateLengthValues);
const dateFields = new Set(dateFieldsValue.split('-'));
if (dateFields.has('year'))
options.year = 'numeric';
if (dateFields.has('month')) {
options.month =
dateLength === 'long'
? 'long'
: dateLength === 'short'
? 'numeric'
: 'short';
}
if (dateFields.has('day'))
options.day = 'numeric';
if (dateFields.has('weekday')) {
options.weekday = dateLength === 'long' ? 'long' : 'short';
}
}
// Time formatting options
if (functionName !== 'date') {
const tpName = functionName === 'time' ? 'precision' : 'timePrecision';
switch (readStringOption(ctx, exprOpt, tpName, timePrecisionValues)) {
case 'hour':
options.hour = 'numeric';
break;
case 'second':
options.hour = 'numeric';
options.minute = 'numeric';
options.second = 'numeric';
break;
default:
options.hour = 'numeric';
options.minute = 'numeric';
}
options.timeZoneName = readStringOption(ctx, exprOpt, 'timeZoneStyle', timeZoneStyleValues);
}
// Resolved value
const dtf = new Intl.DateTimeFormat(ctx.locales, options);
let dir = ctx.dir;
let locale;
let str;
return {
type: 'datetime',
get dir() {
if (dir == null) {
locale ??= dtf.resolvedOptions().locale;
dir = getLocaleDir(locale);
}
return dir;
},
get options() {
return { ...options };
},
toParts() {
const parts = dtf.formatToParts(value);
locale ??= dtf.resolvedOptions().locale;
dir ??= getLocaleDir(locale);
return dir === 'ltr' || dir === 'rtl'
? [{ type: 'datetime', dir, locale, parts }]
: [{ type: 'datetime', locale, parts }];
},
toString() {
str ??= dtf.format(value);
return str;
},
valueOf: () => value
};
}
function readStringOption(ctx, options, name, allowed) {
const value = options[name];
if (value !== undefined) {
try {
const str = asString(value);
if (allowed && !allowed.has(str))
throw Error();
return str;
}
catch {
ctx.onError('bad-option', `Invalid value for ${name} option`);
}
}
return undefined;
}