fix(ivy): update ICU placeholders format to match Closure compiler (#31459)
Since `goog.getMsg` does not process ICUs (post-processing is required via goog.i18n.MessageFormat, https://google.github.io/closure-library/api/goog.i18n.MessageFormat.html) and placeholder format used for ICUs and regular messages inside `goog.getMsg` are different, the current implementation (that assumed the same placeholder format) needs to be updated. This commit updates placeholder format used inside ICUs from `{$placeholder}` to `{PLACEHOLDER}` to better align with Closure. ICU placeholders (that were left as is prior to this commit) are now replaced with actual values in post-processing step (inside `i18nPostprocess`). PR Close #31459
This commit is contained in:

committed by
Matias Niemelä

parent
6da1446afc
commit
dee16a4355
@ -10,13 +10,26 @@ import * as i18n from '../../../i18n/i18n_ast';
|
||||
|
||||
import {formatI18nPlaceholderName} from './util';
|
||||
|
||||
const formatPh = (value: string): string => `{$${formatI18nPlaceholderName(value)}}`;
|
||||
|
||||
/**
|
||||
* This visitor walks over i18n tree and generates its string representation,
|
||||
* including ICUs and placeholders in {$PLACEHOLDER} format.
|
||||
* This visitor walks over i18n tree and generates its string representation, including ICUs and
|
||||
* placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format.
|
||||
*/
|
||||
class SerializerVisitor implements i18n.Visitor {
|
||||
/**
|
||||
* Flag that indicates that we are processing elements of an ICU.
|
||||
*
|
||||
* This flag is needed due to the fact that placeholders in ICUs and in other messages are
|
||||
* represented differently in Closure:
|
||||
* - {$placeholder} in non-ICU case
|
||||
* - {PLACEHOLDER} inside ICU
|
||||
*/
|
||||
private insideIcu = false;
|
||||
|
||||
private formatPh(value: string): string {
|
||||
const formatted = formatI18nPlaceholderName(value, /* useCamelCase */ !this.insideIcu);
|
||||
return this.insideIcu ? `{${formatted}}` : `{$${formatted}}`;
|
||||
}
|
||||
|
||||
visitText(text: i18n.Text, context: any): any { return text.value; }
|
||||
|
||||
visitContainer(container: i18n.Container, context: any): any {
|
||||
@ -24,20 +37,25 @@ class SerializerVisitor implements i18n.Visitor {
|
||||
}
|
||||
|
||||
visitIcu(icu: i18n.Icu, context: any): any {
|
||||
this.insideIcu = true;
|
||||
const strCases =
|
||||
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||
return `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||
const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||
this.insideIcu = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
||||
return ph.isVoid ?
|
||||
formatPh(ph.startName) :
|
||||
`${formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${formatPh(ph.closeName)}`;
|
||||
this.formatPh(ph.startName) :
|
||||
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return formatPh(ph.name); }
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return this.formatPh(ph.name); }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { return formatPh(ph.name); }
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
return this.formatPh(ph.name);
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new SerializerVisitor();
|
||||
|
@ -220,8 +220,12 @@ export function parseI18nMeta(meta?: string): I18nMeta {
|
||||
* @param name The placeholder name that should be formatted
|
||||
* @returns Formatted placeholder name
|
||||
*/
|
||||
export function formatI18nPlaceholderName(name: string): string {
|
||||
const chunks = toPublicName(name).split('_');
|
||||
export function formatI18nPlaceholderName(name: string, useCamelCase: boolean = true): string {
|
||||
const publicName = toPublicName(name);
|
||||
if (!useCamelCase) {
|
||||
return publicName;
|
||||
}
|
||||
const chunks = publicName.split('_');
|
||||
if (chunks.length === 1) {
|
||||
// if no "_" found - just lowercase the value
|
||||
return name.toLowerCase();
|
||||
|
@ -324,18 +324,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
|
||||
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
|
||||
const closureVar = this.i18nGenerateClosureVar(message.id);
|
||||
const _params: {[key: string]: any} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
|
||||
}
|
||||
const formattedParams = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ true);
|
||||
const meta = metaFromI18nMessage(message);
|
||||
const content = getSerializedI18nContent(message);
|
||||
const statements =
|
||||
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
|
||||
getTranslationDeclStmts(_ref, closureVar, content, meta, formattedParams, transformFn);
|
||||
this.constantPool.statements.push(...statements);
|
||||
return _ref;
|
||||
}
|
||||
|
||||
i18nFormatPlaceholderNames(params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) {
|
||||
const _params: {[key: string]: o.Expression} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(
|
||||
key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]);
|
||||
}
|
||||
return _params;
|
||||
}
|
||||
|
||||
i18nAppendBindings(expressions: AST[]) {
|
||||
if (expressions.length > 0) {
|
||||
expressions.forEach(expression => this.i18n !.appendBinding(expression));
|
||||
@ -994,17 +1000,29 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
// output ICU directly and keep ICU reference in context
|
||||
const message = icu.i18n !as i18n.Message;
|
||||
const transformFn = (raw: o.ReadVarExpr) =>
|
||||
instruction(null, R3.i18nPostprocess, [raw, mapLiteral(vars, true)]);
|
||||
|
||||
// we always need post-processing function for ICUs, to make sure that:
|
||||
// - all placeholders in a form of {PLACEHOLDER} are replaced with actual values (note:
|
||||
// `goog.getMsg` does not process ICUs and uses the `{PLACEHOLDER}` format for placeholders
|
||||
// inside ICUs)
|
||||
// - all ICU vars (such as `VAR_SELECT` or `VAR_PLURAL`) are replaced with correct values
|
||||
const transformFn = (raw: o.ReadVarExpr) => {
|
||||
const params = {...vars, ...placeholders};
|
||||
const formatted = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ false);
|
||||
return instruction(null, R3.i18nPostprocess, [raw, mapLiteral(formatted, true)]);
|
||||
};
|
||||
|
||||
// in case the whole i18n message is a single ICU - we do not need to
|
||||
// create a separate top-level translation, we can use the root ref instead
|
||||
// and make this ICU a top-level translation
|
||||
// note: ICU placeholders are replaced with actual values in `i18nPostprocess` function
|
||||
// separately, so we do not pass placeholders into `i18nTranslate` function.
|
||||
if (isSingleI18nIcu(i18n.meta)) {
|
||||
this.i18nTranslate(message, placeholders, i18n.ref, transformFn);
|
||||
this.i18nTranslate(message, /* placeholders */ {}, i18n.ref, transformFn);
|
||||
} else {
|
||||
// output ICU directly and keep ICU reference in context
|
||||
const ref = this.i18nTranslate(message, placeholders, undefined, transformFn);
|
||||
const ref =
|
||||
this.i18nTranslate(message, /* placeholders */ {}, /* ref */ undefined, transformFn);
|
||||
i18n.appendIcu(icuFromI18nMessage(message).name, ref);
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,7 @@ describe('Serializer', () => {
|
||||
// ICU with nested HTML
|
||||
[
|
||||
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
|
||||
'{VAR_PLURAL, plural, 10 {{$startBoldText}ten{$closeBoldText}} other {{$startTagDiv}other{$closeTagDiv}}}'
|
||||
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'
|
||||
]
|
||||
];
|
||||
|
||||
|
Reference in New Issue
Block a user