diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 21cb9fbcb1..54704a01b4 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 136777, + "main-es2015": 137320, "polyfills-es2015": 37334 } } @@ -64,4 +64,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 04888681e5..5bcda795a8 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -441,6 +441,15 @@ function i18nStartFirstPass( for (let j = 0; j < parts.length; j++) { if (j & 1) { // Odd indexes are ICU expressions + 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). + if (typeof icuExpression !== 'object') { + 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++; createOpCodes.push( @@ -448,7 +457,6 @@ function i18nStartFirstPass( parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); // Update codes for the ICU expression - const icuExpression = parts[j] as IcuExpression; const mask = getBindingMask(icuExpression); icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex); // Since this is recursive, the last TIcu that was pushed is the one we want diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 1a45e7cacc..c32c56f02b 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -2119,6 +2119,55 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { }); }); + describe('invalid translations handling', () => { + it('should throw in case invalid ICU is present in a template', () => { + // Error message is produced by Compiler. + expect(() => initWithTemplate(AppComp, '{count, select, 10 {ten} other {other}')) + .toThrowError( + /Invalid ICU message. Missing '}'. \("{count, select, 10 {ten} other {other}\[ERROR ->\]"\)/); + }); + + it('should throw in case invalid ICU is present in translation', () => { + loadTranslations({ + [computeMsgId('{VAR_SELECT, select, 10 {ten} other {other}}')]: + // Missing "}" at the end of translation. + '{VAR_SELECT, select, 10 {dix} other {autre}' + }); + + // Error message is produced at runtime. + expect(() => initWithTemplate(AppComp, '{count, select, 10 {ten} other {other}}')) + .toThrowError( + /Unable to parse ICU expression in "{�0�, select, 10 {dix} other {autre}" message./); + }); + + it('should throw in case unescaped curly braces are present in a template', () => { + // Error message is produced by Compiler. + expect(() => initWithTemplate(AppComp, 'Text { count }')) + .toThrowError( + /Do you have an unescaped "{" in your template\? Use "{{ '{' }}"\) to escape it/); + }); + + it('should throw in case curly braces are added into translation', () => { + loadTranslations({ + // Curly braces which were not present in a template were added into translation. + [computeMsgId('Text')]: 'Text { count }', + }); + expect(() => initWithTemplate(AppComp, '