Skip to content

Commit d5149ca

Browse files
feat(mf2): Implement :unit formatter (unicode-org/message-format-wg#922)
1 parent a67b9a5 commit d5149ca

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

mf2/fluent/src/fluent.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const testCases: Record<string, TestCase> = {
8383
num-fraction-bad = { NUMBER($arg, minimumFractionDigits: "oops") }
8484
num-currency = { NUMBER($arg, style: "currency", currency: "EUR") }
8585
# TODO: num-percent = { NUMBER($arg, style: "percent") }
86-
# TODO: num-unit = { NUMBER($arg, style: "unit", unit: "meter") }
86+
num-unit = { NUMBER($arg, style: "unit", unit: "meter") }
8787
num-unknown = { NUMBER($arg, unknown: "unknown") }
8888
`,
8989
tests: [
@@ -97,7 +97,7 @@ const testCases: Record<string, TestCase> = {
9797
},
9898
{ msg: 'num-currency', scope: { arg: 1234 }, exp: '€1,234.00' },
9999
// TODO: { msg: 'num-percent', scope: { arg: 1234 }, exp: '123,400%' },
100-
// TODO: { msg: 'num-unit', scope: { arg: 1234 }, exp: '1,234 m' },
100+
{ msg: 'num-unit', scope: { arg: 1234 }, exp: '1,234 m' },
101101
{ msg: 'num-unknown', scope: { arg: 1234 }, exp: '1,234' }
102102
]
103103
},

mf2/messageformat/src/functions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export { fallback, type MessageFallback } from './fallback.js';
66
export { math } from './math.js';
77
export { integer, number, type MessageNumber } from './number.js';
88
export { string, type MessageString } from './string.js';
9+
export { unit } from './unit.js';
910
export { unknown, type MessageUnknownValue } from './unknown.js';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { MessageFormat } from '../index.js';
2+
3+
test('selection', () => {
4+
const mf = new MessageFormat(
5+
'en',
6+
'.local $n = {42 :unit unit=meter} .match $n 42 {{exact}} * {{other}}'
7+
);
8+
const onError = jest.fn();
9+
expect(mf.format(undefined, onError)).toEqual('other');
10+
expect(onError.mock.calls).toMatchObject([[{ type: 'bad-selector' }]]);
11+
});
12+
13+
describe('complex operand', () => {
14+
test(':currency result', () => {
15+
const mf = new MessageFormat(
16+
'en',
17+
'.local $n = {42 :unit unit=meter trailingZeroDisplay=stripIfInteger} {{{$n :unit signDisplay=always}}}'
18+
);
19+
const nf = new Intl.NumberFormat('en', {
20+
signDisplay: 'always',
21+
style: 'unit',
22+
// @ts-expect-error TS doesn't know about trailingZeroDisplay
23+
trailingZeroDisplay: 'stripIfInteger',
24+
unit: 'meter'
25+
});
26+
expect(mf.format()).toEqual(nf.format(42));
27+
expect(mf.formatToParts()).toMatchObject([{ parts: nf.formatToParts(42) }]);
28+
});
29+
30+
test('external variable', () => {
31+
const mf = new MessageFormat('en', '{$n :unit}');
32+
const nf = new Intl.NumberFormat('en', { style: 'unit', unit: 'meter' });
33+
const n = { valueOf: () => 42, options: { unit: 'meter' } };
34+
expect(mf.format({ n })).toEqual(nf.format(42));
35+
expect(mf.formatToParts({ n })).toMatchObject([
36+
{ parts: nf.formatToParts(42) }
37+
]);
38+
});
39+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { MessageError, MessageResolutionError } from '../errors.js';
2+
import type { MessageFunctionContext } from './index.js';
3+
import type { MessageNumber, MessageNumberOptions } from './number.js';
4+
import { getMessageNumber, readNumericOperand } from './number.js';
5+
import { asPositiveInteger, asString } from './utils.js';
6+
7+
/**
8+
* `unit` accepts as input numerical values as well as
9+
* objects wrapping a numerical value that also include a `unit` property.
10+
*
11+
* @beta
12+
*/
13+
export function unit(
14+
ctx: MessageFunctionContext,
15+
exprOpt: Record<string | symbol, unknown>,
16+
operand?: unknown
17+
): MessageNumber {
18+
const { source } = ctx;
19+
const input = readNumericOperand(operand, source);
20+
const options: MessageNumberOptions = Object.assign({}, input.options, {
21+
localeMatcher: ctx.localeMatcher,
22+
style: 'unit'
23+
} as const);
24+
25+
for (const [name, optval] of Object.entries(exprOpt)) {
26+
if (optval === undefined) continue;
27+
try {
28+
switch (name) {
29+
case 'signDisplay':
30+
case 'roundingMode':
31+
case 'roundingPriority':
32+
case 'trailingZeroDisplay':
33+
case 'unit':
34+
case 'unitDisplay':
35+
case 'useGrouping':
36+
// @ts-expect-error Let Intl.NumberFormat construction fail
37+
options[name] = asString(optval);
38+
break;
39+
case 'minimumIntegerDigits':
40+
case 'minimumFractionDigits':
41+
case 'maximumFractionDigits':
42+
case 'minimumSignificantDigits':
43+
case 'maximumSignificantDigits':
44+
case 'roundingIncrement':
45+
// @ts-expect-error TS types don't know about roundingIncrement
46+
options[name] = asPositiveInteger(optval);
47+
break;
48+
}
49+
} catch (error) {
50+
if (error instanceof MessageError) {
51+
ctx.onError(error);
52+
} else {
53+
const msg = `Value ${optval} is not valid for :currency option ${name}`;
54+
ctx.onError(new MessageResolutionError('bad-option', msg, source));
55+
}
56+
}
57+
}
58+
59+
if (!options.unit) {
60+
const msg = 'A unit identifier is required for :unit';
61+
throw new MessageResolutionError('bad-operand', msg, source);
62+
}
63+
64+
return getMessageNumber(ctx, input.value, options, false);
65+
}

mf2/messageformat/src/messageformat.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
math,
1414
number,
1515
string,
16-
time
16+
time,
17+
unit
1718
} from './functions/index.js';
1819
import { BIDI_ISOLATE, type MessageValue } from './message-value.js';
1920
import { formatMarkup } from './resolve/format-markup.js';
@@ -30,7 +31,8 @@ const defaultFunctions = Object.freeze({
3031
math,
3132
number,
3233
string,
33-
time
34+
time,
35+
unit
3436
});
3537

3638
/**

0 commit comments

Comments
 (0)