diff --git a/integration/cli-hello-world-ivy-i18n/src/app/app.component.html b/integration/cli-hello-world-ivy-i18n/src/app/app.component.html index 4e22afcce0..b181126918 100644 --- a/integration/cli-hello-world-ivy-i18n/src/app/app.component.html +++ b/integration/cli-hello-world-ivy-i18n/src/app/app.component.html @@ -1,4 +1,4 @@ -

Hello {{ title }}!

+

Hello {{ title }}!

{{ locale }}

{{ 1 | percent }} awesome

{{ jan | date : 'LLLL' }}

diff --git a/integration/ivy-i18n/src/app/app.component.html b/integration/ivy-i18n/src/app/app.component.html index 7bee9429e1..eb7616861d 100644 --- a/integration/ivy-i18n/src/app/app.component.html +++ b/integration/ivy-i18n/src/app/app.component.html @@ -1,6 +1,6 @@
-

+

Hello {{ title }}!

diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index c4462fdbbc..63f43730c3 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -549,9 +549,9 @@ function visitLocalizedString(ast: LocalizedString, context: Context, visitor: E let template: ts.TemplateLiteral; const metaBlock = serializeI18nHead(ast.metaBlock, ast.messageParts[0]); if (ast.messageParts.length === 1) { - template = ts.createNoSubstitutionTemplateLiteral(metaBlock); + template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw); } else { - const head = ts.createTemplateHead(metaBlock); + const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw); const spans: ts.TemplateSpan[] = []; for (let i = 1; i < ast.messageParts.length; i++) { const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index bcb1f76c97..9614315155 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -264,7 +264,7 @@ describe('i18n support in the template compiler', () => { $I18N_23$ = $MSG_EXTERNAL_idG$$APP_SPEC_TS_24$; } else { - $I18N_23$ = $localize \`:[BACKUP_MESSAGE_ID\\:idH]desc@@idG:Title G\`; + $I18N_23$ = $localize \`:[BACKUP_MESSAGE_ID\:idH]desc@@idG:Title G\`; } const $_c25$ = ["title", $I18N_23$]; … diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index b5671ffe81..9c505c9d18 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -364,7 +364,7 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex visitLocalizedString(ast: o.LocalizedString, ctx: EmitterVisitorContext): any { const head = serializeI18nHead(ast.metaBlock, ast.messageParts[0]); - ctx.print(ast, '$localize `' + escapeBackticks(head)); + ctx.print(ast, '$localize `' + escapeBackticks(head.raw)); for (let i = 1; i < ast.messageParts.length; i++) { ctx.print(ast, '${'); ast.expressions[i - 1].visitExpression(this, ctx); diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index 2e10fe02f4..0b78536955 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -235,13 +235,15 @@ export function parseI18nMeta(meta?: string): I18nMeta { } /** - * Serialize the given `meta` and `messagePart` a string that can be used in a `$localize` - * tagged string. The format of the metadata is the same as that parsed by `parseI18nMeta()`. + * Serialize the given `meta` and `messagePart` "cooked" and "raw" strings that can be used in a + * `$localize` tagged string. The format of the metadata is the same as that parsed by + * `parseI18nMeta()`. * * @param meta The metadata to serialize * @param messagePart The first part of the tagged string */ -export function serializeI18nHead(meta: I18nMeta, messagePart: string): string { +export function serializeI18nHead( + meta: I18nMeta, messagePart: string): {cooked: string, raw: string} { let metaBlock = meta.description || ''; if (meta.meaning) { metaBlock = `${meta.meaning}|${metaBlock}`; @@ -251,9 +253,12 @@ export function serializeI18nHead(meta: I18nMeta, messagePart: string): string { } if (metaBlock === '') { // There is no metaBlock, so we must ensure that any starting colon is escaped. - return escapeStartingColon(messagePart); + return {cooked: messagePart, raw: escapeStartingColon(messagePart)}; } else { - return `:${escapeColons(metaBlock)}:${messagePart}`; + return { + cooked: `:${metaBlock}:${messagePart}`, + raw: `:${escapeColons(metaBlock)}:${messagePart}` + }; } } diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts index b9b77c1ca3..d446349e73 100644 --- a/packages/compiler/test/render3/view/i18n_spec.ts +++ b/packages/compiler/test/render3/view/i18n_spec.ts @@ -210,26 +210,38 @@ describe('Utils', () => { }); it('serializeI18nHead()', () => { - expect(serializeI18nHead(meta(), '')).toEqual(''); - expect(serializeI18nHead(meta('', '', 'desc'), '')).toEqual(':desc:'); - expect(serializeI18nHead(meta('id', '', 'desc'), '')).toEqual(':desc@@id:'); - expect(serializeI18nHead(meta('', 'meaning', 'desc'), '')).toEqual(':meaning|desc:'); - expect(serializeI18nHead(meta('id', 'meaning', 'desc'), '')).toEqual(':meaning|desc@@id:'); - expect(serializeI18nHead(meta('id', '', ''), '')).toEqual(':@@id:'); + expect(serializeI18nHead(meta(), '')).toEqual({cooked: '', raw: ''}); + expect(serializeI18nHead(meta('', '', 'desc'), '')) + .toEqual({cooked: ':desc:', raw: ':desc:'}); + expect(serializeI18nHead(meta('id', '', 'desc'), '')) + .toEqual({cooked: ':desc@@id:', raw: ':desc@@id:'}); + expect(serializeI18nHead(meta('', 'meaning', 'desc'), '')) + .toEqual({cooked: ':meaning|desc:', raw: ':meaning|desc:'}); + expect(serializeI18nHead(meta('id', 'meaning', 'desc'), '')) + .toEqual({cooked: ':meaning|desc@@id:', raw: ':meaning|desc@@id:'}); + expect(serializeI18nHead(meta('id', '', ''), '')).toEqual({cooked: ':@@id:', raw: ':@@id:'}); // Escaping colons (block markers) expect(serializeI18nHead(meta('id:sub_id', 'meaning', 'desc'), '')) - .toEqual(':meaning|desc@@id\\:sub_id:'); - expect(serializeI18nHead(meta('id', 'meaning:sub_meaning', 'desc'), '')) - .toEqual(':meaning\\:sub_meaning|desc@@id:'); + .toEqual({cooked: ':meaning|desc@@id:sub_id:', raw: ':meaning|desc@@id\\:sub_id:'}); + expect(serializeI18nHead(meta('id', 'meaning:sub_meaning', 'desc'), '')).toEqual({ + cooked: ':meaning:sub_meaning|desc@@id:', + raw: ':meaning\\:sub_meaning|desc@@id:' + }); expect(serializeI18nHead(meta('id', 'meaning', 'desc:sub_desc'), '')) - .toEqual(':meaning|desc\\:sub_desc@@id:'); - expect(serializeI18nHead(meta('id', 'meaning', 'desc'), 'message source')) - .toEqual(':meaning|desc@@id:message source'); - expect(serializeI18nHead(meta('id', 'meaning', 'desc'), ':message source')) - .toEqual(':meaning|desc@@id::message source'); - expect(serializeI18nHead(meta('', '', ''), 'message source')).toEqual('message source'); - expect(serializeI18nHead(meta('', '', ''), ':message source')).toEqual('\\:message source'); + .toEqual({cooked: ':meaning|desc:sub_desc@@id:', raw: ':meaning|desc\\:sub_desc@@id:'}); + expect(serializeI18nHead(meta('id', 'meaning', 'desc'), 'message source')).toEqual({ + cooked: ':meaning|desc@@id:message source', + raw: ':meaning|desc@@id:message source' + }); + expect(serializeI18nHead(meta('id', 'meaning', 'desc'), ':message source')).toEqual({ + cooked: ':meaning|desc@@id::message source', + raw: ':meaning|desc@@id::message source' + }); + expect(serializeI18nHead(meta('', '', ''), 'message source')) + .toEqual({cooked: 'message source', raw: 'message source'}); + expect(serializeI18nHead(meta('', '', ''), ':message source')) + .toEqual({cooked: ':message source', raw: '\\:message source'}); }); it('serializeI18nPlaceholderBlock()', () => {