Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e94fc7d

Browse files
authored
Merge pull request #433 from messageformat/mf2-updates
2 parents 742292b + 3622b9c commit e94fc7d

27 files changed

+477
-212
lines changed

mf2/fluent/src/fluent.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const testCases: Record<string, TestCase> = {
9191
{
9292
msg: 'num-fraction-bad',
9393
scope: { arg: 1234 },
94-
exp: '{$arg}',
94+
exp: '1,234',
9595
errors: ['bad-option']
9696
},
9797
{ msg: 'num-style', scope: { arg: 1234 }, exp: '123,400%' },

mf2/fluent/src/functions.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export function getFluentFunctions(res: FluentMessageResource) {
3838
type: 'fluent-message' as const,
3939
source,
4040
dir,
41-
locale,
4241
selectKey(keys) {
4342
str ??= mf.format(options, onError);
4443
return keys.has(str) ? str : null;

mf2/icu-messageformat-1/src/functions.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function time(
6060
* trailing `.sss` part for non-integer input
6161
*/
6262
function duration(
63-
{ locales, source }: MessageFunctionContext,
63+
{ source }: MessageFunctionContext,
6464
_options: unknown,
6565
input?: unknown
6666
) {
@@ -96,7 +96,6 @@ function duration(
9696

9797
return {
9898
type: 'mf1-duration' as const,
99-
locale: locales[0],
10099
source,
101100
toParts() {
102101
const res = { type: 'mf1-duration' as const, source, value: str };
@@ -148,12 +147,12 @@ function number(
148147
type: 'number',
149148
source,
150149
get dir() {
151-
dir ??= getLocaleDir(this.locale);
150+
if (dir == null) {
151+
locale ??= Intl.NumberFormat.supportedLocalesOf(locales, opt)[0];
152+
dir = getLocaleDir(locale);
153+
}
152154
return dir;
153155
},
154-
get locale() {
155-
return (locale ??= Intl.NumberFormat.supportedLocalesOf(locales, opt)[0]);
156-
},
157156
get options() {
158157
return { ...opt };
159158
},

mf2/messageformat/src/errors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export class MessageResolutionError extends MessageError {
8888
| 'bad-function-result'
8989
| 'bad-operand'
9090
| 'bad-option'
91-
| 'unresolved-variable';
91+
| 'unresolved-variable'
92+
| 'unsupported-operation';
9293
source: string;
9394
constructor(
9495
type: typeof MessageResolutionError.prototype.type,

mf2/messageformat/src/format-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MessageValue } from './functions/index.js';
1+
import type { MessageValue } from './message-value.js';
22
import type { MessageFunctions } from './messageformat.js';
33

44
export interface Context {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { MessageFormat } from '../index.js';
2+
3+
describe('fractionDigits', () => {
4+
for (const fd of [0, 2, 'auto' as const]) {
5+
test(`fractionDigits=${fd}`, () => {
6+
const mf = new MessageFormat(
7+
'en',
8+
`{42 :currency currency=EUR fractionDigits=${fd}}`
9+
);
10+
const nf = new Intl.NumberFormat('en', {
11+
style: 'currency',
12+
currency: 'EUR',
13+
minimumFractionDigits: fd === 'auto' ? undefined : fd,
14+
maximumFractionDigits: fd === 'auto' ? undefined : fd
15+
});
16+
expect(mf.format()).toEqual(nf.format(42));
17+
expect(mf.formatToParts()).toMatchObject([
18+
{ parts: nf.formatToParts(42) }
19+
]);
20+
});
21+
}
22+
});
23+
24+
describe('currencyDisplay', () => {
25+
for (const cd of [
26+
'narrowSymbol',
27+
'symbol',
28+
'name',
29+
'code',
30+
'formalSymbol',
31+
'never'
32+
]) {
33+
test(`currencyDisplay=${cd}`, () => {
34+
const mf = new MessageFormat(
35+
'en',
36+
`{42 :currency currency=EUR currencyDisplay=${cd}}`
37+
);
38+
const nf = new Intl.NumberFormat('en', {
39+
style: 'currency',
40+
currency: 'EUR',
41+
currencyDisplay:
42+
cd === 'formalSymbol' || cd === 'never' ? undefined : cd
43+
});
44+
const onError = jest.fn();
45+
expect(mf.format(undefined, onError)).toEqual(nf.format(42));
46+
expect(mf.formatToParts(undefined, onError)).toMatchObject([
47+
{ parts: nf.formatToParts(42) }
48+
]);
49+
if (cd === 'formalSymbol' || cd === 'never') {
50+
expect(onError.mock.calls).toMatchObject([
51+
[{ type: 'unsupported-operation' }],
52+
[{ type: 'unsupported-operation' }]
53+
]);
54+
} else {
55+
expect(onError.mock.calls).toMatchObject([]);
56+
}
57+
});
58+
}
59+
});
60+
61+
test('select=ordinal', () => {
62+
const mf = new MessageFormat(
63+
'en',
64+
'.local $n = {42 :currency currency=EUR select=ordinal} .match $n * {{res}}'
65+
);
66+
const onError = jest.fn();
67+
expect(mf.format(undefined, onError)).toEqual('res');
68+
expect(onError.mock.calls).toMatchObject([[{ type: 'bad-option' }]]);
69+
});
70+
71+
describe('complex operand', () => {
72+
test(':currency result', () => {
73+
const mf = new MessageFormat(
74+
'en',
75+
'.local $n = {-42 :currency currency=USD trailingZeroDisplay=stripIfInteger} {{{$n :currency currencySign=accounting}}}'
76+
);
77+
const nf = new Intl.NumberFormat('en', {
78+
style: 'currency',
79+
currencySign: 'accounting',
80+
// @ts-expect-error TS doesn't know about trailingZeroDisplay
81+
trailingZeroDisplay: 'stripIfInteger',
82+
currency: 'USD'
83+
});
84+
expect(mf.format()).toEqual(nf.format(-42));
85+
expect(mf.formatToParts()).toMatchObject([
86+
{ parts: nf.formatToParts(-42) }
87+
]);
88+
});
89+
90+
test('external variable', () => {
91+
const mf = new MessageFormat('en', '{$n :currency}');
92+
const nf = new Intl.NumberFormat('en', {
93+
style: 'currency',
94+
currency: 'EUR'
95+
});
96+
const n = { valueOf: () => 42, options: { currency: 'EUR' } };
97+
expect(mf.format({ n })).toEqual(nf.format(42));
98+
expect(mf.formatToParts({ n })).toMatchObject([
99+
{ parts: nf.formatToParts(42) }
100+
]);
101+
});
102+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { MessageError, MessageResolutionError } from '../errors.js';
2+
import type { MessageFunctionContext } from './index.js';
3+
import { type MessageNumber, number, readNumericOperand } from './number.js';
4+
import { asPositiveInteger, asString } from './utils.js';
5+
6+
/**
7+
* `currency` accepts as input numerical values as well as
8+
* objects wrapping a numerical value that also include a `currency` property.
9+
*
10+
* @beta
11+
*/
12+
export function currency(
13+
ctx: MessageFunctionContext,
14+
exprOpt: Record<string | symbol, unknown>,
15+
operand?: unknown
16+
): MessageNumber {
17+
const { source } = ctx;
18+
const input = readNumericOperand(operand, source);
19+
const value = input.value;
20+
const options: Intl.NumberFormatOptions &
21+
Intl.PluralRulesOptions & { select?: 'exact' | 'cardinal' } = Object.assign(
22+
{},
23+
input.options
24+
);
25+
26+
options.style = 'currency';
27+
for (const [name, optval] of Object.entries(exprOpt)) {
28+
if (optval === undefined) continue;
29+
try {
30+
switch (name) {
31+
case 'compactDisplay':
32+
case 'currency':
33+
case 'currencySign':
34+
case 'notation':
35+
case 'numberingSystem':
36+
case 'roundingMode':
37+
case 'roundingPriority':
38+
case 'trailingZeroDisplay':
39+
// @ts-expect-error Let Intl.NumberFormat construction fail
40+
options[name] = asString(optval);
41+
break;
42+
case 'minimumIntegerDigits':
43+
case 'minimumSignificantDigits':
44+
case 'maximumSignificantDigits':
45+
case 'roundingIncrement':
46+
// @ts-expect-error TS types don't know about roundingIncrement
47+
options[name] = asPositiveInteger(optval);
48+
break;
49+
case 'currencyDisplay': {
50+
const strval = asString(optval);
51+
if (strval === 'formalSymbol' || strval === 'never') {
52+
throw new MessageResolutionError(
53+
'unsupported-operation',
54+
`Currency display "${strval}" is not supported on :currency`,
55+
source
56+
);
57+
}
58+
options[name] = strval;
59+
break;
60+
}
61+
case 'fractionDigits': {
62+
const strval = asString(optval);
63+
if (strval === 'auto') {
64+
options.minimumFractionDigits = undefined;
65+
options.maximumFractionDigits = undefined;
66+
} else {
67+
const numval = asPositiveInteger(strval);
68+
options.minimumFractionDigits = numval;
69+
options.maximumFractionDigits = numval;
70+
}
71+
break;
72+
}
73+
case 'select': {
74+
const strval = asString(optval);
75+
if (strval === 'ordinal') {
76+
throw new MessageResolutionError(
77+
'bad-option',
78+
'Ordinal selection is not supported on :currency',
79+
source
80+
);
81+
}
82+
// @ts-expect-error Let Intl.NumberFormat construction fail
83+
options[name] = strval;
84+
break;
85+
}
86+
case 'useGrouping': {
87+
const strval = asString(optval);
88+
// @ts-expect-error TS type is wrong
89+
options[name] = strval === 'never' ? false : strval;
90+
break;
91+
}
92+
}
93+
} catch (error) {
94+
if (error instanceof MessageError) {
95+
ctx.onError(error);
96+
} else {
97+
const msg = `Value ${optval} is not valid for :currency option ${name}`;
98+
ctx.onError(new MessageResolutionError('bad-option', msg, source));
99+
}
100+
}
101+
}
102+
103+
if (!options.currency) {
104+
const msg = 'A currency code is required for :currency';
105+
throw new MessageResolutionError('bad-operand', msg, source);
106+
}
107+
108+
return number(ctx, {}, { valueOf: () => value, options });
109+
}

0 commit comments

Comments
 (0)