Skip to content
Permalink
Browse files

fix(ivy): i18n - ensure that escaped chars are handled in localized s…

…trings (#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 microsoft/TypeScript#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
  • Loading branch information
petebacondarwin authored and mhevery committed Nov 26, 2019
1 parent 1b4fac1 commit 00f8d6ac6445d6a6135c84858870ec9b56221818
@@ -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.
@@ -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)) {
@@ -956,23 +956,40 @@ describe('i18n support in the template compiler', () => {

it('should properly escape quotes in content', () => {
const input = `
<div i18n>Some text 'with single quotes', "with double quotes" and without quotes.</div>
<div i18n>Some text 'with single quotes', "with double quotes", \`with backticks\` and without quotes.</div>
`;

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 = '<div i18n>`{{ count }}`</div>';
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 = `
<div i18n>My i18n block #1</div>

0 comments on commit 00f8d6a

Please sign in to comment.
You can’t perform that action at this time.