, 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);
}
diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts
index 8acdd6fc41..fa1a1a5214 100644
--- a/packages/compiler/test/render3/view/i18n_spec.ts
+++ b/packages/compiler/test/render3/view/i18n_spec.ts
@@ -249,7 +249,7 @@ describe('Serializer', () => {
// ICU with nested HTML
[
'{age, plural, 10 {ten} other {other
}}',
- '{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}}}'
]
];
diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts
index 9e55eb569c..2a328885b3 100644
--- a/packages/core/src/render3/i18n.ts
+++ b/packages/core/src/render3/i18n.ts
@@ -51,6 +51,7 @@ const ROOT_TEMPLATE_ID = 0;
const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]/;
const PP_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]|(�\/?\*\d+:\d+�)/g;
const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
+const PP_ICU_PLACEHOLDERS_REGEXP = /{([A-Z0-9_]+)}/g;
const PP_ICUS_REGEXP = /�I18N_EXP_(ICU(_\d+)?)�/g;
const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/;
const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/;
@@ -549,7 +550,8 @@ function appendI18nNode(
*
* 1. Resolve all multi-value cases (like [�*1:1��#2:1�|�#4:1�|�5�])
* 2. Replace all ICU vars (like "VAR_PLURAL")
- * 3. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
+ * 3. Replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
+ * 4. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
* in case multiple ICUs have the same placeholder name
*
* @param message Raw translation string for post processing
@@ -627,7 +629,14 @@ export function ɵɵi18nPostprocess(
});
/**
- * Step 3: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) in case
+ * Step 3: replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
+ */
+ result = result.replace(PP_ICU_PLACEHOLDERS_REGEXP, (match, key): string => {
+ return replacements.hasOwnProperty(key) ? replacements[key] as string : match;
+ });
+
+ /**
+ * Step 4: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) in case
* multiple ICUs have the same placeholder name
*/
result = result.replace(PP_ICUS_REGEXP, (match, key): string => {
diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts
index e1b0c722ee..eccc3b883a 100644
--- a/packages/core/test/acceptance/i18n_spec.ts
+++ b/packages/core/test/acceptance/i18n_spec.ts
@@ -39,6 +39,26 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.nativeElement.innerHTML).toEqual(`Bonjour John!
`);
});
+ it('should support named interpolations', () => {
+ ɵi18nConfigureLocalize({
+ translations: {
+ ' Hello {$userName}! Emails: {$amountOfEmailsReceived} ':
+ ' Bonjour {$userName}! Emails: {$amountOfEmailsReceived} '
+ }
+ });
+ const fixture = initWithTemplate(AppComp, `
+
+ Hello {{ name // i18n(ph="user_name") }}!
+ Emails: {{ count // i18n(ph="amount of emails received") }}
+
+ `);
+ expect(fixture.nativeElement.innerHTML).toEqual(` Bonjour Angular! Emails: 0
`);
+ fixture.componentRef.instance.name = `John`;
+ fixture.componentRef.instance.count = 5;
+ fixture.detectChanges();
+ expect(fixture.nativeElement.innerHTML).toEqual(` Bonjour John! Emails: 5
`);
+ });
+
it('should support interpolations with custom interpolation config', () => {
ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}});
const interpolation = ['{%', '%}'] as[string, string];
@@ -470,8 +490,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
it('multiple', () => {
ɵi18nConfigureLocalize({
translations: {
- '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}':
- '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}',
+ '{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}':
+ '{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
'{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}'
}
});
@@ -516,8 +536,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
it('inside HTML elements', () => {
ɵi18nConfigureLocalize({
translations: {
- '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}':
- '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}',
+ '{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}':
+ '{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} {START_TAG_SPAN}emails{CLOSE_TAG_SPAN}}}',
'{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}'
}
});
@@ -599,8 +619,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
it('nested', () => {
ɵi18nConfigureLocalize({
translations: {
- '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}':
- '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {chats} dog {chients} other {animaux}}!}}'
+ '{VAR_PLURAL, plural, =0 {zero} other {{INTERPOLATION} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}':
+ '{VAR_PLURAL, plural, =0 {zero} other {{INTERPOLATION} {VAR_SELECT, select, cat {chats} dog {chients} other {animaux}}!}}'
}
});
const fixture = initWithTemplate(AppComp, `{count, plural,
@@ -837,6 +857,40 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('A');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('C1');
});
+
+ it('with named interpolations', () => {
+ @Component({
+ selector: 'comp',
+ template: `
+ {
+ type,
+ select,
+ A {A - {{ typeA // i18n(ph="PH_A") }}}
+ B {B - {{ typeB // i18n(ph="PH_B") }}}
+ other {other - {{ typeC // i18n(ph="PH WITH SPACES") }}}
+ }
+ `,
+ })
+ class Comp {
+ type = 'A';
+ typeA = 'Type A';
+ typeB = 'Type B';
+ typeC = 'Type C';
+ }
+
+ TestBed.configureTestingModule({declarations: [Comp]});
+
+ const fixture = TestBed.createComponent(Comp);
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.nativeElement.innerHTML).toContain('A - Type A');
+
+ fixture.componentInstance.type = 'C'; // trigger "other" case
+ fixture.detectChanges();
+
+ expect(fixture.debugElement.nativeElement.innerHTML).not.toContain('A - Type A');
+ expect(fixture.debugElement.nativeElement.innerHTML).toContain('other - Type C');
+ });
});
describe('should support attributes', () => {
@@ -994,8 +1048,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
translations: {
'start {$interpolation} middle {$interpolation_1} end':
'début {$interpolation_1} milieu {$interpolation} fin',
- '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}':
- '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}',
+ '{VAR_PLURAL, plural, =0 {no {START_BOLD_TEXT}emails{CLOSE_BOLD_TEXT}!} =1 {one {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} emails}}':
+ '{VAR_PLURAL, plural, =0 {aucun {START_BOLD_TEXT}email{CLOSE_BOLD_TEXT}!} =1 {un {START_ITALIC_TEXT}email{CLOSE_ITALIC_TEXT}} other {{INTERPOLATION} emails}}',
' trad: {$icu}': ' traduction: {$icu}'
}
});