Skip to content

Commit 0345e08

Browse files
committed
Allow VariableReference as NamedArgument value
1 parent 82797c4 commit 0345e08

File tree

14 files changed

+302
-30
lines changed

14 files changed

+302
-30
lines changed

fluent-bundle/src/ast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export type Variant = {
6868
export type NamedArgument = {
6969
type: "narg";
7070
name: string;
71-
value: Literal;
71+
value: Literal | VariableReference;
7272
};
7373

7474
export type Literal = StringLiteral | NumberLiteral;

fluent-bundle/src/resource.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const RE_VARIANT_START = /\*?\[/y;
2929
const RE_NUMBER_LITERAL = /(-?[0-9]+(?:\.([0-9]+))?)/y;
3030
const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y;
3131
const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y;
32+
const RE_VARIABLE_REF = /[$]([a-zA-Z][\w-]*)/y;
3233
const RE_FUNCTION_NAME = /^[A-Z][A-Z0-9_-]*$/;
3334

3435
// A "run" is a sequence of text or string literal characters which don't
@@ -349,11 +350,10 @@ function parseMessage(source: string, cursor: number, id: string): Message {
349350

350351
if (consumeToken(TOKEN_COLON)) {
351352
// The reference is the beginning of a named argument.
352-
return {
353-
type: "narg",
354-
name: expr.name,
355-
value: parseLiteral(),
356-
} satisfies NamedArgument;
353+
const ref = match(RE_VARIABLE_REF, false);
354+
const value: Literal | VariableReference =
355+
ref === null ? parseLiteral() : { type: "var", name: ref[1] };
356+
return { type: "narg", name: expr.name, value } satisfies NamedArgument;
357357
}
358358

359359
// It's a regular message reference.

fluent-bundle/test/fixtures_reference/call_expressions.json

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,31 @@
160160
],
161161
"attributes": {}
162162
},
163+
{
164+
"id": "variable-args",
165+
"value": [
166+
{
167+
"type": "func",
168+
"name": "FUN",
169+
"args": [
170+
{
171+
"type": "var",
172+
"name": "foo"
173+
},
174+
{
175+
"type": "narg",
176+
"name": "arg",
177+
"value": {
178+
"type": "var",
179+
"name": "bar"
180+
}
181+
}
182+
183+
]
184+
}
185+
],
186+
"attributes": {}
187+
},
163188
{
164189
"id": "shuffled-args",
165190
"value": [
@@ -555,7 +580,7 @@
555580
"attributes": {}
556581
},
557582
{
558-
"id": "mulitline-args",
583+
"id": "multiline-args",
559584
"value": [
560585
{
561586
"type": "func",
@@ -577,7 +602,7 @@
577602
"attributes": {}
578603
},
579604
{
580-
"id": "mulitline-sparse-args",
605+
"id": "multiline-sparse-args",
581606
"value": [
582607
{
583608
"type": "func",

fluent-bundle/test/fixtures_reference/term_parameters.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@
103103
}
104104
],
105105
"attributes": {}
106+
},
107+
{
108+
"id": "key05",
109+
"value": [
110+
{
111+
"type": "term",
112+
"name": "term",
113+
"attr": null,
114+
"args": [
115+
{
116+
"type": "narg",
117+
"name": "arg",
118+
"value": {
119+
"type": "var",
120+
"name": "foo"
121+
}
122+
}
123+
]
124+
}
125+
],
126+
"attributes": {}
106127
}
107128
]
108129
}

fluent-bundle/test/functions_builtin_test.js

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ suite("Built-in functions", function () {
1414
bundle.addResource(
1515
new FluentResource(ftl`
1616
num-bare = { NUMBER($arg) }
17-
num-fraction-valid = { NUMBER($arg, minimumFractionDigits: 1) }
17+
num-fraction-literal = { NUMBER($arg, minimumFractionDigits: 1) }
18+
num-fraction-variable = { NUMBER($arg, minimumFractionDigits: $mfd) }
1819
num-fraction-bad = { NUMBER($arg, minimumFractionDigits: "oops") }
1920
num-style = { NUMBER($arg, style: "percent") }
2021
num-currency = { NUMBER($arg, currency: "EUR") }
@@ -35,7 +36,7 @@ suite("Built-in functions", function () {
3536
assert.strictEqual(errors[0].message, "Unknown variable: $arg");
3637

3738
errors = [];
38-
msg = bundle.getMessage("num-fraction-valid");
39+
msg = bundle.getMessage("num-fraction-literal");
3940
assert.strictEqual(
4041
bundle.formatPattern(msg.value, {}, errors),
4142
"{NUMBER($arg)}"
@@ -44,6 +45,18 @@ suite("Built-in functions", function () {
4445
assert.ok(errors[0] instanceof ReferenceError);
4546
assert.strictEqual(errors[0].message, "Unknown variable: $arg");
4647

48+
errors = [];
49+
msg = bundle.getMessage("num-fraction-variable");
50+
assert.strictEqual(
51+
bundle.formatPattern(msg.value, {}, errors),
52+
"{NUMBER($arg)}"
53+
);
54+
assert.strictEqual(errors.length, 2);
55+
assert.ok(errors[0] instanceof ReferenceError);
56+
assert.strictEqual(errors[0].message, "Unknown variable: $arg");
57+
assert.ok(errors[1] instanceof ReferenceError);
58+
assert.strictEqual(errors[1].message, "Unknown variable: $mfd");
59+
4760
errors = [];
4861
msg = bundle.getMessage("num-fraction-bad");
4962
assert.strictEqual(
@@ -97,13 +110,21 @@ suite("Built-in functions", function () {
97110
assert.strictEqual(errors.length, 0);
98111

99112
errors = [];
100-
msg = bundle.getMessage("num-fraction-valid");
113+
msg = bundle.getMessage("num-fraction-literal");
101114
assert.strictEqual(
102115
bundle.formatPattern(msg.value, { arg }, errors),
103116
"1,234.0"
104117
);
105118
assert.strictEqual(errors.length, 0);
106119

120+
errors = [];
121+
msg = bundle.getMessage("num-fraction-variable");
122+
assert.strictEqual(
123+
bundle.formatPattern(msg.value, { arg, mfd: 1 }, errors),
124+
"1,234.0"
125+
);
126+
assert.strictEqual(errors.length, 0);
127+
107128
errors = [];
108129
msg = bundle.getMessage("num-fraction-bad");
109130
assert.strictEqual(
@@ -152,13 +173,22 @@ suite("Built-in functions", function () {
152173
assert.strictEqual(errors.length, 0);
153174

154175
errors = [];
155-
msg = bundle.getMessage("num-fraction-valid");
176+
msg = bundle.getMessage("num-fraction-literal");
156177
assert.strictEqual(
157178
bundle.formatPattern(msg.value, { arg }, errors),
158179
"1,234.0"
159180
);
160181
assert.strictEqual(errors.length, 0);
161182

183+
errors = [];
184+
msg = bundle.getMessage("num-fraction-variable");
185+
const mfd = new FluentNumber(1);
186+
assert.strictEqual(
187+
bundle.formatPattern(msg.value, { arg, mfd }, errors),
188+
"1,234.0"
189+
);
190+
assert.strictEqual(errors.length, 0);
191+
162192
errors = [];
163193
msg = bundle.getMessage("num-fraction-bad");
164194
assert.strictEqual(
@@ -208,13 +238,22 @@ suite("Built-in functions", function () {
208238
assert.strictEqual(errors.length, 0);
209239

210240
errors = [];
211-
msg = bundle.getMessage("num-fraction-valid");
241+
msg = bundle.getMessage("num-fraction-literal");
212242
assert.strictEqual(
213243
bundle.formatPattern(msg.value, { arg }, errors),
214244
"$1,234.0"
215245
);
216246
assert.strictEqual(errors.length, 0);
217247

248+
errors = [];
249+
msg = bundle.getMessage("num-fraction-variable");
250+
const mfd = new FluentNumber(1, { style: "currency", currency: "USD" });
251+
assert.strictEqual(
252+
bundle.formatPattern(msg.value, { arg, mfd }, errors),
253+
"$1,234.0"
254+
);
255+
assert.strictEqual(errors.length, 0);
256+
218257
errors = [];
219258
msg = bundle.getMessage("num-fraction-bad");
220259
assert.strictEqual(
@@ -266,7 +305,7 @@ suite("Built-in functions", function () {
266305
assert.strictEqual(errors.length, 0);
267306

268307
errors = [];
269-
msg = bundle.getMessage("num-fraction-valid");
308+
msg = bundle.getMessage("num-fraction-literal");
270309
assert.strictEqual(
271310
bundle.formatPattern(msg.value, { arg }, errors),
272311
"1,475,107,200,000.0"
@@ -288,7 +327,7 @@ suite("Built-in functions", function () {
288327
assert.strictEqual(errors[0].message, "Invalid argument to NUMBER");
289328

290329
errors = [];
291-
msg = bundle.getMessage("num-fraction-valid");
330+
msg = bundle.getMessage("num-fraction-literal");
292331
assert.strictEqual(
293332
bundle.formatPattern(msg.value, { arg }, errors),
294333
"{NUMBER()}"
@@ -297,6 +336,14 @@ suite("Built-in functions", function () {
297336
assert.ok(errors[0] instanceof TypeError);
298337
assert.strictEqual(errors[0].message, "Invalid argument to NUMBER");
299338

339+
errors = [];
340+
msg = bundle.getMessage("num-fraction-variable");
341+
assert.strictEqual(
342+
bundle.formatPattern(msg.value, { arg: 10, mfd: " 1 " }, errors),
343+
"10.0"
344+
);
345+
assert.strictEqual(errors.length, 0);
346+
300347
errors = [];
301348
msg = bundle.getMessage("num-fraction-bad");
302349
assert.strictEqual(
@@ -350,7 +397,7 @@ suite("Built-in functions", function () {
350397
assert.strictEqual(errors.length, 0);
351398

352399
errors = [];
353-
msg = bundle.getMessage("num-fraction-valid");
400+
msg = bundle.getMessage("num-fraction-literal");
354401
assert.strictEqual(
355402
bundle.formatPattern(msg.value, { arg }, errors),
356403
"1,475,107,200,000.0"
@@ -408,7 +455,7 @@ suite("Built-in functions", function () {
408455
);
409456

410457
errors = [];
411-
msg = bundle.getMessage("num-fraction-valid");
458+
msg = bundle.getMessage("num-fraction-literal");
412459
assert.strictEqual(
413460
bundle.formatPattern(msg.value, { arg }, errors),
414461
"{NUMBER($arg)}"
@@ -420,6 +467,20 @@ suite("Built-in functions", function () {
420467
"Variable type not supported: $arg, object"
421468
);
422469

470+
errors = [];
471+
msg = bundle.getMessage("num-fraction-variable");
472+
assert.strictEqual(
473+
bundle.formatPattern(msg.value, { arg: 10, mfd: [] }, errors),
474+
"10"
475+
);
476+
assert.strictEqual(errors.length, 2);
477+
assert.ok(errors[0] instanceof TypeError);
478+
assert.strictEqual(
479+
errors[0].message,
480+
"Variable type not supported: $mfd, object"
481+
);
482+
assert.ok(errors[1] instanceof RangeError);
483+
423484
errors = [];
424485
msg = bundle.getMessage("num-fraction-bad");
425486
assert.strictEqual(

fluent-bundle/test/functions_test.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { FluentBundle } from "../src/bundle.ts";
55
import { FluentResource } from "../src/resource.ts";
66

77
suite("Functions", function () {
8-
let bundle, errs;
8+
/** @type {FluentBundle} */
9+
let bundle;
10+
let errs;
911

1012
beforeEach(function () {
1113
errs = [];
@@ -30,7 +32,7 @@ suite("Functions", function () {
3032
});
3133
});
3234

33-
suite("arguments", function () {
35+
suite("positional arguments", function () {
3436
beforeAll(function () {
3537
bundle = new FluentBundle("en-US", {
3638
useIsolating: false,
@@ -107,4 +109,60 @@ suite("Functions", function () {
107109
assert.strictEqual(errs.length, 0);
108110
});
109111
});
112+
113+
suite("named arguments", function () {
114+
beforeAll(function () {
115+
bundle = new FluentBundle("en-US", {
116+
useIsolating: false,
117+
functions: {
118+
IDENTITY: (args, nargs) => nargs.arg,
119+
},
120+
});
121+
bundle.addResource(
122+
new FluentResource(ftl`
123+
foo = Foo
124+
.attr = Attribute
125+
pass-string = { IDENTITY(arg: "a") }
126+
pass-number = { IDENTITY(arg: 1) }
127+
pass-variable = { IDENTITY(arg: $var) }
128+
pass-message = { IDENTITY(arg: foo) }
129+
pass-attr = { IDENTITY(arg: foo.attr) }
130+
pass-function-call = { IDENTITY(arg: IDENTITY(1)) }
131+
`)
132+
);
133+
});
134+
135+
test("accepts strings", function () {
136+
const msg = bundle.getMessage("pass-string");
137+
const val = bundle.formatPattern(msg.value, undefined, errs);
138+
assert.strictEqual(val, "a");
139+
assert.strictEqual(errs.length, 0);
140+
});
141+
142+
test("accepts numbers", function () {
143+
const msg = bundle.getMessage("pass-number");
144+
const val = bundle.formatPattern(msg.value, undefined, errs);
145+
assert.strictEqual(val, "1");
146+
assert.strictEqual(errs.length, 0);
147+
});
148+
149+
test("accepts variables", function () {
150+
const msg = bundle.getMessage("pass-variable");
151+
const val = bundle.formatPattern(msg.value, { var: "Variable" }, errs);
152+
assert.strictEqual(val, "Variable");
153+
assert.strictEqual(errs.length, 0);
154+
});
155+
156+
test("does not accept entities", function () {
157+
assert.strictEqual(bundle.hasMessage("pass-message"), false);
158+
});
159+
160+
test("does not accept attributes", function () {
161+
assert.strictEqual(bundle.hasMessage("pass-attr"), false);
162+
});
163+
164+
test("does not accept function calls", function () {
165+
assert.strictEqual(bundle.hasMessage("pass-function-call"), false);
166+
});
167+
});
110168
});

fluent-syntax/src/ast.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,9 @@ export class Variant extends SyntaxNode {
390390
export class NamedArgument extends SyntaxNode {
391391
public type = "NamedArgument" as const;
392392
public name: Identifier;
393-
public value: Literal;
393+
public value: Literal | VariableReference;
394394

395-
constructor(name: Identifier, value: Literal) {
395+
constructor(name: Identifier, value: Literal | VariableReference) {
396396
super();
397397
this.name = name;
398398
this.value = value;

fluent-syntax/src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function getErrorMessage(code: string, args: Array<unknown>): string {
5050
case "E0013":
5151
return "Expected variant key";
5252
case "E0014":
53-
return "Expected literal";
53+
return "Expected literal or variable reference";
5454
case "E0015":
5555
return "Only one variant can be marked as default (*)";
5656
case "E0016":

0 commit comments

Comments
 (0)