fix(ivy): i18n - support "\", "`" and "${" sequences in i18n messages (#33820)
Since i18n messages are mapped to `$localize` tagged template strings, the "raw" version must be properly escaped. Otherwise TS will throw an error such as: ``` Error: Debug Failure. False expression: Expected argument 'text' to be the normalized (i.e. 'cooked') version of argument 'rawText'. ``` This commit ensures that we properly escape these raw strings before creating TS AST nodes from them. PR Close #33820
This commit is contained in:

committed by
Alex Rickabaugh

parent
bc28ca7b0c
commit
b53a1ac999
@ -7,8 +7,6 @@
|
||||
*/
|
||||
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
import {serializeI18nHead, serializeI18nTemplatePart} from '../render3/view/i18n/meta';
|
||||
|
||||
import * as o from './output_ast';
|
||||
import {SourceMapGenerator} from './source_map';
|
||||
|
||||
@ -363,14 +361,12 @@ 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.raw));
|
||||
const head = ast.serializeI18nHead();
|
||||
ctx.print(ast, '$localize `' + head.raw);
|
||||
for (let i = 1; i < ast.messageParts.length; i++) {
|
||||
ctx.print(ast, '${');
|
||||
ast.expressions[i - 1].visitExpression(this, ctx);
|
||||
ctx.print(
|
||||
ast,
|
||||
`}${escapeBackticks(serializeI18nTemplatePart(ast.placeHolderNames[i - 1], ast.messageParts[i]))}`);
|
||||
ctx.print(ast, `}${ast.serializeI18nTemplatePart(i).raw}`);
|
||||
}
|
||||
ctx.print(ast, '`');
|
||||
return null;
|
||||
@ -560,7 +556,3 @@ function _createIndent(count: number): string {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function escapeBackticks(str: string): string {
|
||||
return str.replace(/`/g, '\\`');
|
||||
}
|
||||
|
@ -499,8 +499,74 @@ export class LocalizedString extends Expression {
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitLocalizedString(this, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the given `meta` and `messagePart` into "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
|
||||
*/
|
||||
serializeI18nHead(): {cooked: string, raw: string} {
|
||||
let metaBlock = this.metaBlock.description || '';
|
||||
if (this.metaBlock.meaning) {
|
||||
metaBlock = `${this.metaBlock.meaning}|${metaBlock}`;
|
||||
}
|
||||
if (this.metaBlock.customId || this.metaBlock.legacyId) {
|
||||
metaBlock = `${metaBlock}@@${this.metaBlock.customId || this.metaBlock.legacyId}`;
|
||||
}
|
||||
return createCookedRawString(metaBlock, this.messageParts[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the given `placeholderName` and `messagePart` into "cooked" and "raw" strings that
|
||||
* can be used in a `$localize` tagged string.
|
||||
*
|
||||
* @param placeholderName The placeholder name to serialize
|
||||
* @param messagePart The following message string after this placeholder
|
||||
*/
|
||||
serializeI18nTemplatePart(partIndex: number): {cooked: string, raw: string} {
|
||||
const placeholderName = this.placeHolderNames[partIndex - 1];
|
||||
const messagePart = this.messageParts[partIndex];
|
||||
return createCookedRawString(placeholderName, messagePart);
|
||||
}
|
||||
}
|
||||
|
||||
const escapeSlashes = (str: string): string => str.replace(/\\/g, '\\\\');
|
||||
const escapeStartingColon = (str: string): string => str.replace(/^:/, '\\:');
|
||||
const escapeColons = (str: string): string => str.replace(/:/g, '\\:');
|
||||
const escapeForMessagePart = (str: string): string =>
|
||||
str.replace(/`/g, '\\`').replace(/\${/g, '$\\{');
|
||||
|
||||
/**
|
||||
* Creates a `{cooked, raw}` object from the `metaBlock` and `messagePart`.
|
||||
*
|
||||
* The `raw` text must have various character sequences escaped:
|
||||
* * "\" would otherwise indicate that the next character is a control character.
|
||||
* * "`" and "${" are template string control sequences that would otherwise prematurely indicate
|
||||
* the end of a message part.
|
||||
* * ":" inside a metablock would prematurely indicate the end of the metablock.
|
||||
* * ":" at the start of a messagePart with no metablock would erroneously indicate the start of a
|
||||
* metablock.
|
||||
*
|
||||
* @param metaBlock Any metadata that should be prepended to the string
|
||||
* @param messagePart The message part of the string
|
||||
*/
|
||||
function createCookedRawString(metaBlock: string, messagePart: string) {
|
||||
if (metaBlock === '') {
|
||||
return {
|
||||
cooked: messagePart,
|
||||
raw: escapeForMessagePart(escapeStartingColon(escapeSlashes(messagePart)))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
cooked: `:${metaBlock}:${messagePart}`,
|
||||
raw: escapeForMessagePart(
|
||||
`:${escapeColons(escapeSlashes(metaBlock))}:${escapeSlashes(messagePart)}`)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ExternalExpr extends Expression {
|
||||
constructor(
|
||||
|
@ -234,50 +234,6 @@ export function parseI18nMeta(meta?: string): I18nMeta {
|
||||
return {customId, meaning, description};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): {cooked: string, raw: string} {
|
||||
let metaBlock = meta.description || '';
|
||||
if (meta.meaning) {
|
||||
metaBlock = `${meta.meaning}|${metaBlock}`;
|
||||
}
|
||||
if (meta.customId || meta.legacyId) {
|
||||
metaBlock = `${metaBlock}@@${meta.customId || meta.legacyId}`;
|
||||
}
|
||||
if (metaBlock === '') {
|
||||
// There is no metaBlock, so we must ensure that any starting colon is escaped.
|
||||
return {cooked: messagePart, raw: escapeStartingColon(messagePart)};
|
||||
} else {
|
||||
return {
|
||||
cooked: `:${metaBlock}:${messagePart}`,
|
||||
raw: `:${escapeColons(metaBlock)}:${messagePart}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the given `placeholderName` and `messagePart` into strings that can be used in a
|
||||
* `$localize` tagged string.
|
||||
*
|
||||
* @param placeholderName The placeholder name to serialize
|
||||
* @param messagePart The following message string after this placeholder
|
||||
*/
|
||||
export function serializeI18nTemplatePart(placeholderName: string, messagePart: string): string {
|
||||
if (placeholderName === '') {
|
||||
// There is no placeholder name block, so we must ensure that any starting colon is escaped.
|
||||
return escapeStartingColon(messagePart);
|
||||
} else {
|
||||
return `:${placeholderName}:${messagePart}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts i18n meta information for a message (id, description, meaning)
|
||||
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||
export function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
|
||||
@ -290,11 +246,3 @@ export function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
|
||||
}
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
|
||||
export function escapeStartingColon(str: string): string {
|
||||
return str.replace(/^:/, '\\:');
|
||||
}
|
||||
|
||||
export function escapeColons(str: string): string {
|
||||
return str.replace(/:/g, '\\:');
|
||||
}
|
Reference in New Issue
Block a user