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.
|
* external template and removes all sub-templates.
|
||||||
*/
|
*/
|
||||||
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
|
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
|
||||||
if (typeof subTemplateIndex !== 'number') {
|
if (isRootTemplateMessage(subTemplateIndex)) {
|
||||||
// We want the root template message, ignore all sub-templates
|
// We want the root template message, ignore all sub-templates
|
||||||
return removeInnerTemplateTranslation(message);
|
return removeInnerTemplateTranslation(message);
|
||||||
} else {
|
} else {
|
||||||
@ -376,6 +376,10 @@ export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?:
|
|||||||
// This is reset to 0 when `i18nStartFirstPass` is called.
|
// This is reset to 0 when `i18nStartFirstPass` is called.
|
||||||
let i18nVarsCount: number;
|
let i18nVarsCount: number;
|
||||||
|
|
||||||
|
function allocNodeIndex(startIndex: number): number {
|
||||||
|
return startIndex + i18nVarsCount++;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See `i18nStart` above.
|
* See `i18nStart` above.
|
||||||
*/
|
*/
|
||||||
@ -409,6 +413,13 @@ function i18nStartFirstPass(
|
|||||||
const updateOpCodes: I18nUpdateOpCodes = [];
|
const updateOpCodes: I18nUpdateOpCodes = [];
|
||||||
const icuExpressions: TIcu[] = [];
|
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 templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
|
||||||
const msgParts = replaceNgsp(templateTranslation).split(PH_REGEXP);
|
const msgParts = replaceNgsp(templateTranslation).split(PH_REGEXP);
|
||||||
for (let i = 0; i < msgParts.length; i++) {
|
for (let i = 0; i < msgParts.length; i++) {
|
||||||
@ -426,8 +437,8 @@ function i18nStartFirstPass(
|
|||||||
const phIndex = parseInt(value.substr(1), 10);
|
const phIndex = parseInt(value.substr(1), 10);
|
||||||
const isElement = value.charAt(0) === TagType.ELEMENT;
|
const isElement = value.charAt(0) === TagType.ELEMENT;
|
||||||
// The value represents a placeholder that we move to the designated index.
|
// 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
|
// Note: positive indicies indicate that a TNode with a given index should also be marked
|
||||||
// parent while executing `Select` instruction.
|
// as parent while executing `Select` instruction.
|
||||||
createOpCodes.push(
|
createOpCodes.push(
|
||||||
(isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
|
(isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
|
||||||
I18nMutateOpCode.Select,
|
I18nMutateOpCode.Select,
|
||||||
@ -446,14 +457,15 @@ function i18nStartFirstPass(
|
|||||||
const icuExpression = parts[j] as IcuExpression;
|
const icuExpression = parts[j] as IcuExpression;
|
||||||
|
|
||||||
// Verify that ICU expression has the right shape. Translations might contain invalid
|
// 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
|
// constructions (while original messages were correct), so ICU parsing at runtime may
|
||||||
// succeed (thus `icuExpression` remains a string).
|
// not succeed (thus `icuExpression` remains a string).
|
||||||
if (typeof icuExpression !== 'object') {
|
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
|
// Create the comment node that will anchor the ICU expression
|
||||||
const icuNodeIndex = startIndex + i18nVarsCount++;
|
const icuNodeIndex = allocNodeIndex(startIndex);
|
||||||
createOpCodes.push(
|
createOpCodes.push(
|
||||||
COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
|
COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
|
||||||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||||||
@ -476,7 +488,7 @@ function i18nStartFirstPass(
|
|||||||
// Even indexes are text (including bindings)
|
// Even indexes are text (including bindings)
|
||||||
const hasBinding = text.match(BINDING_REGEXP);
|
const hasBinding = text.match(BINDING_REGEXP);
|
||||||
// Create text nodes
|
// Create text nodes
|
||||||
const textNodeIndex = startIndex + i18nVarsCount++;
|
const textNodeIndex = allocNodeIndex(startIndex);
|
||||||
createOpCodes.push(
|
createOpCodes.push(
|
||||||
// If there is a binding, the value will be set during update
|
// If there is a binding, the value will be set during update
|
||||||
hasBinding ? '' : text, textNodeIndex,
|
hasBinding ? '' : text, textNodeIndex,
|
||||||
@ -489,6 +501,7 @@ function i18nStartFirstPass(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (i18nVarsCount > 0) {
|
if (i18nVarsCount > 0) {
|
||||||
allocExpando(tView, lView, i18nVarsCount);
|
allocExpando(tView, lView, i18nVarsCount);
|
||||||
@ -558,6 +571,10 @@ function appendI18nNode(
|
|||||||
return tNode;
|
return tNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRootTemplateMessage(subTemplateIndex: number|undefined): subTemplateIndex is undefined {
|
||||||
|
return subTemplateIndex === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles message string post-processing for internationalization.
|
* 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', () => {
|
it('should work with directives and host bindings', () => {
|
||||||
let directiveInstances: ClsDir[] = [];
|
let directiveInstances: ClsDir[] = [];
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user