UNPKG

messageformat

Version:

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

163 lines (162 loc) 5.9 kB
import { getLocaleDir } from "../dir-utils.js"; import { MessageFunctionError } from "../errors.js"; import { asPositiveInteger, asString } from "./utils.js"; export function readNumericOperand(value) { let options = undefined; if (typeof value === 'object') { const valueOf = value?.valueOf; if (typeof valueOf === 'function') { options = value.options; value = valueOf.call(value); } } if (typeof value === 'string') { try { value = JSON.parse(value); } catch { // handled below } } if (typeof value !== 'bigint' && typeof value !== 'number') { throw new MessageFunctionError('bad-operand', 'Input is not numeric'); } return { value, options }; } export function getMessageNumber(ctx, value, options, canSelect) { let { dir, locales } = ctx; // @ts-expect-error We may have been a bit naughty earlier. if (options.useGrouping === 'never') options.useGrouping = false; if (canSelect && 'select' in options && !ctx.literalOptionKeys.has('select')) { ctx.onError('bad-option', 'The option select may only be set by a literal value'); canSelect = false; } let locale; let nf; let cat; let str; return { type: 'number', get dir() { if (dir == null) { locale ??= Intl.NumberFormat.supportedLocalesOf(locales, options)[0]; dir = getLocaleDir(locale); } return dir; }, get options() { return { ...options }; }, selectKey: canSelect ? keys => { let numVal = value; if (options.style === 'percent') { if (typeof numVal === 'bigint') numVal *= 100n; else numVal *= 100; } const str = String(numVal); if (keys.has(str)) return str; if (options.select === 'exact') return null; const pluralOpt = options.select ? { ...options, select: undefined, type: options.select } : options; // Intl.PluralRules needs a number, not bigint cat ??= new Intl.PluralRules(locales, pluralOpt).select(Number(numVal)); return keys.has(cat) ? cat : null; } : undefined, toParts() { nf ??= new Intl.NumberFormat(locales, options); const parts = nf.formatToParts(value); locale ??= nf.resolvedOptions().locale; dir ??= getLocaleDir(locale); return dir === 'ltr' || dir === 'rtl' ? [{ type: 'number', dir, locale, parts }] : [{ type: 'number', locale, parts }]; }, toString() { nf ??= new Intl.NumberFormat(locales, options); str ??= nf.format(value); return str; }, valueOf: () => value }; } export function number(ctx, exprOpt, operand) { const input = readNumericOperand(operand); const value = input.value; const options = Object.assign({}, input.options, { localeMatcher: ctx.localeMatcher, style: 'decimal' }); for (const [name, optval] of Object.entries(exprOpt)) { if (optval === undefined) continue; try { switch (name) { case 'minimumIntegerDigits': case 'minimumFractionDigits': case 'maximumFractionDigits': case 'minimumSignificantDigits': case 'maximumSignificantDigits': case 'roundingIncrement': // @ts-expect-error TS types don't know about roundingIncrement options[name] = asPositiveInteger(optval); break; case 'roundingMode': case 'roundingPriority': case 'select': // Called 'type' in Intl.PluralRules case 'signDisplay': case 'trailingZeroDisplay': case 'useGrouping': // @ts-expect-error Let Intl.NumberFormat construction fail options[name] = asString(optval); } } catch { ctx.onError('bad-option', `Value ${optval} is not valid for :number option ${name}`); } } return getMessageNumber(ctx, value, options, true); } export function integer(ctx, exprOpt, operand) { const input = readNumericOperand(operand); const value = Number.isFinite(input.value) ? Math.round(input.value) : input.value; const options = Object.assign({}, input.options, { //localeMatcher: ctx.localeMatcher, maximumFractionDigits: 0, minimumFractionDigits: undefined, minimumSignificantDigits: undefined, style: 'decimal' }); for (const [name, optval] of Object.entries(exprOpt)) { if (optval === undefined) continue; try { switch (name) { case 'minimumIntegerDigits': case 'maximumSignificantDigits': options[name] = asPositiveInteger(optval); break; case 'select': // Called 'type' in Intl.PluralRules case 'signDisplay': case 'useGrouping': // @ts-expect-error Let Intl.NumberFormat construction fail options[name] = asString(optval); } } catch { ctx.onError('bad-option', `Value ${optval} is not valid for :integer option ${name}`); } } return getMessageNumber(ctx, value, options, true); }