diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 05dad886e2..2755e0b57a 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -3661,6 +3661,39 @@ describe('i18n support in the template compiler', () => { verify(input, output); }); + + it('should produce proper messages when `select` or `plural` keywords have spaces after them', + () => { + const input = ` +
+ {count, select , 1 {one} other {more than one}} + {count, plural , =1 {one} other {more than one}} +
+ `; + + const output = String.raw` + var $I18N_1$; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + const $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$ = goog.getMsg("{VAR_SELECT , select , 1 {one} other {more than one}}"); + $I18N_1$ = $MSG_EXTERNAL_199763560911211963$$APP_SPEC_TS_2$; + } + else { + $I18N_1$ = $localize \`{VAR_SELECT , select , 1 {one} other {more than one}}\`; + } + $I18N_1$ = i0.ɵɵi18nPostprocess($I18N_1$, { "VAR_SELECT": "\uFFFD0\uFFFD" }); + var $I18N_3$; + if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { + const $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$ = goog.getMsg("{VAR_PLURAL , plural , =1 {one} other {more than one}}"); + $I18N_3$ = $MSG_EXTERNAL_3383986062053865025$$APP_SPEC_TS_4$; + } + else { + $I18N_3$ = $localize \`{VAR_PLURAL , plural , =1 {one} other {more than one}}\`; + } + $I18N_3$ = i0.ɵɵi18nPostprocess($I18N_3$, { "VAR_PLURAL": "\uFFFD1\uFFFD" }); + `; + + verify(input, output); + }); }); describe('$localize legacy message ids', () => { diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index e91502af63..7d1ffeba28 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -273,10 +273,20 @@ class HtmlAstToIvyAst implements html.Visitor { const value = message.placeholders[key]; if (key.startsWith(I18N_ICU_VAR_PREFIX)) { const config = this.bindingParser.interpolationConfig; + // ICU expression is a plain string, not wrapped into start // and end tags, so we wrap it before passing to binding parser const wrapped = `${config.start}${value}${config.end}`; - vars[key] = this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText; + + // Currently when the `plural` or `select` keywords in an ICU contain trailing spaces (e.g. + // `{count, select , ...}`), these spaces are also included into the key names in ICU vars + // (e.g. "VAR_SELECT "). These trailing spaces are not desirable, since they will later be + // converted into `_` symbols while normalizing placeholder names, which might lead to + // mismatches at runtime (i.e. placeholder will not be replaced with the correct value). + const formattedKey = key.trim(); + + vars[formattedKey] = + this._visitTextWithInterpolation(wrapped, expansion.sourceSpan) as t.BoundText; } else { placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan); } diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts index 796f55756e..cbca1e1246 100644 --- a/packages/core/test/acceptance/i18n_spec.ts +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -627,6 +627,24 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { expect(element.textContent).toContain('ICU start --> Autre <-- ICU end'); }); + it('when `select` or `plural` keywords have spaces after them', () => { + loadTranslations({ + [computeMsgId('{VAR_SELECT , select , 10 {ten} 20 {twenty} other {other}}')]: + '{VAR_SELECT , select , 10 {dix} 20 {vingt} other {autre}}', + [computeMsgId('{VAR_PLURAL , plural , =0 {zero} =1 {one} other {other}}')]: + '{VAR_PLURAL , plural , =0 {zéro} =1 {une} other {autre}}' + }); + const fixture = initWithTemplate(AppComp, ` +
+ {count, select , 10 {ten} 20 {twenty} other {other}} - + {count, plural , =0 {zero} =1 {one} other {other}} +
+ `); + + const element = fixture.nativeElement; + expect(element.textContent).toContain('autre - zéro'); + }); + it('with no root node and text and DOM nodes surrounding ICU', () => { loadTranslations({ [computeMsgId('{VAR_SELECT, select, 10 {Ten} 20 {Twenty} other {Other}}')]: