From 703b67b7e0bb649e1a1047d90de3e53291a9ddae Mon Sep 17 00:00:00 2001 From: Lukas Matejka Date: Thu, 28 Aug 2025 21:29:56 +0200 Subject: [PATCH] Emit INVALID_TOKEN_IN_PLACEHOLDER instead of UNTERMINATED_CLOSING_BRACE when invalid token is in placeholder and update docs --- docs/guide/essentials/syntax.md | 6 +- packages/message-compiler/src/tokenizer.ts | 20 ++++ .../test/tokenizer/named.test.ts | 108 +++++++++++++----- 3 files changed, 103 insertions(+), 31 deletions(-) diff --git a/docs/guide/essentials/syntax.md b/docs/guide/essentials/syntax.md index f000ac6c2..145d908d6 100644 --- a/docs/guide/essentials/syntax.md +++ b/docs/guide/essentials/syntax.md @@ -26,6 +26,10 @@ The locale messages is the resource specified by the `messages` option of `creat 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. +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 (`$`). + +Examples: `{msg}`, `{_userName}`, `{user-id}`, `{total$}` + The following is an example of the use of `$t` in a template: ```html @@ -267,7 +271,7 @@ In `message.greeting`, we use a named interpolation for `{count}` and link to `m The key `message.name` contains `{name}`, which will be interpolated with the passed `name` param. -The `message.greeting` is linked to the locale message key `message.name`. +The `message.greeting` is linked to the locale message key `message.name`. ```html

{{ $t('message.greeting', { name: 'Alice', count: 5 }) }}

diff --git a/packages/message-compiler/src/tokenizer.ts b/packages/message-compiler/src/tokenizer.ts index 0ef63bf88..6f94bfe9b 100644 --- a/packages/message-compiler/src/tokenizer.ts +++ b/packages/message-compiler/src/tokenizer.ts @@ -484,6 +484,26 @@ export function createTokenizer( name += ch } + // Check if takeNamedIdentifierChar stoped because of invalid characters + const currentChar = scnr.currentChar() + if ( + currentChar && + currentChar !== '}' && + currentChar !== EOF && + currentChar !== SPACE && + currentChar !== NEW_LINE && + currentChar !== '\u3000' + ) { + const invalidPart = readInvalidIdentifier(scnr) + emitError( + CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER, + currentPosition(), + 0, + name + invalidPart + ) + return name + invalidPart + } + if (scnr.currentChar() === EOF) { emitError( CompileErrorCodes.UNTERMINATED_CLOSING_BRACE, diff --git a/packages/message-compiler/test/tokenizer/named.test.ts b/packages/message-compiler/test/tokenizer/named.test.ts index c7c3bb3c6..7405000a8 100644 --- a/packages/message-compiler/test/tokenizer/named.test.ts +++ b/packages/message-compiler/test/tokenizer/named.test.ts @@ -2,13 +2,13 @@ import { format } from '@intlify/shared' import { CompileErrorCodes, errorMessages } from '../../src/errors' import { createTokenizer, - TokenTypes, ERROR_DOMAIN, - parse + parse, + TokenTypes } from '../../src/tokenizer' -import type { TokenizeOptions } from '../../src/options' import type { CompileError } from '../../src/errors' +import type { TokenizeOptions } from '../../src/options' test('basic', () => { const tokenizer = createTokenizer('hi {name} !') @@ -645,32 +645,80 @@ describe('errors', () => { } ] as CompileError[]) }) - const items = [`$`, `-`] - for (const ch of items) { - test(`invalid '${ch}' in placeholder`, () => { - parse(`hi {${ch}} !`, options) - expect(errors).toEqual([ - { - code: CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER, - domain: ERROR_DOMAIN, - message: format( - errorMessages[CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER], - ch - ), - location: { - start: { - line: 1, - offset: 4, - column: 5 - }, - end: { - line: 1, - offset: 5, - column: 6 - } - } + + test.each([ + [ + '$', + { + start: { + line: 1, + offset: 4, + column: 5 + }, + end: { + line: 1, + offset: 5, + column: 6 + } + } + ], + [ + '-', + { + start: { + line: 1, + offset: 4, + column: 5 + }, + end: { + line: 1, + offset: 5, + column: 6 + } + } + ], + [ + 'àaa', + { + start: { + line: 1, + offset: 4, + column: 5 + }, + end: { + line: 1, + offset: 7, + column: 8 } - ] as CompileError[]) - }) - } + } + ], + [ + 'aàa', + { + start: { + line: 1, + offset: 4, + column: 5 + }, + end: { + line: 1, + offset: 7, + column: 8 + } + } + ] + ])(`invalid '%s' in placeholder`, (ch, location) => { + parse(`hi {${ch}} !`, options) + expect(errors).toEqual([ + { + code: CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER, + domain: ERROR_DOMAIN, + message: format( + errorMessages[CompileErrorCodes.INVALID_TOKEN_IN_PLACEHOLDER], + ch + ), + location + } + ] as CompileError[]) + }) })