From ee7857300bee6e3ab9f2dd301f2ff81d5284ef70 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 26 Nov 2019 14:03:19 +0000 Subject: [PATCH] fix(ivy): i18n - ensure that escaped chars are handled in localized strings (#34065) When creating synthesized tagged template literals, one must provide both the "cooked" text and the "raw" (unparsed) text. Previously there were no good APIs for creating the AST nodes with raw text for such literals. Recently the APIs were improved to support this, and they do an extra check to ensure that the raw text parses to be equal to the cooked text. It turns out there is a bug in this check - see https://github.com/microsoft/TypeScript/issues/35374. This commit works around the bug by synthesizing a "head" node and morphing it by changing its `kind` into the required node type. // FW-1747 PR Close #34065 --- .../src/ngtsc/translator/src/translator.ts | 36 +++++++++++++++---- .../test/compliance/mock_compile.ts | 2 +- .../compliance/r3_view_compiler_i18n_spec.ts | 23 ++++++++++-- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 77456f8677..b951c820b5 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -552,27 +552,49 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { function createLocalizedStringTaggedTemplate( ast: LocalizedString, context: Context, visitor: ExpressionVisitor) { let template: ts.TemplateLiteral; + const length = ast.messageParts.length; const metaBlock = ast.serializeI18nHead(); - if (ast.messageParts.length === 1) { + if (length === 1) { template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw); } else { + // Create the head part const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw); const spans: ts.TemplateSpan[] = []; - for (let i = 1; i < ast.messageParts.length; i++) { + // Create the middle parts + for (let i = 1; i < length - 1; i++) { const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context); const templatePart = ast.serializeI18nTemplatePart(i); - const templateMiddle = ts.createTemplateMiddle(templatePart.cooked, templatePart.raw); + const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw); spans.push(ts.createTemplateSpan(resolvedExpression, templateMiddle)); } - if (spans.length > 0) { - // The last span is supposed to have a tail rather than a middle - spans[spans.length - 1].literal.kind = ts.SyntaxKind.TemplateTail; - } + // Create the tail part + const resolvedExpression = ast.expressions[length - 2].visitExpression(visitor, context); + const templatePart = ast.serializeI18nTemplatePart(length - 1); + const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw); + spans.push(ts.createTemplateSpan(resolvedExpression, templateTail)); + // Put it all together template = ts.createTemplateExpression(head, spans); } return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template); } + +// HACK: Use this in place of `ts.createTemplateMiddle()`. +// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed +function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle { + const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw); + node.kind = ts.SyntaxKind.TemplateMiddle; + return node as ts.TemplateMiddle; +} + +// HACK: Use this in place of `ts.createTemplateTail()`. +// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed +function createTemplateTail(cooked: string, raw: string): ts.TemplateTail { + const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw); + node.kind = ts.SyntaxKind.TemplateTail; + return node as ts.TemplateTail; +} + /** * Translate the `LocalizedString` node into a `$localize` call using the imported * `__makeTemplateObject` helper for ES5 formatted output. diff --git a/packages/compiler-cli/test/compliance/mock_compile.ts b/packages/compiler-cli/test/compliance/mock_compile.ts index 859451d81d..6d55d1a376 100644 --- a/packages/compiler-cli/test/compliance/mock_compile.ts +++ b/packages/compiler-cli/test/compliance/mock_compile.ts @@ -78,7 +78,7 @@ function tokenizeBackTickString(str: string): Piece[] { const pieces: Piece[] = ['`']; // Unescape backticks that are inside the backtick string // (we had to double escape them in the test string so they didn't look like string markers) - str = str.replace(/\\\\\\`/, '\\`'); + str = str.replace(/\\\\\\`/g, '\\`'); const backTickPieces = str.slice(2, -2).split(BACKTICK_INTERPOLATION); backTickPieces.forEach((backTickPiece) => { if (BACKTICK_INTERPOLATION.test(backTickPiece)) { 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 30002b1b0b..b1af024abd 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 @@ -956,23 +956,40 @@ describe('i18n support in the template compiler', () => { it('should properly escape quotes in content', () => { const input = ` -
Some text 'with single quotes', "with double quotes" and without quotes.
+
Some text 'with single quotes', "with double quotes", \`with backticks\` and without quotes.
`; const output = String.raw ` var $I18N_0$; if (ngI18nClosureMode) { - const $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$ = goog.getMsg("Some text 'with single quotes', \"with double quotes\" and without quotes."); + const $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$ = goog.getMsg("Some text 'with single quotes', \"with double quotes\", ` + + '`with backticks`' + String.raw ` and without quotes."); $I18N_0$ = $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$; } else { - $I18N_0$ = $localize \`Some text 'with single quotes', "with double quotes" and without quotes.\`; + $I18N_0$ = $localize \`Some text 'with single quotes', "with double quotes", \\\`with backticks\\\` and without quotes.\`; } `; verify(input, output); }); + it('should handle interpolations wrapped in backticks', () => { + const input = '
`{{ count }}`
'; + const output = String.raw ` + var $I18N_0$; + if (ngI18nClosureMode) { + const $MSG_APP_SPEC_TS_1$ = goog.getMsg("` + + '`{$interpolation}`' + String.raw `", { "interpolation": "\uFFFD0\uFFFD" }); + $I18N_0$ = $MSG_APP_SPEC_TS_1$; + } + else { + $I18N_0$ = $localize \`\\\`$` + + String.raw `{"\uFFFD0\uFFFD"}:INTERPOLATION:\\\`\`; + }`; + verify(input, output); + }); + it('should handle i18n attributes with plain-text content', () => { const input = `
My i18n block #1