fix(ivy): update ICU placeholders format to match Closure compiler (#31459)
Since `goog.getMsg` does not process ICUs (post-processing is required via goog.i18n.MessageFormat, https://google.github.io/closure-library/api/goog.i18n.MessageFormat.html) and placeholder format used for ICUs and regular messages inside `goog.getMsg` are different, the current implementation (that assumed the same placeholder format) needs to be updated. This commit updates placeholder format used inside ICUs from `{$placeholder}` to `{PLACEHOLDER}` to better align with Closure. ICU placeholders (that were left as is prior to this commit) are now replaced with actual values in post-processing step (inside `i18nPostprocess`). PR Close #31459
This commit is contained in:
parent
6da1446afc
commit
dee16a4355
|
@ -967,25 +967,30 @@ describe('i18n support in the view compiler', () => {
|
||||||
|
|
||||||
it('should support named interpolations', () => {
|
it('should support named interpolations', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div i18n>Some value: {{ valueA // i18n(ph="PH_A") }}</div>
|
<div i18n>
|
||||||
|
Named interpolation: {{ valueA // i18n(ph="PH_A") }}
|
||||||
|
Named interpolation with spaces: {{ valueB // i18n(ph="PH B") }}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_2817319788724342848$$APP_SPEC_TS_0$ = goog.getMsg("Some value: {$phA}", {
|
const $MSG_EXTERNAL_7597881511811528589$$APP_SPEC_TS_0$ = goog.getMsg(" Named interpolation: {$phA} Named interpolation with spaces: {$phB} ", {
|
||||||
"phA": "\uFFFD0\uFFFD"
|
"phA": "\uFFFD0\uFFFD",
|
||||||
|
"phB": "\uFFFD1\uFFFD"
|
||||||
});
|
});
|
||||||
$I18N_0$ = $MSG_EXTERNAL_2817319788724342848$$APP_SPEC_TS_0$;
|
$I18N_0$ = $MSG_EXTERNAL_7597881511811528589$$APP_SPEC_TS_0$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_0$ = $r3$.ɵɵi18nLocalize("Some value: {$phA}", {
|
$I18N_0$ = $r3$.ɵɵi18nLocalize(" Named interpolation: {$phA} Named interpolation with spaces: {$phB} ", {
|
||||||
"phA": "\uFFFD0\uFFFD"
|
"phA": "\uFFFD0\uFFFD",
|
||||||
|
"phB": "\uFFFD1\uFFFD"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
…
|
…
|
||||||
consts: 2,
|
consts: 2,
|
||||||
vars: 1,
|
vars: 2,
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵelementStart(0, "div");
|
$r3$.ɵɵelementStart(0, "div");
|
||||||
|
@ -994,7 +999,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵɵselect(1);
|
$r3$.ɵɵselect(1);
|
||||||
$r3$.ɵɵi18nExp(ctx.valueA);
|
$r3$.ɵɵi18nExp(ctx.valueA)(ctx.valueB);
|
||||||
$r3$.ɵɵi18nApply(1);
|
$r3$.ɵɵi18nApply(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2609,18 +2614,15 @@ describe('i18n support in the view compiler', () => {
|
||||||
const $_c3$ = ["title", "icu and text"];
|
const $_c3$ = ["title", "icu and text"];
|
||||||
var $I18N_5$;
|
var $I18N_5$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_1922743304863699161$$APP_SPEC_TS__5$ = goog.getMsg("{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{$interpolation} emails}}", {
|
const $MSG_EXTERNAL_1922743304863699161$$APP_SPEC_TS__5$ = goog.getMsg("{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_5$ = $MSG_EXTERNAL_1922743304863699161$$APP_SPEC_TS__5$;
|
$I18N_5$ = $MSG_EXTERNAL_1922743304863699161$$APP_SPEC_TS__5$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_5$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{$interpolation} emails}}", {
|
$I18N_5$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 0 {no emails} 1 {one email} other {{INTERPOLATION} emails}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_5$ = $r3$.ɵɵi18nPostprocess($I18N_5$, {
|
$I18N_5$ = $r3$.ɵɵi18nPostprocess($I18N_5$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"INTERPOLATION": "\uFFFD1\uFFFD"
|
||||||
});
|
});
|
||||||
function MyComponent_div_3_Template(rf, ctx) {
|
function MyComponent_div_3_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
|
@ -2671,18 +2673,15 @@ describe('i18n support in the view compiler', () => {
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_2949673783721159566$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{$interpolation}}}", {
|
const $MSG_EXTERNAL_2949673783721159566$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_0$ = $MSG_EXTERNAL_2949673783721159566$$APP_SPEC_TS_0$;
|
$I18N_0$ = $MSG_EXTERNAL_2949673783721159566$$APP_SPEC_TS_0$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{$interpolation}}}", {
|
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {{INTERPOLATION}}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"INTERPOLATION": "\uFFFD1\uFFFD"
|
||||||
});
|
});
|
||||||
…
|
…
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
@ -2714,28 +2713,20 @@ describe('i18n support in the view compiler', () => {
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_1$;
|
var $I18N_1$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_2417296354340576868$$APP_SPEC_TS_1$ = goog.getMsg("{VAR_SELECT, select, male {male - {$startBoldText}male{$closeBoldText}} female {female {$startBoldText}female{$closeBoldText}} other {{$startTagDiv}{$startItalicText}other{$closeItalicText}{$closeTagDiv}}}", {
|
const $MSG_EXTERNAL_2417296354340576868$$APP_SPEC_TS_1$ = goog.getMsg("{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}");
|
||||||
"startBoldText": "<b>",
|
|
||||||
"closeBoldText": "</b>",
|
|
||||||
"startItalicText": "<i>",
|
|
||||||
"closeItalicText": "</i>",
|
|
||||||
"startTagDiv": "<div class=\"other\">",
|
|
||||||
"closeTagDiv": "</div>"
|
|
||||||
});
|
|
||||||
$I18N_1$ = $MSG_EXTERNAL_2417296354340576868$$APP_SPEC_TS_1$;
|
$I18N_1$ = $MSG_EXTERNAL_2417296354340576868$$APP_SPEC_TS_1$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male - {$startBoldText}male{$closeBoldText}} female {female {$startBoldText}female{$closeBoldText}} other {{$startTagDiv}{$startItalicText}other{$closeItalicText}{$closeTagDiv}}}", {
|
$I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male - {START_BOLD_TEXT}male{CLOSE_BOLD_TEXT}} female {female {START_BOLD_TEXT}female{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}{START_ITALIC_TEXT}other{CLOSE_ITALIC_TEXT}{CLOSE_TAG_DIV}}}");
|
||||||
"startBoldText": "<b>",
|
|
||||||
"closeBoldText": "</b>",
|
|
||||||
"startItalicText": "<i>",
|
|
||||||
"closeItalicText": "</i>",
|
|
||||||
"startTagDiv": "<div class=\"other\">",
|
|
||||||
"closeTagDiv": "</div>"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, {
|
$I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"START_BOLD_TEXT": "<b>",
|
||||||
|
"CLOSE_BOLD_TEXT": "</b>",
|
||||||
|
"START_ITALIC_TEXT": "<i>",
|
||||||
|
"CLOSE_ITALIC_TEXT": "</i>",
|
||||||
|
"START_TAG_DIV": "<div class=\"other\">",
|
||||||
|
"CLOSE_TAG_DIV": "</div>"
|
||||||
});
|
});
|
||||||
const $_c2$ = [1, "other"];
|
const $_c2$ = [1, "other"];
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
|
@ -2795,18 +2786,15 @@ describe('i18n support in the view compiler', () => {
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_6879461626778511059$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, male {male of age: {$interpolation}} female {female} other {other}}", {
|
const $MSG_EXTERNAL_6879461626778511059$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_0$ = $MSG_EXTERNAL_6879461626778511059$$APP_SPEC_TS_0$;
|
$I18N_0$ = $MSG_EXTERNAL_6879461626778511059$$APP_SPEC_TS_0$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male of age: {$interpolation}} female {female} other {other}}", {
|
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male of age: {INTERPOLATION}} female {female} other {other}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"INTERPOLATION": "\uFFFD1\uFFFD"
|
||||||
});
|
});
|
||||||
…
|
…
|
||||||
consts: 2,
|
consts: 2,
|
||||||
|
@ -3148,36 +3136,29 @@ describe('i18n support in the view compiler', () => {
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_1$;
|
var $I18N_1$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$ = goog.getMsg("{VAR_SELECT, select, male {male {$interpolation}} female {female {$interpolation_1}} other {other}}", {
|
const $MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$ = goog.getMsg("{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD",
|
|
||||||
"interpolation_1": "\uFFFD2\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_1$ = $MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$;
|
$I18N_1$ = $MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {$interpolation}} female {female {$interpolation_1}} other {other}}", {
|
$I18N_1$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {INTERPOLATION}} female {female {INTERPOLATION_1}} other {other}}");
|
||||||
"interpolation": "\uFFFD1\uFFFD",
|
|
||||||
"interpolation_1": "\uFFFD2\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, {
|
$I18N_1$ = $r3$.ɵɵi18nPostprocess($I18N_1$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"INTERPOLATION": "\uFFFD1\uFFFD",
|
||||||
|
"INTERPOLATION_1": "\uFFFD2\uFFFD"
|
||||||
});
|
});
|
||||||
const $_c0$ = [${AttributeMarker.Template}, "ngIf"];
|
const $_c0$ = [${AttributeMarker.Template}, "ngIf"];
|
||||||
var $I18N_3$;
|
var $I18N_3$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {$interpolation}}}", {
|
const $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}");
|
||||||
"interpolation": "\uFFFD1:1\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_3$ = $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$;
|
$I18N_3$ = $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_3$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {$interpolation}}}", {
|
$I18N_3$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {INTERPOLATION}}}");
|
||||||
"interpolation": "\uFFFD1:1\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_3$ = $r3$.ɵɵi18nPostprocess($I18N_3$, {
|
$I18N_3$ = $r3$.ɵɵi18nPostprocess($I18N_3$, {
|
||||||
"VAR_SELECT": "\uFFFD0:1\uFFFD"
|
"VAR_SELECT": "\uFFFD0:1\uFFFD",
|
||||||
|
"INTERPOLATION": "\uFFFD1:1\uFFFD"
|
||||||
});
|
});
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
|
@ -3239,29 +3220,24 @@ describe('i18n support in the view compiler', () => {
|
||||||
select,
|
select,
|
||||||
male {male {{ weight // i18n(ph="PH_A") }}}
|
male {male {{ weight // i18n(ph="PH_A") }}}
|
||||||
female {female {{ height // i18n(ph="PH_B") }}}
|
female {female {{ height // i18n(ph="PH_B") }}}
|
||||||
other {other {{ age // i18n(ph="PH_C") }}}
|
other {other {{ age // i18n(ph="PH WITH SPACES") }}}
|
||||||
}</div>
|
}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const output = String.raw `
|
const output = String.raw `
|
||||||
var $I18N_0$;
|
var $I18N_0$;
|
||||||
if (ngI18nClosureMode) {
|
if (ngI18nClosureMode) {
|
||||||
const $MSG_EXTERNAL_4853189513362404940$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, male {male {$phA}} female {female {$phB}} other {other {$phC}}}", {
|
const $MSG_EXTERNAL_6318060397235942326$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}");
|
||||||
"phA": "\uFFFD1\uFFFD",
|
$I18N_0$ = $MSG_EXTERNAL_6318060397235942326$$APP_SPEC_TS_0$;
|
||||||
"phB": "\uFFFD2\uFFFD",
|
|
||||||
"phC": "\uFFFD3\uFFFD"
|
|
||||||
});
|
|
||||||
$I18N_0$ = $MSG_EXTERNAL_4853189513362404940$$APP_SPEC_TS_0$;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {$phA}} female {female {$phB}} other {other {$phC}}}", {
|
$I18N_0$ = $r3$.ɵɵi18nLocalize("{VAR_SELECT, select, male {male {PH_A}} female {female {PH_B}} other {other {PH_WITH_SPACES}}}");
|
||||||
"phA": "\uFFFD1\uFFFD",
|
|
||||||
"phB": "\uFFFD2\uFFFD",
|
|
||||||
"phC": "\uFFFD3\uFFFD"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
$I18N_0$ = $r3$.ɵɵi18nPostprocess($I18N_0$, {
|
||||||
"VAR_SELECT": "\uFFFD0\uFFFD"
|
"VAR_SELECT": "\uFFFD0\uFFFD",
|
||||||
|
"PH_A": "\uFFFD1\uFFFD",
|
||||||
|
"PH_B": "\uFFFD2\uFFFD",
|
||||||
|
"PH_WITH_SPACES": "\uFFFD3\uFFFD"
|
||||||
});
|
});
|
||||||
…
|
…
|
||||||
consts: 2,
|
consts: 2,
|
||||||
|
|
|
@ -10,13 +10,26 @@ import * as i18n from '../../../i18n/i18n_ast';
|
||||||
|
|
||||||
import {formatI18nPlaceholderName} from './util';
|
import {formatI18nPlaceholderName} from './util';
|
||||||
|
|
||||||
const formatPh = (value: string): string => `{$${formatI18nPlaceholderName(value)}}`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This visitor walks over i18n tree and generates its string representation,
|
* This visitor walks over i18n tree and generates its string representation, including ICUs and
|
||||||
* including ICUs and placeholders in {$PLACEHOLDER} format.
|
* placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format.
|
||||||
*/
|
*/
|
||||||
class SerializerVisitor implements i18n.Visitor {
|
class SerializerVisitor implements i18n.Visitor {
|
||||||
|
/**
|
||||||
|
* Flag that indicates that we are processing elements of an ICU.
|
||||||
|
*
|
||||||
|
* This flag is needed due to the fact that placeholders in ICUs and in other messages are
|
||||||
|
* represented differently in Closure:
|
||||||
|
* - {$placeholder} in non-ICU case
|
||||||
|
* - {PLACEHOLDER} inside ICU
|
||||||
|
*/
|
||||||
|
private insideIcu = false;
|
||||||
|
|
||||||
|
private formatPh(value: string): string {
|
||||||
|
const formatted = formatI18nPlaceholderName(value, /* useCamelCase */ !this.insideIcu);
|
||||||
|
return this.insideIcu ? `{${formatted}}` : `{$${formatted}}`;
|
||||||
|
}
|
||||||
|
|
||||||
visitText(text: i18n.Text, context: any): any { return text.value; }
|
visitText(text: i18n.Text, context: any): any { return text.value; }
|
||||||
|
|
||||||
visitContainer(container: i18n.Container, context: any): any {
|
visitContainer(container: i18n.Container, context: any): any {
|
||||||
|
@ -24,20 +37,25 @@ class SerializerVisitor implements i18n.Visitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
visitIcu(icu: i18n.Icu, context: any): any {
|
visitIcu(icu: i18n.Icu, context: any): any {
|
||||||
|
this.insideIcu = true;
|
||||||
const strCases =
|
const strCases =
|
||||||
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||||
return `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||||
|
this.insideIcu = false;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
||||||
return ph.isVoid ?
|
return ph.isVoid ?
|
||||||
formatPh(ph.startName) :
|
this.formatPh(ph.startName) :
|
||||||
`${formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${formatPh(ph.closeName)}`;
|
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return formatPh(ph.name); }
|
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return this.formatPh(ph.name); }
|
||||||
|
|
||||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { return formatPh(ph.name); }
|
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||||
|
return this.formatPh(ph.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializerVisitor = new SerializerVisitor();
|
const serializerVisitor = new SerializerVisitor();
|
||||||
|
|
|
@ -220,8 +220,12 @@ export function parseI18nMeta(meta?: string): I18nMeta {
|
||||||
* @param name The placeholder name that should be formatted
|
* @param name The placeholder name that should be formatted
|
||||||
* @returns Formatted placeholder name
|
* @returns Formatted placeholder name
|
||||||
*/
|
*/
|
||||||
export function formatI18nPlaceholderName(name: string): string {
|
export function formatI18nPlaceholderName(name: string, useCamelCase: boolean = true): string {
|
||||||
const chunks = toPublicName(name).split('_');
|
const publicName = toPublicName(name);
|
||||||
|
if (!useCamelCase) {
|
||||||
|
return publicName;
|
||||||
|
}
|
||||||
|
const chunks = publicName.split('_');
|
||||||
if (chunks.length === 1) {
|
if (chunks.length === 1) {
|
||||||
// if no "_" found - just lowercase the value
|
// if no "_" found - just lowercase the value
|
||||||
return name.toLowerCase();
|
return name.toLowerCase();
|
||||||
|
|
|
@ -324,18 +324,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
|
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
|
||||||
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
|
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
|
||||||
const closureVar = this.i18nGenerateClosureVar(message.id);
|
const closureVar = this.i18nGenerateClosureVar(message.id);
|
||||||
const _params: {[key: string]: any} = {};
|
const formattedParams = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ true);
|
||||||
if (params && Object.keys(params).length) {
|
|
||||||
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
|
|
||||||
}
|
|
||||||
const meta = metaFromI18nMessage(message);
|
const meta = metaFromI18nMessage(message);
|
||||||
const content = getSerializedI18nContent(message);
|
const content = getSerializedI18nContent(message);
|
||||||
const statements =
|
const statements =
|
||||||
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
|
getTranslationDeclStmts(_ref, closureVar, content, meta, formattedParams, transformFn);
|
||||||
this.constantPool.statements.push(...statements);
|
this.constantPool.statements.push(...statements);
|
||||||
return _ref;
|
return _ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i18nFormatPlaceholderNames(params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) {
|
||||||
|
const _params: {[key: string]: o.Expression} = {};
|
||||||
|
if (params && Object.keys(params).length) {
|
||||||
|
Object.keys(params).forEach(
|
||||||
|
key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]);
|
||||||
|
}
|
||||||
|
return _params;
|
||||||
|
}
|
||||||
|
|
||||||
i18nAppendBindings(expressions: AST[]) {
|
i18nAppendBindings(expressions: AST[]) {
|
||||||
if (expressions.length > 0) {
|
if (expressions.length > 0) {
|
||||||
expressions.forEach(expression => this.i18n !.appendBinding(expression));
|
expressions.forEach(expression => this.i18n !.appendBinding(expression));
|
||||||
|
@ -994,17 +1000,29 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
// output ICU directly and keep ICU reference in context
|
// output ICU directly and keep ICU reference in context
|
||||||
const message = icu.i18n !as i18n.Message;
|
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
|
// 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
|
// create a separate top-level translation, we can use the root ref instead
|
||||||
// and make this ICU a top-level translation
|
// 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)) {
|
if (isSingleI18nIcu(i18n.meta)) {
|
||||||
this.i18nTranslate(message, placeholders, i18n.ref, transformFn);
|
this.i18nTranslate(message, /* placeholders */ {}, i18n.ref, transformFn);
|
||||||
} else {
|
} else {
|
||||||
// output ICU directly and keep ICU reference in context
|
// 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);
|
i18n.appendIcu(icuFromI18nMessage(message).name, ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -249,7 +249,7 @@ describe('Serializer', () => {
|
||||||
// ICU with nested HTML
|
// ICU with nested HTML
|
||||||
[
|
[
|
||||||
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
|
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
|
||||||
'{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}}}'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ const ROOT_TEMPLATE_ID = 0;
|
||||||
const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]/;
|
const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]/;
|
||||||
const PP_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]|(<28>\/?\*\d+:\d+<2B>)/g;
|
const PP_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]|(<28>\/?\*\d+:\d+<2B>)/g;
|
||||||
const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/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 = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
|
const PP_ICUS_REGEXP = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
|
||||||
const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/;
|
const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/;
|
||||||
const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/;
|
const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/;
|
||||||
|
@ -549,7 +550,8 @@ function appendI18nNode(
|
||||||
*
|
*
|
||||||
* 1. Resolve all multi-value cases (like [<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>|<EFBFBD>5<EFBFBD>])
|
* 1. Resolve all multi-value cases (like [<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>|<EFBFBD>5<EFBFBD>])
|
||||||
* 2. Replace all ICU vars (like "VAR_PLURAL")
|
* 2. Replace all ICU vars (like "VAR_PLURAL")
|
||||||
* 3. Replace all ICU references with corresponding values (like <EFBFBD>ICU_EXP_ICU_1<EFBFBD>)
|
* 3. Replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
|
||||||
|
* 4. Replace all ICU references with corresponding values (like <EFBFBD>ICU_EXP_ICU_1<EFBFBD>)
|
||||||
* in case multiple ICUs have the same placeholder name
|
* in case multiple ICUs have the same placeholder name
|
||||||
*
|
*
|
||||||
* @param message Raw translation string for post processing
|
* @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 <EFBFBD>ICU_EXP_ICU_1<EFBFBD>) 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 <EFBFBD>ICU_EXP_ICU_1<EFBFBD>) in case
|
||||||
* multiple ICUs have the same placeholder name
|
* multiple ICUs have the same placeholder name
|
||||||
*/
|
*/
|
||||||
result = result.replace(PP_ICUS_REGEXP, (match, key): string => {
|
result = result.replace(PP_ICUS_REGEXP, (match, key): string => {
|
||||||
|
|
|
@ -39,6 +39,26 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
expect(fixture.nativeElement.innerHTML).toEqual(`<div>Bonjour John!</div>`);
|
expect(fixture.nativeElement.innerHTML).toEqual(`<div>Bonjour John!</div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support named interpolations', () => {
|
||||||
|
ɵi18nConfigureLocalize({
|
||||||
|
translations: {
|
||||||
|
' Hello {$userName}! Emails: {$amountOfEmailsReceived} ':
|
||||||
|
' Bonjour {$userName}! Emails: {$amountOfEmailsReceived} '
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fixture = initWithTemplate(AppComp, `
|
||||||
|
<div i18n>
|
||||||
|
Hello {{ name // i18n(ph="user_name") }}!
|
||||||
|
Emails: {{ count // i18n(ph="amount of emails received") }}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
expect(fixture.nativeElement.innerHTML).toEqual(`<div> Bonjour Angular! Emails: 0 </div>`);
|
||||||
|
fixture.componentRef.instance.name = `John`;
|
||||||
|
fixture.componentRef.instance.count = 5;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.nativeElement.innerHTML).toEqual(`<div> Bonjour John! Emails: 5 </div>`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should support interpolations with custom interpolation config', () => {
|
it('should support interpolations with custom interpolation config', () => {
|
||||||
ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}});
|
ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}});
|
||||||
const interpolation = ['{%', '%}'] as[string, string];
|
const interpolation = ['{%', '%}'] as[string, string];
|
||||||
|
@ -470,8 +490,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
it('multiple', () => {
|
it('multiple', () => {
|
||||||
ɵi18nConfigureLocalize({
|
ɵi18nConfigureLocalize({
|
||||||
translations: {
|
translations: {
|
||||||
'{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$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 {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}',
|
'{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})}}'
|
'{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', () => {
|
it('inside HTML elements', () => {
|
||||||
ɵi18nConfigureLocalize({
|
ɵi18nConfigureLocalize({
|
||||||
translations: {
|
translations: {
|
||||||
'{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$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 {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}',
|
'{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})}}'
|
'{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -599,8 +619,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
it('nested', () => {
|
it('nested', () => {
|
||||||
ɵi18nConfigureLocalize({
|
ɵi18nConfigureLocalize({
|
||||||
translations: {
|
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 {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 {chats} dog {chients} other {animaux}}!}}'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const fixture = initWithTemplate(AppComp, `<div i18n>{count, plural,
|
const fixture = initWithTemplate(AppComp, `<div i18n>{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).not.toContain('A');
|
||||||
expect(fixture.debugElement.nativeElement.innerHTML).toContain('C1');
|
expect(fixture.debugElement.nativeElement.innerHTML).toContain('C1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('with named interpolations', () => {
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: `
|
||||||
|
<ng-container i18n>{
|
||||||
|
type,
|
||||||
|
select,
|
||||||
|
A {A - {{ typeA // i18n(ph="PH_A") }}}
|
||||||
|
B {B - {{ typeB // i18n(ph="PH_B") }}}
|
||||||
|
other {other - {{ typeC // i18n(ph="PH WITH SPACES") }}}
|
||||||
|
}</ng-container>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
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', () => {
|
describe('should support attributes', () => {
|
||||||
|
@ -994,8 +1048,8 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
translations: {
|
translations: {
|
||||||
'start {$interpolation} middle {$interpolation_1} end':
|
'start {$interpolation} middle {$interpolation_1} end':
|
||||||
'début {$interpolation_1} milieu {$interpolation} fin',
|
'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 {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 {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} 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}'
|
' trad: {$icu}': ' traduction: {$icu}'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue