UNPKG

messageformat

Version:

Intl.MessageFormat / Unicode MessageFormat 2 parser, runtime and polyfill

186 lines (185 loc) 6.5 kB
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; }