Skip to content

Commit fe3f716

Browse files
kazuponlutejka
andauthored
fix: Emit INVALID_TOKEN_IN_PLACEHOLDER instead of UNTERMINATED_CLOSING_BRACE when invalid token is in placeholder and update docs (#2255)
* Emit INVALID_TOKEN_IN_PLACEHOLDER instead of UNTERMINATED_CLOSING_BRACE when invalid token is in placeholder and update docs (#2252) * chore: drop node v18 --------- Co-authored-by: lutejka <[email protected]>
1 parent bbc15c6 commit fe3f716

File tree

6 files changed

+175
-36
lines changed

6 files changed

+175
-36
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
matrix:
2121
os: [ubuntu-latest]
22-
node: [18]
22+
node: [20]
2323

2424
runs-on: ${{ matrix.os }}
2525

@@ -47,7 +47,7 @@ jobs:
4747
strategy:
4848
matrix:
4949
os: [ubuntu-latest, windows-latest, macos-latest]
50-
node: [18]
50+
node: [20]
5151

5252
runs-on: ${{ matrix.os }}
5353

@@ -83,7 +83,7 @@ jobs:
8383
strategy:
8484
matrix:
8585
os: [ubuntu-latest, windows-latest, macos-latest]
86-
node: [18.19, 20, 22]
86+
node: [20, 22, 24]
8787

8888
runs-on: ${{ matrix.os }}
8989

@@ -116,7 +116,7 @@ jobs:
116116
strategy:
117117
matrix:
118118
os: [ubuntu-latest, windows-latest, macos-latest]
119-
node: [18.19, 20, 22]
119+
node: [20, 22, 24]
120120

121121
runs-on: ${{ matrix.os }}
122122

.github/workflows/nightly-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup node
2424
uses: actions/setup-node@v4
2525
with:
26-
node-version: 18.18
26+
node-version: 20
2727
cache: pnpm
2828

2929
- name: Install dependencies

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Setup Node
2828
uses: actions/setup-node@v4
2929
with:
30-
node-version: 18
30+
node-version: 20
3131

3232
- name: Install dependencies
3333
run: pnpm install --no-frozen-lockfile

docs/guide/essentials/syntax.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ The locale messages is the resource specified by the `messages` option of `creat
2626

2727
Named interpolation allows you to specify variables defined in JavaScript. In the locale message above, you can localize it by giving the JavaScript defined `msg` as a parameter to the translation function.
2828

29+
The variable name inside `{}` must starts with a letter (a-z, A-Z) or an underscore (`_`), followed by any combination of letters, digits, underscores (`_`), hyphens (`-`), or dollar signs (`$`).
30+
31+
Examples: `{msg}`, `{_userName}`, `{user-id}`, `{total$}`
32+
2933
The following is an example of the use of `$t` in a template:
3034

3135
```html
@@ -241,6 +245,73 @@ You can use the interpolations (Named, List, and Literal) for the key of Linked
241245
:::
242246

243247

248+
This example shows the use of modifiers (`@.lower`, `@.upper`, `@.capitalize`) combined with named, list, and literal interpolations.
249+
250+
251+
```js
252+
const messages = {
253+
en: {
254+
message: {
255+
greeting: "Hello, @.lower:{'message.name'}! You have {count} new messages.",
256+
name:"{name}"
257+
},
258+
259+
welcome: "Welcome, @.upper:{'name'}! Today is @.capitalize:{'day'}.",
260+
name: '{0}',
261+
day: '{1}',
262+
263+
literalMessage: "This is an email: foo{'@'}@.lower:domain",
264+
domain: 'SHOUTING'
265+
}
266+
}
267+
```
268+
### Named interpolation with modifier
269+
270+
In `message.greeting`, we use a named interpolation for `{count}` and link to `message.name`, applying the .lower modifier.
271+
272+
The key `message.name` contains `{name}`, which will be interpolated with the passed `name` param.
273+
274+
The `message.greeting` is linked to the locale message key `message.name`.
275+
276+
```html
277+
<p>{{ $t('message.greeting', { name: 'Alice', count: 5 }) }}</p>
278+
```
279+
As result, the below
280+
281+
```html
282+
<p>Hello, alice! You have 5 new messages.</p>
283+
```
284+
285+
### List interpolation with modifier
286+
287+
In this case, the values for `{0}` and `{1}` are passed as an array. The keys `name` and `day` are resolved using list interpolation and transformed with modifiers.
288+
289+
```html
290+
<p>{{ $t('welcome', ['bob', 'MONDAY']) }}</p>
291+
```
292+
293+
As result, the below
294+
295+
```html
296+
<p>Welcome, BOB! Today is Monday.</p>
297+
```
298+
299+
### Literal interpolation with modifier
300+
301+
In this example, we use a literal string inside the message and apply the `.lower` modifier.
302+
303+
```html
304+
<p>{{ $t('literalMessage') }}</p>
305+
```
306+
307+
Here, the modifier is applied to the content inside `domain`, and the `@` is preserved as literal output.
308+
309+
As result, the below
310+
311+
```html
312+
<p>This is an email: foo@shouting</p>
313+
```
314+
244315
## Special Characters
245316

246317
The following characters used in the message format syntax are processed by the compiler as special characters:

packages/message-compiler/src/tokenizer.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,26 @@ export function createTokenizer(
484484
name += ch
485485
}
486486

487+
// Check if takeNamedIdentifierChar stoped because of invalid characters
488+
const currentChar = scnr.currentChar()
489+
if (
490+
currentChar &&
491+
currentChar !== '}' &&
492+
currentChar !== EOF &&
493+
currentChar !== SPACE &&
494+
currentChar !== NEW_LINE &&
495+
currentChar !== '\u3000'
496+
) {
497+
const invalidPart = readInvalidIdentifier(scnr)
498+
emitError(
499+
CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER,
500+
currentPosition(),
501+
0,
502+
name + invalidPart
503+
)
504+
return name + invalidPart
505+
}
506+
487507
if (scnr.currentChar() === EOF) {
488508
emitError(
489509
CompileErrorCodes.UNTERMINATED_CLOSING_BRACE,

packages/message-compiler/test/tokenizer/named.test.ts

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { format } from '@intlify/shared'
22
import { CompileErrorCodes, errorMessages } from '../../src/errors'
33
import {
44
createTokenizer,
5-
TokenTypes,
65
ERROR_DOMAIN,
7-
parse
6+
parse,
7+
TokenTypes
88
} from '../../src/tokenizer'
99

10-
import type { TokenizeOptions } from '../../src/options'
1110
import type { CompileError } from '../../src/errors'
11+
import type { TokenizeOptions } from '../../src/options'
1212

1313
test('basic', () => {
1414
const tokenizer = createTokenizer('hi {name} !')
@@ -645,32 +645,80 @@ describe('errors', () => {
645645
}
646646
] as CompileError[])
647647
})
648-
const items = [`$`, `-`]
649-
for (const ch of items) {
650-
test(`invalid '${ch}' in placeholder`, () => {
651-
parse(`hi {${ch}} !`, options)
652-
expect(errors).toEqual([
653-
{
654-
code: CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER,
655-
domain: ERROR_DOMAIN,
656-
message: format(
657-
errorMessages[CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER],
658-
ch
659-
),
660-
location: {
661-
start: {
662-
line: 1,
663-
offset: 4,
664-
column: 5
665-
},
666-
end: {
667-
line: 1,
668-
offset: 5,
669-
column: 6
670-
}
671-
}
648+
649+
test.each([
650+
[
651+
'$',
652+
{
653+
start: {
654+
line: 1,
655+
offset: 4,
656+
column: 5
657+
},
658+
end: {
659+
line: 1,
660+
offset: 5,
661+
column: 6
662+
}
663+
}
664+
],
665+
[
666+
'-',
667+
{
668+
start: {
669+
line: 1,
670+
offset: 4,
671+
column: 5
672+
},
673+
end: {
674+
line: 1,
675+
offset: 5,
676+
column: 6
677+
}
678+
}
679+
],
680+
[
681+
'àaa',
682+
{
683+
start: {
684+
line: 1,
685+
offset: 4,
686+
column: 5
687+
},
688+
end: {
689+
line: 1,
690+
offset: 7,
691+
column: 8
672692
}
673-
] as CompileError[])
674-
})
675-
}
693+
}
694+
],
695+
[
696+
'aàa',
697+
{
698+
start: {
699+
line: 1,
700+
offset: 4,
701+
column: 5
702+
},
703+
end: {
704+
line: 1,
705+
offset: 7,
706+
column: 8
707+
}
708+
}
709+
]
710+
])(`invalid '%s' in placeholder`, (ch, location) => {
711+
parse(`hi {${ch}} !`, options)
712+
expect(errors).toEqual([
713+
{
714+
code: CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER,
715+
domain: ERROR_DOMAIN,
716+
message: format(
717+
errorMessages[CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER],
718+
ch
719+
),
720+
location
721+
}
722+
] as CompileError[])
723+
})
676724
})

0 commit comments

Comments
 (0)