fix(core): handle empty translations correctly (#36499)
In certain use-cases it's useful to have an ability to use empty strings as translations. Currently Ivy fails at runtime if empty string is used as a translation, since some parts of internal data structures are not created properly. This commit updates runtime i18n logic to handle empty translations and avoid unnecessary extra processing for such cases. Fixes #36476. PR Close #36499
This commit is contained in:
parent
03ff380e13
commit
b1f1d3ffd2
@ -243,7 +243,7 @@ function removeInnerTemplateTranslation(message: string): string {
|
||||
* external template and removes all sub-templates.
|
||||
*/
|
||||
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
|
||||
if (typeof subTemplateIndex !== 'number') {
|
||||
if (isRootTemplateMessage(subTemplateIndex)) {
|
||||
// We want the root template message, ignore all sub-templates
|
||||
return removeInnerTemplateTranslation(message);
|
||||
} else {
|
||||
@ -376,6 +376,10 @@ export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?:
|
||||
// This is reset to 0 when `i18nStartFirstPass` is called.
|
||||
let i18nVarsCount: number;
|
||||
|
||||
function allocNodeIndex(startIndex: number): number {
|
||||
return startIndex + i18nVarsCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* See `i18nStart` above.
|
||||
*/
|
||||
@ -409,6 +413,13 @@ function i18nStartFirstPass(
|
||||
const updateOpCodes: I18nUpdateOpCodes = [];
|
||||
const icuExpressions: TIcu[] = [];
|
||||
|
||||
if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
|
||||
// If top level translation is an empty string, do not invoke additional processing
|
||||
// and just create op codes for empty text node instead.
|
||||
createOpCodes.push(
|
||||
message, allocNodeIndex(startIndex),
|
||||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||||
} else {
|
||||
const templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
|
||||
const msgParts = replaceNgsp(templateTranslation).split(PH_REGEXP);
|
||||
for (let i = 0; i < msgParts.length; i++) {
|
||||
@ -426,8 +437,8 @@ function i18nStartFirstPass(
|
||||
const phIndex = parseInt(value.substr(1), 10);
|
||||
const isElement = value.charAt(0) === TagType.ELEMENT;
|
||||
// The value represents a placeholder that we move to the designated index.
|
||||
// Note: positive indicies indicate that a TNode with a given index should also be marked as
|
||||
// parent while executing `Select` instruction.
|
||||
// Note: positive indicies indicate that a TNode with a given index should also be marked
|
||||
// as parent while executing `Select` instruction.
|
||||
createOpCodes.push(
|
||||
(isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
|
||||
I18nMutateOpCode.Select,
|
||||
@ -446,14 +457,15 @@ function i18nStartFirstPass(
|
||||
const icuExpression = parts[j] as IcuExpression;
|
||||
|
||||
// Verify that ICU expression has the right shape. Translations might contain invalid
|
||||
// constructions (while original messages were correct), so ICU parsing at runtime may not
|
||||
// succeed (thus `icuExpression` remains a string).
|
||||
// constructions (while original messages were correct), so ICU parsing at runtime may
|
||||
// not succeed (thus `icuExpression` remains a string).
|
||||
if (typeof icuExpression !== 'object') {
|
||||
throw new Error(`Unable to parse ICU expression in "${templateTranslation}" message.`);
|
||||
throw new Error(
|
||||
`Unable to parse ICU expression in "${templateTranslation}" message.`);
|
||||
}
|
||||
|
||||
// Create the comment node that will anchor the ICU expression
|
||||
const icuNodeIndex = startIndex + i18nVarsCount++;
|
||||
const icuNodeIndex = allocNodeIndex(startIndex);
|
||||
createOpCodes.push(
|
||||
COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
|
||||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||||
@ -476,7 +488,7 @@ function i18nStartFirstPass(
|
||||
// Even indexes are text (including bindings)
|
||||
const hasBinding = text.match(BINDING_REGEXP);
|
||||
// Create text nodes
|
||||
const textNodeIndex = startIndex + i18nVarsCount++;
|
||||
const textNodeIndex = allocNodeIndex(startIndex);
|
||||
createOpCodes.push(
|
||||
// If there is a binding, the value will be set during update
|
||||
hasBinding ? '' : text, textNodeIndex,
|
||||
@ -489,6 +501,7 @@ function i18nStartFirstPass(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (i18nVarsCount > 0) {
|
||||
allocExpando(tView, lView, i18nVarsCount);
|
||||
@ -558,6 +571,10 @@ function appendI18nNode(
|
||||
return tNode;
|
||||
}
|
||||
|
||||
function isRootTemplateMessage(subTemplateIndex: number|undefined): subTemplateIndex is undefined {
|
||||
return subTemplateIndex === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles message string post-processing for internationalization.
|
||||
*
|
||||
|
@ -1611,6 +1611,41 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty translations', () => {
|
||||
it('should replace existing text content with empty translation', () => {
|
||||
loadTranslations({[computeMsgId('Some Text')]: ''});
|
||||
const fixture = initWithTemplate(AppComp, '<div i18n>Some Text</div>');
|
||||
expect(fixture.nativeElement.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should replace existing DOM elements with empty translation', () => {
|
||||
loadTranslations({
|
||||
[computeMsgId(
|
||||
' Start {$START_TAG_DIV}DIV{$CLOSE_TAG_DIV}' +
|
||||
'{$START_TAG_SPAN}SPAN{$CLOSE_TAG_SPAN} End ')]: '',
|
||||
});
|
||||
const fixture = initWithTemplate(AppComp, `
|
||||
<div i18n>
|
||||
Start
|
||||
<div>DIV</div>
|
||||
<span>SPAN</span>
|
||||
End
|
||||
</div>
|
||||
`);
|
||||
expect(fixture.nativeElement.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('should replace existing ICU content with empty translation', () => {
|
||||
loadTranslations({
|
||||
[computeMsgId('{VAR_PLURAL, plural, =0 {zero} other {more than zero}}')]: '',
|
||||
});
|
||||
const fixture = initWithTemplate(AppComp, `
|
||||
<div i18n>{count, plural, =0 {zero} other {more than zero}}</div>
|
||||
`);
|
||||
expect(fixture.nativeElement.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with directives and host bindings', () => {
|
||||
let directiveInstances: ClsDir[] = [];
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user