diff --git a/packages/core-base/src/format.ts b/packages/core-base/src/format.ts index 75dcf3751..947ceff7f 100644 --- a/packages/core-base/src/format.ts +++ b/packages/core-base/src/format.ts @@ -56,7 +56,7 @@ function formatMessageParts( node: MessageNode ): MessageFunctionReturn { const _static = node.s || node.static - if (_static) { + if (_static != null) { return ctx.type === 'text' ? (_static as MessageFunctionReturn) : ctx.normalize([_static] as MessageType[]) diff --git a/packages/core-base/test/format.test.ts b/packages/core-base/test/format.test.ts index 193e87905..aa6794262 100644 --- a/packages/core-base/test/format.test.ts +++ b/packages/core-base/test/format.test.ts @@ -130,3 +130,16 @@ describe('features', () => { }) }) }) + +describe('edge cases', () => { + test('empty string in interpolation', () => { + const { ast } = compile(`{''} | {n} test | {n} tests`, { + jit: true + }) + const msg = format(ast) + const ctx = context({ + pluralIndex: 0 + }) + expect(msg(ctx)).toBe('') + }) +}) diff --git a/packages/message-compiler/test/__snapshots__/compiler.test.ts.snap b/packages/message-compiler/test/__snapshots__/compiler.test.ts.snap index 76abaa3a6..8ffed48ee 100644 --- a/packages/message-compiler/test/__snapshots__/compiler.test.ts.snap +++ b/packages/message-compiler/test/__snapshots__/compiler.test.ts.snap @@ -381,6 +381,15 @@ exports[`edge cases > {_field} with the same value already exists. > code 1`] = }" `; +exports[`edge cases > empty literal string in interpolation > code 1`] = ` +"function __msg__ (ctx) { + const { normalize: _normalize } = ctx + return _normalize([ + "" + ]) +}" +`; + exports[`edge cases > hi %s ! > code 1`] = ` "function __msg__ (ctx) { const { normalize: _normalize } = ctx diff --git a/packages/message-compiler/test/__snapshots__/optimizer.test.ts.snap b/packages/message-compiler/test/__snapshots__/optimizer.test.ts.snap index c408b2ffb..215dfb033 100644 --- a/packages/message-compiler/test/__snapshots__/optimizer.test.ts.snap +++ b/packages/message-compiler/test/__snapshots__/optimizer.test.ts.snap @@ -1,5 +1,20 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`empty literal string in interpolation 1`] = ` +{ + "body": { + "items": [ + { + "type": 9, + }, + ], + "static": "", + "type": 2, + }, + "type": 0, +} +`; + exports[`full text items: foo{'@'}domain.com 1`] = ` { "body": { diff --git a/packages/message-compiler/test/compiler.test.ts b/packages/message-compiler/test/compiler.test.ts index aaa26ff62..e9e50257f 100644 --- a/packages/message-compiler/test/compiler.test.ts +++ b/packages/message-compiler/test/compiler.test.ts @@ -151,4 +151,9 @@ describe('edge cases', () => { const { code } = compile(`%{nickname} %{action} issue %{code}`) expect(code).toMatchSnapshot('code') }) + + test('empty literal string in interpolation', () => { + const { code } = compile(`{''}`) + expect(code).toMatchSnapshot('code') + }) }) diff --git a/packages/message-compiler/test/optimizer.test.ts b/packages/message-compiler/test/optimizer.test.ts index 5349f5196..11293a3df 100644 --- a/packages/message-compiler/test/optimizer.test.ts +++ b/packages/message-compiler/test/optimizer.test.ts @@ -62,3 +62,12 @@ test(`incldue dynamic node in pluarl: no apples | {0} apple | {n} apples`, () => .filter(Boolean) expect(messages).toEqual(['no apples']) }) + +test('empty literal string in interpolation', () => { + const parser = createParser({ location: false }) + const msg = `{''}` + const ast = optimize(parser.parse(msg)) + + expect(ast).toMatchSnapshot() + expect((ast.body as MessageNode).static).toBe('') +}) diff --git a/packages/message-compiler/test/parser/__snapshots__/literal.test.ts.snap b/packages/message-compiler/test/parser/__snapshots__/literal.test.ts.snap index 7ef345aea..06547024d 100644 --- a/packages/message-compiler/test/parser/__snapshots__/literal.test.ts.snap +++ b/packages/message-compiler/test/parser/__snapshots__/literal.test.ts.snap @@ -188,6 +188,64 @@ exports[`emoji > hi, {'😺'} ! 1`] = ` } `; +exports[`empty string 1`] = ` +{ + "body": { + "end": 4, + "items": [ + { + "end": 4, + "loc": { + "end": { + "column": 5, + "line": 1, + "offset": 4, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "start": 0, + "type": 9, + "value": "", + }, + ], + "loc": { + "end": { + "column": 5, + "line": 1, + "offset": 4, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "start": 0, + "type": 2, + }, + "end": 4, + "loc": { + "end": { + "column": 5, + "line": 1, + "offset": 4, + }, + "source": "{''}", + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "start": 0, + "type": 0, +} +`; + exports[`errors > include new line: hi { 'foo\\n' } 1`] = ` { "body": { diff --git a/packages/message-compiler/test/parser/literal.test.ts b/packages/message-compiler/test/parser/literal.test.ts index 1f7c5405a..6f358b337 100644 --- a/packages/message-compiler/test/parser/literal.test.ts +++ b/packages/message-compiler/test/parser/literal.test.ts @@ -152,6 +152,25 @@ describe('unicode', () => { }) }) +test('empty string', () => { + const text = `{''}` + const parser = createParser({ onError: spy }) + const ast = parser.parse(text) + + expect(ast).toMatchSnapshot() + expect(spy).not.toHaveBeenCalled() + expect(ast.type).toEqual(NodeTypes.Resource) + expect(ast.body.type).toEqual(NodeTypes.Message) + const message = ast.body as MessageNode + expect(message.items).toHaveLength(1) + expect(message.items).toMatchObject([ + { + type: NodeTypes.Literal, + value: '' + } + ]) +}) + describe('intlify message syntax special characters', () => { const items = ['{', '}', '@', '|', '%'] for (const ch of items) { diff --git a/packages/message-compiler/test/tokenizer/literal.test.ts b/packages/message-compiler/test/tokenizer/literal.test.ts index f49e243f8..ccbb77161 100644 --- a/packages/message-compiler/test/tokenizer/literal.test.ts +++ b/packages/message-compiler/test/tokenizer/literal.test.ts @@ -604,6 +604,41 @@ describe('escapes', () => { } }) }) + + test('empty string', () => { + const tokenizer = createTokenizer(`{''}`) + expect(tokenizer.nextToken()).toEqual({ + type: TokenTypes.BraceLeft, + value: '{', + loc: { + start: { line: 1, column: 1, offset: 0 }, + end: { line: 1, column: 2, offset: 1 } + } + }) + expect(tokenizer.nextToken()).toEqual({ + type: TokenTypes.Literal, + value: '', + loc: { + start: { line: 1, column: 2, offset: 1 }, + end: { line: 1, column: 4, offset: 3 } + } + }) + expect(tokenizer.nextToken()).toEqual({ + type: TokenTypes.BraceRight, + value: '}', + loc: { + start: { line: 1, column: 4, offset: 3 }, + end: { line: 1, column: 5, offset: 4 } + } + }) + expect(tokenizer.nextToken()).toEqual({ + type: TokenTypes.EOF, + loc: { + start: { line: 1, column: 5, offset: 4 }, + end: { line: 1, column: 5, offset: 4 } + } + }) + }) }) describe('errors', () => { diff --git a/packages/vue-i18n-core/test/issues.test.ts b/packages/vue-i18n-core/test/issues.test.ts index 2b3be11e0..552aa9dcb 100644 --- a/packages/vue-i18n-core/test/issues.test.ts +++ b/packages/vue-i18n-core/test/issues.test.ts @@ -1405,3 +1405,16 @@ test('#1912', async () => { expect(el?.innerHTML).include(`No apples found`) }) + +test('#1972', async () => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: { + test: "{''} | {n} test | {n} tests" + } + } + }) + expect(i18n.global.t('test', 0)).toEqual('') +})