messageformat
Version:
Intl.MessageFormat / Unicode MessageFormat 2 parser, runtime and polyfill
163 lines (162 loc) • 5.9 kB
JavaScript
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);
}