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