Skip to content

Commit 79ac455

Browse files
Merge pull request #427 from messageformat/mf2-updates
MF2 spec updates
2 parents a450b36 + 0844814 commit 79ac455

32 files changed

+495
-321
lines changed

.github/workflows/docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v4
14-
- uses: actions/setup-node@v3
15-
with: { node-version: 18 }
14+
- uses: actions/setup-node@v4
15+
with: { node-version: 20 }
1616
- uses: ruby/setup-ruby@v1
1717
with:
1818
bundler-cache: true
19-
ruby-version: '2.7'
19+
ruby-version: '3.3'
2020
working-directory: ./docs
2121
- run: npm ci
2222
- run: npm run build

docs/Gemfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
source "https://rubygems.org"
22

33
gem "github-pages", group: :jekyll_plugins
4-
5-
# https://github.com/jekyll/jekyll/issues/8523
6-
gem "webrick", "~> 1.7"

packages/mf2-fluent/src/fluent-to-message.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ function asExpression(exp: Fluent.Expression): Expression {
119119
throw new Error(`More than one positional argument is not supported.`);
120120
}
121121
if (named.length > 0) {
122-
annotation.options = [];
122+
annotation.options = new Map();
123123
for (const { name, value } of named) {
124124
const quoted = value.type !== 'NumberLiteral';
125125
const litValue = quoted ? value.parse().value : value.value;
126-
annotation.options.push({
127-
name: name.name,
128-
value: { type: 'literal', value: litValue }
126+
annotation.options.set(name.name, {
127+
type: 'literal',
128+
value: litValue
129129
});
130130
}
131131
}
@@ -152,13 +152,13 @@ function asExpression(exp: Fluent.Expression): Expression {
152152
? `-${exp.id.name}.${exp.attribute.name}`
153153
: `-${exp.id.name}`;
154154
if (exp.arguments?.named.length) {
155-
annotation.options = [];
155+
annotation.options = new Map();
156156
for (const { name, value } of exp.arguments.named) {
157157
const quoted = value.type !== 'NumberLiteral';
158158
const litValue = quoted ? value.parse().value : value.value;
159-
annotation.options.push({
160-
name: name.name,
161-
value: { type: 'literal', value: litValue }
159+
annotation.options.set(name.name, {
160+
type: 'literal',
161+
value: litValue
162162
});
163163
}
164164
}

packages/mf2-fluent/src/fluent.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,15 @@ const testCases: Record<string, TestCase> = {
272272
msg: 'plural',
273273
scope: {},
274274
exp: 'B',
275-
errors: ['$selector', 'bad-operand', 'not-selectable']
275+
errors: ['$selector', 'bad-operand', 'bad-selector']
276276
},
277277
{ msg: 'plural', scope: { selector: 1 }, exp: 'A' },
278278
{ msg: 'plural', scope: { selector: 2 }, exp: 'B' },
279279
{
280280
msg: 'plural',
281281
scope: { selector: 'one' },
282282
exp: 'B',
283-
errors: ['bad-operand', 'not-selectable']
283+
errors: ['bad-operand', 'bad-selector']
284284
},
285285
{
286286
msg: 'default',
@@ -667,7 +667,7 @@ describe('formatToParts', () => {
667667
expect(msg).toEqual([{ type: 'literal', value: 'Other' }]);
668668
expect(onError.mock.calls.map(args => args[0].type)).toEqual([
669669
'bad-operand',
670-
'not-selectable'
670+
'bad-selector'
671671
]);
672672
});
673673
});

packages/mf2-fluent/src/message-to-fluent.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,19 @@ function functionRefToFluent(
165165
): Fluent.InlineExpression {
166166
const args = new Fluent.CallArguments();
167167
if (arg) args.positional[0] = arg;
168-
if (options) {
169-
args.named = options.map(opt => {
170-
const va = valueToFluent(ctx, opt.value);
168+
if (options?.size) {
169+
args.named = [];
170+
for (const [name, value] of options) {
171+
const va = valueToFluent(ctx, value);
171172
if (va instanceof Fluent.BaseLiteral) {
172-
const id = new Fluent.Identifier(opt.name);
173-
return new Fluent.NamedArgument(id, va);
173+
const id = new Fluent.Identifier(name);
174+
args.named.push(new Fluent.NamedArgument(id, va));
175+
} else {
176+
throw new Error(
177+
`Fluent options must have literal values (got ${va.type} for ${name})`
178+
);
174179
}
175-
throw new Error(
176-
`Fluent options must have literal values (got ${va.type} for ${opt.name})`
177-
);
178-
});
180+
}
179181
}
180182

181183
const id = ctx.functionMap[name];

packages/mf2-icu-mf1/src/mf1-to-message-data.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
Expression,
44
FunctionAnnotation,
55
Message,
6-
Option,
6+
Options,
77
VariableRef,
88
Variant
99
} from 'messageformat';
@@ -76,9 +76,7 @@ function tokenToPart(
7676
if (pt.type === 'content') value += pt.value;
7777
else throw new Error(`Unsupported param type: ${pt.type}`);
7878
}
79-
annotation.options = [
80-
{ name: 'param', value: { type: 'literal', value } }
81-
];
79+
annotation.options = new Map([['param', { type: 'literal', value }]]);
8280
}
8381
return {
8482
type: 'expression',
@@ -93,12 +91,9 @@ function tokenToPart(
9391
name: 'number'
9492
};
9593
if (pluralOffset) {
96-
annotation.options = [
97-
{
98-
name: 'pluralOffset',
99-
value: { type: 'literal', value: String(pluralOffset) }
100-
}
101-
];
94+
annotation.options = new Map([
95+
['pluralOffset', { type: 'literal', value: String(pluralOffset) }]
96+
]);
10297
}
10398
return {
10499
type: 'expression',
@@ -126,22 +121,19 @@ function argToExpression({
126121
};
127122
}
128123

129-
const options: Option[] = [];
124+
const options: Options = new Map();
130125
if (pluralOffset) {
131-
options.push({
132-
name: 'pluralOffset',
133-
value: { type: 'literal', value: String(pluralOffset) }
126+
options.set('pluralOffset', {
127+
type: 'literal',
128+
value: String(pluralOffset)
134129
});
135130
}
136131
if (type === 'selectordinal') {
137-
options.push({
138-
name: 'type',
139-
value: { type: 'literal', value: 'ordinal' }
140-
});
132+
options.set('type', { type: 'literal', value: 'ordinal' });
141133
}
142134

143135
const annotation: FunctionAnnotation = { type: 'function', name: 'number' };
144-
if (options.length) annotation.options = options;
136+
if (options.size) annotation.options = options;
145137

146138
return { type: 'expression', arg, annotation };
147139
}

packages/mf2-messageformat/src/cst/declarations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import type * as CST from './types.js';
55
import { whitespaces } from './util.js';
66
import { parseVariable } from './values.js';
77

8-
export function parseDeclarations(ctx: ParseContext): {
8+
export function parseDeclarations(
9+
ctx: ParseContext,
10+
start: number
11+
): {
912
declarations: CST.Declaration[];
1013
end: number;
1114
} {
1215
const { source } = ctx;
13-
let pos = whitespaces(source, 0);
16+
let pos = start;
1417
const declarations: CST.Declaration[] = [];
1518
loop: while (source[pos] === '.') {
1619
const keyword = parseNameValue(source, pos + 1);

packages/mf2-messageformat/src/cst/expression.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,15 +277,12 @@ function parseAttribute(ctx: ParseContext, start: number): CST.Attribute {
277277
let pos = id.end;
278278
const ws = whitespaces(source, pos);
279279
let equals: CST.Syntax<'='> | undefined;
280-
let value: CST.Literal | CST.VariableRef | undefined;
280+
let value: CST.Literal | undefined;
281281
if (source[pos + ws] === '=') {
282282
pos += ws + 1;
283283
equals = { start: pos - 1, end: pos, value: '=' };
284284
pos += whitespaces(source, pos);
285-
value =
286-
source[pos] === '$'
287-
? parseVariable(ctx, pos)
288-
: parseLiteral(ctx, pos, true);
285+
value = parseLiteral(ctx, pos, true);
289286
pos = value.end;
290287
}
291288
return {

packages/mf2-messageformat/src/cst/parse-cst.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,16 @@ export function parseCST(
5252
): CST.Message {
5353
const ctx = new ParseContext(source, opt);
5454

55-
if (source.startsWith('.')) {
56-
const { declarations, end: pos } = parseDeclarations(ctx);
57-
return source.startsWith('.match', pos)
58-
? parseSelectMessage(ctx, pos, declarations)
59-
: parsePatternMessage(ctx, pos, declarations, true);
55+
const pos = whitespaces(source, 0);
56+
if (source.startsWith('.', pos)) {
57+
const { declarations, end } = parseDeclarations(ctx, pos);
58+
return source.startsWith('.match', end)
59+
? parseSelectMessage(ctx, end, declarations)
60+
: parsePatternMessage(ctx, end, declarations, true);
6061
} else {
61-
return parsePatternMessage(ctx, 0, [], source.startsWith('{{'));
62+
return source.startsWith('{{', pos)
63+
? parsePatternMessage(ctx, pos, [], true)
64+
: parsePatternMessage(ctx, 0, [], false);
6265
}
6366
}
6467

@@ -95,7 +98,7 @@ function parseSelectMessage(
9598
const sel = parseExpression(ctx, pos);
9699
const body = sel.markup ?? sel.annotation;
97100
if (body && body.type !== 'function') {
98-
ctx.onError('bad-selector', body.start, body.end);
101+
ctx.onError('parse-error', body.start, body.end);
99102
}
100103
selectors.push(sel);
101104
pos = sel.end;

packages/mf2-messageformat/src/cst/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export interface Attribute {
186186
open: Syntax<'@'>;
187187
name: Identifier;
188188
equals?: Syntax<'='>;
189-
value?: Literal | VariableRef;
189+
value?: Literal;
190190
}
191191

192192
/** @beta */

0 commit comments

Comments
 (0)