feat(ivy): i18n compiler support for i18nStart and i18nEnd instructions (#26442)
PR Close #26442
This commit is contained in:
parent
8024857d4c
commit
8a3fd58cad
@ -9,8 +9,6 @@
|
|||||||
import {setup} from '@angular/compiler/test/aot/test_util';
|
import {setup} from '@angular/compiler/test/aot/test_util';
|
||||||
import {compile, expectEmit} from './mock_compile';
|
import {compile, expectEmit} from './mock_compile';
|
||||||
|
|
||||||
const TRANSLATION_NAME_REGEXP = /^MSG_[A-Z0-9]+/;
|
|
||||||
|
|
||||||
describe('i18n support in the view compiler', () => {
|
describe('i18n support in the view compiler', () => {
|
||||||
const angularFiles = setup({
|
const angularFiles = setup({
|
||||||
compileAngular: false,
|
compileAngular: false,
|
||||||
@ -18,56 +16,7 @@ describe('i18n support in the view compiler', () => {
|
|||||||
compileAnimations: false,
|
compileAnimations: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('single text nodes', () => {
|
describe('element attributes', () => {
|
||||||
it('should translate single text nodes with the i18n attribute', () => {
|
|
||||||
const files = {
|
|
||||||
app: {
|
|
||||||
'spec.ts': `
|
|
||||||
import {Component, NgModule} from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-component',
|
|
||||||
template: \`
|
|
||||||
<div i18n>Hello world</div>
|
|
||||||
<div>&</div>
|
|
||||||
<div i18n>farewell</div>
|
|
||||||
<div i18n>farewell</div>
|
|
||||||
\`
|
|
||||||
})
|
|
||||||
export class MyComponent {}
|
|
||||||
|
|
||||||
@NgModule({declarations: [MyComponent]})
|
|
||||||
export class MyModule {}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
const $msg_1$ = goog.getMsg("Hello world");
|
|
||||||
const $msg_2$ = goog.getMsg("farewell");
|
|
||||||
…
|
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
|
||||||
if (rf & 1) {
|
|
||||||
…
|
|
||||||
$r3$.ɵtext(1, $msg_1$);
|
|
||||||
…
|
|
||||||
$r3$.ɵtext(3,"&");
|
|
||||||
…
|
|
||||||
$r3$.ɵtext(5, $msg_2$);
|
|
||||||
…
|
|
||||||
$r3$.ɵtext(7, $msg_2$);
|
|
||||||
…
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = compile(files, angularFiles);
|
|
||||||
expectEmit(result.source, template, 'Incorrect template', {
|
|
||||||
'$msg_1$': TRANSLATION_NAME_REGEXP,
|
|
||||||
'$msg_2$': TRANSLATION_NAME_REGEXP,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add the meaning and description as JsDoc comments', () => {
|
it('should add the meaning and description as JsDoc comments', () => {
|
||||||
const files = {
|
const files = {
|
||||||
app: {
|
app: {
|
||||||
@ -77,7 +26,12 @@ describe('i18n support in the view compiler', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
template: \`
|
template: \`
|
||||||
<div i18n="meaning|desc@@id" i18n-title="desc" title="introduction">Hello world</div>
|
<div i18n="meaningA|descA@@idA">Content A</div>
|
||||||
|
<div i18n-title="meaningB|descB@@idB" title="Title B">Content B</div>
|
||||||
|
<div i18n-title="meaningC" title="Title C">Content C</div>
|
||||||
|
<div i18n-title="meaningD|descD" title="Title D">Content D</div>
|
||||||
|
<div i18n-title="meaningE@@idE" title="Title E">Content E</div>
|
||||||
|
<div i18n-title="@@idF" title="Title F">Content F</div>
|
||||||
\`
|
\`
|
||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
@ -90,22 +44,63 @@ describe('i18n support in the view compiler', () => {
|
|||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
/**
|
/**
|
||||||
* @desc desc
|
* @desc [BACKUP_MESSAGE_ID:idA] descA
|
||||||
|
* @meaning meaningA
|
||||||
*/
|
*/
|
||||||
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("introduction");
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("Content A");
|
||||||
const $_c1$ = ["title", $MSG_APP_SPEC_TS_0$, 0];
|
|
||||||
…
|
|
||||||
/**
|
/**
|
||||||
* @desc desc
|
* @desc [BACKUP_MESSAGE_ID:idB] descB
|
||||||
* @meaning meaning
|
* @meaning meaningB
|
||||||
*/
|
*/
|
||||||
const $MSG_APP_SPEC_TS_2$ = goog.getMsg("Hello world");
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("Title B");
|
||||||
|
const $_c2$ = ["title", $MSG_APP_SPEC_TS_1$, 0];
|
||||||
|
/**
|
||||||
|
* @desc meaningC
|
||||||
|
*/
|
||||||
|
const $MSG_APP_SPEC_TS_3$ = goog.getMsg("Title C");
|
||||||
|
const $_c4$ = ["title", $MSG_APP_SPEC_TS_3$, 0];
|
||||||
|
/**
|
||||||
|
* @desc descD
|
||||||
|
* @meaning meaningD
|
||||||
|
*/
|
||||||
|
const $MSG_APP_SPEC_TS_5$ = goog.getMsg("Title D");
|
||||||
|
const $_c6$ = ["title", $MSG_APP_SPEC_TS_5$, 0];
|
||||||
|
/**
|
||||||
|
* @desc [BACKUP_MESSAGE_ID:idE] meaningE
|
||||||
|
*/
|
||||||
|
const $MSG_APP_SPEC_TS_7$ = goog.getMsg("Title E");
|
||||||
|
const $_c8$ = ["title", $MSG_APP_SPEC_TS_7$, 0];
|
||||||
|
/**
|
||||||
|
* @desc [BACKUP_MESSAGE_ID:idF]
|
||||||
|
*/
|
||||||
|
const $MSG_APP_SPEC_TS_9$ = goog.getMsg("Title F");
|
||||||
|
const $_c10$ = ["title", $MSG_APP_SPEC_TS_9$, 0];
|
||||||
…
|
…
|
||||||
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");
|
||||||
$r3$.ɵi18nAttribute(1, $_c1$);
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
$r3$.ɵtext(2, $MSG_APP_SPEC_TS_2$);
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(2, "div");
|
||||||
|
$r3$.ɵi18nAttribute(3, $_c2$);
|
||||||
|
$r3$.ɵtext(4, "Content B");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(5, "div");
|
||||||
|
$r3$.ɵi18nAttribute(6, $_c4$);
|
||||||
|
$r3$.ɵtext(7, "Content C");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(8, "div");
|
||||||
|
$r3$.ɵi18nAttribute(9, $_c6$);
|
||||||
|
$r3$.ɵtext(10, "Content D");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(11, "div");
|
||||||
|
$r3$.ɵi18nAttribute(12, $_c8$);
|
||||||
|
$r3$.ɵtext(13, "Content E");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(14, "div");
|
||||||
|
$r3$.ɵi18nAttribute(15, $_c10$);
|
||||||
|
$r3$.ɵtext(16, "Content F");
|
||||||
$r3$.ɵelementEnd();
|
$r3$.ɵelementEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,9 +109,6 @@ describe('i18n support in the view compiler', () => {
|
|||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect template');
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('element attributes', () => {
|
|
||||||
|
|
||||||
it('should translate static attributes', () => {
|
it('should translate static attributes', () => {
|
||||||
const files = {
|
const files = {
|
||||||
@ -127,7 +119,7 @@ describe('i18n support in the view compiler', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
template: \`
|
template: \`
|
||||||
<div i18n id="static" i18n-title="m|d" title="introduction"></div>
|
<div id="static" i18n-title="m|d" title="introduction"></div>
|
||||||
\`
|
\`
|
||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
@ -169,12 +161,12 @@ describe('i18n support in the view compiler', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
template: \`
|
template: \`
|
||||||
<div i18n id="dynamic-1"
|
<div id="dynamic-1"
|
||||||
i18n-title="m|d" title="intro {{ valueA | uppercase }}"
|
i18n-title="m|d" title="intro {{ valueA | uppercase }}"
|
||||||
i18n-aria-label="m1|d1" aria-label="{{ valueB }}"
|
i18n-aria-label="m1|d1" aria-label="{{ valueB }}"
|
||||||
i18n-aria-roledescription aria-roledescription="static text"
|
i18n-aria-roledescription aria-roledescription="static text"
|
||||||
></div>
|
></div>
|
||||||
<div i18n id="dynamic-2"
|
<div id="dynamic-2"
|
||||||
i18n-title="m2|d2" title="{{ valueA }} and {{ valueB }} and again {{ valueA + valueB }}"
|
i18n-title="m2|d2" title="{{ valueA }} and {{ valueB }} and again {{ valueA + valueB }}"
|
||||||
i18n-aria-roledescription aria-roledescription="{{ valueC }}"
|
i18n-aria-roledescription aria-roledescription="{{ valueC }}"
|
||||||
></div>
|
></div>
|
||||||
@ -184,7 +176,7 @@ describe('i18n support in the view compiler', () => {
|
|||||||
|
|
||||||
@NgModule({declarations: [MyComponent]})
|
@NgModule({declarations: [MyComponent]})
|
||||||
export class MyModule {}
|
export class MyModule {}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -307,12 +299,12 @@ describe('i18n support in the view compiler', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
template: \`
|
template: \`
|
||||||
<div i18n id="dynamic-1"
|
<div id="dynamic-1"
|
||||||
i18n-title="m|d" title="intro {{ valueA | uppercase }}"
|
i18n-title="m|d" title="intro {{ valueA | uppercase }}"
|
||||||
i18n-aria-label="m1|d1" aria-label="{{ valueB }}"
|
i18n-aria-label="m1|d1" aria-label="{{ valueB }}"
|
||||||
i18n-aria-roledescription aria-roledescription="static text"
|
i18n-aria-roledescription aria-roledescription="static text"
|
||||||
></div>
|
></div>
|
||||||
<div i18n id="dynamic-2"
|
<div id="dynamic-2"
|
||||||
i18n-title="m2|d2" title="{{ valueA }} and {{ valueB }} and again {{ valueA + valueB }}"
|
i18n-title="m2|d2" title="{{ valueA }} and {{ valueB }} and again {{ valueA + valueB }}"
|
||||||
i18n-aria-roledescription aria-roledescription="{{ valueC }}"
|
i18n-aria-roledescription aria-roledescription="{{ valueC }}"
|
||||||
></div>
|
></div>
|
||||||
@ -437,9 +429,8 @@ describe('i18n support in the view compiler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(vicb): this feature is not supported yet
|
describe('nested nodes', () => {
|
||||||
xdescribe('nested nodes', () => {
|
it('should not produce instructions for empty content', () => {
|
||||||
it('should generate the placeholders maps', () => {
|
|
||||||
const files = {
|
const files = {
|
||||||
app: {
|
app: {
|
||||||
'spec.ts': `
|
'spec.ts': `
|
||||||
@ -448,24 +439,471 @@ describe('i18n support in the view compiler', () => {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'my-component',
|
selector: 'my-component',
|
||||||
template: \`
|
template: \`
|
||||||
<div i18n>Hello <b>{{name}}<i>!</i><i>!</i></b></div>
|
<div i18n></div>
|
||||||
<div>Other</div>
|
<div i18n> </div>
|
||||||
<div i18n>2nd</div>
|
|
||||||
<div i18n><i>3rd</i></div>
|
|
||||||
\`
|
\`
|
||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
|
|
||||||
@NgModule({declarations: [MyComponent]})
|
@NgModule({declarations: [MyComponent]})
|
||||||
export class MyModule {}
|
export class MyModule {}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = String.raw `
|
||||||
const $r1$ = {"b":[2], "i":[4, 6]};
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
const $r2$ = {"i":[13]};
|
if (rf & 1) {
|
||||||
`;
|
$r3$.ɵelement(0, "div");
|
||||||
|
$r3$.ɵelement(1, "div");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should handle i18n attributes with plain-text content', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div i18n>My i18n block #1</div>
|
||||||
|
<div>My non-i18n block #1</div>
|
||||||
|
<div i18n>My i18n block #2</div>
|
||||||
|
<div>My non-i18n block #2</div>
|
||||||
|
<div i18n>My i18n block #3</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("My i18n block #1");
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("My i18n block #2");
|
||||||
|
const $MSG_APP_SPEC_TS_2$ = goog.getMsg("My i18n block #3");
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(2, "div");
|
||||||
|
$r3$.ɵtext(3, "My non-i18n block #1");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(4, "div");
|
||||||
|
$r3$.ɵi18nStart(5, $MSG_APP_SPEC_TS_1$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(6, "div");
|
||||||
|
$r3$.ɵtext(7, "My non-i18n block #2");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(8, "div");
|
||||||
|
$r3$.ɵi18nStart(9, $MSG_APP_SPEC_TS_2$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle i18n attributes with bindings in content', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div i18n>My i18n block #{{ one }}</div>
|
||||||
|
<div i18n>My i18n block #{{ two | uppercase }}</div>
|
||||||
|
<div i18n>My i18n block #{{ three + four + five }}</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("My i18n block #\uFFFD0\uFFFD");
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("My i18n block #\uFFFD0\uFFFD");
|
||||||
|
const $MSG_APP_SPEC_TS_2$ = goog.getMsg("My i18n block #\uFFFD0\uFFFD");
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(2, "div");
|
||||||
|
$r3$.ɵi18nStart(3, $MSG_APP_SPEC_TS_1$);
|
||||||
|
$r3$.ɵpipe(4, "uppercase");
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(5, "div");
|
||||||
|
$r3$.ɵi18nStart(6, $MSG_APP_SPEC_TS_2$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 0, ctx.two)));
|
||||||
|
$r3$.ɵi18nApply(3);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(((ctx.three + ctx.four) + ctx.five)));
|
||||||
|
$r3$.ɵi18nApply(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle i18n attributes with bindings and nested elements in content', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div i18n>
|
||||||
|
My i18n block #{{ one }}
|
||||||
|
<span>Plain text in nested element</span>
|
||||||
|
</div>
|
||||||
|
<div i18n>
|
||||||
|
My i18n block #{{ two | uppercase }}
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
More bindings in more nested element: {{ nestedInBlockTwo }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("My i18n block #\uFFFD0\uFFFD\uFFFD#2\uFFFDPlain text in nested element\uFFFD/#2\uFFFD");
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("My i18n block #\uFFFD0\uFFFD\uFFFD#6\uFFFD\uFFFD#7\uFFFD\uFFFD#8\uFFFDMore bindings in more nested element: \uFFFD1\uFFFD\uFFFD/#8\uFFFD\uFFFD/#7\uFFFD\uFFFD/#6\uFFFD");
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵelement(2, "span");
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(3, "div");
|
||||||
|
$r3$.ɵi18nStart(4, $MSG_APP_SPEC_TS_1$);
|
||||||
|
$r3$.ɵpipe(5, "uppercase");
|
||||||
|
$r3$.ɵelementStart(6, "div");
|
||||||
|
$r3$.ɵelementStart(7, "div");
|
||||||
|
$r3$.ɵelement(8, "span");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.one));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(5, 0, ctx.two)));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.nestedInBlockTwo));
|
||||||
|
$r3$.ɵi18nApply(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle i18n attributes with bindings in content and element attributes', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div i18n>
|
||||||
|
My i18n block #1 with value: {{ valueA }}
|
||||||
|
<span i18n-title title="Span title {{ valueB }} and {{ valueC }}">
|
||||||
|
Plain text in nested element (block #1)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div i18n>
|
||||||
|
My i18n block #2 with value {{ valueD | uppercase }}
|
||||||
|
<span i18n-title title="Span title {{ valueE }}">
|
||||||
|
Plain text in nested element (block #2)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("My i18n block #1 with value: \uFFFD0\uFFFD\uFFFD#2\uFFFDPlain text in nested element (block #1)\uFFFD/#2\uFFFD");
|
||||||
|
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("Span title \uFFFD0\uFFFD and \uFFFD1\uFFFD");
|
||||||
|
const $_c2$ = ["title", $MSG_APP_SPEC_TS_1$, 2];
|
||||||
|
const $MSG_APP_SPEC_TS_3$ = goog.getMsg("My i18n block #2 with value \uFFFD0\uFFFD\uFFFD#7\uFFFDPlain text in nested element (block #2)\uFFFD/#7\uFFFD");
|
||||||
|
const $MSG_APP_SPEC_TS_4$ = goog.getMsg("Span title \uFFFD0\uFFFD");
|
||||||
|
const $_c5$ = ["title", $MSG_APP_SPEC_TS_4$, 1];
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵelementStart(2, "span");
|
||||||
|
$r3$.ɵi18nAttribute(3, $_c2$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementStart(4, "div");
|
||||||
|
$r3$.ɵi18nStart(5, $MSG_APP_SPEC_TS_3$);
|
||||||
|
$r3$.ɵpipe(6, "uppercase");
|
||||||
|
$r3$.ɵelementStart(7, "span");
|
||||||
|
$r3$.ɵi18nAttribute(8, $_c5$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueB));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueC));
|
||||||
|
$r3$.ɵi18nApply(3);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueA));
|
||||||
|
$r3$.ɵi18nApply(1);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(ctx.valueE));
|
||||||
|
$r3$.ɵi18nApply(8);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(6, 0, ctx.valueD)));
|
||||||
|
$r3$.ɵi18nApply(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle i18n attributes in nested templates', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div>
|
||||||
|
Some content
|
||||||
|
<div *ngIf="visible">
|
||||||
|
<div i18n>
|
||||||
|
Some other content {{ valueA }}
|
||||||
|
<div>
|
||||||
|
More nested levels with bindings {{ valueB | uppercase }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $_c0$ = [1, "ngIf"];
|
||||||
|
const $MSG_APP_SPEC_TS__1$ = goog.getMsg("Some other content \uFFFD0\uFFFD\uFFFD#3\uFFFDMore nested levels with bindings \uFFFD1\uFFFD\uFFFD/#3\uFFFD");
|
||||||
|
…
|
||||||
|
function MyComponent_div_Template_2(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵelementStart(1, "div");
|
||||||
|
$r3$.ɵi18nStart(2, $MSG_APP_SPEC_TS__1$);
|
||||||
|
$r3$.ɵelement(3, "div");
|
||||||
|
$r3$.ɵpipe(4, "uppercase");
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $$ctx_r0$$ = $r3$.ɵnextContext();
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($$ctx_r0$$.valueA));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(4, 0, $$ctx_r0$$.valueB)));
|
||||||
|
$r3$.ɵi18nApply(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵtext(1, " Some content ");
|
||||||
|
$r3$.ɵtemplate(2, MyComponent_div_Template_2, 5, 2, null, $_c0$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵelementProperty(2, "ngIf", $r3$.ɵbind(ctx.visible));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle i18n context in nested templates', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-component',
|
||||||
|
template: \`
|
||||||
|
<div i18n>
|
||||||
|
Some content
|
||||||
|
<div *ngIf="visible">
|
||||||
|
Some other content {{ valueA }}
|
||||||
|
<div>
|
||||||
|
More nested levels with bindings {{ valueB | uppercase }}
|
||||||
|
<div *ngIf="exists">
|
||||||
|
Content inside sub-template {{ valueC }}
|
||||||
|
<div>
|
||||||
|
Bottom level element {{ valueD }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!visible">
|
||||||
|
Some other content {{ valueE + valueF }}
|
||||||
|
<div>
|
||||||
|
More nested levels with bindings {{ valueG | uppercase }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
\`
|
||||||
|
})
|
||||||
|
export class MyComponent {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = String.raw `
|
||||||
|
const $MSG_APP_SPEC_TS_0$ = goog.getMsg("Some content\uFFFD*2:1\uFFFD\uFFFD#1:1\uFFFDSome other content \uFFFD0:1\uFFFD\uFFFD#2:1\uFFFDMore nested levels with bindings \uFFFD1:1\uFFFD\uFFFD*4:2\uFFFD\uFFFD#1:2\uFFFDContent inside sub-template \uFFFD0:2\uFFFD\uFFFD#2:2\uFFFDBottom level element \uFFFD1:2\uFFFD\uFFFD/#2:2\uFFFD\uFFFD/#1:2\uFFFD\uFFFD/*4:2\uFFFD\uFFFD/#2:1\uFFFD\uFFFD/#1:1\uFFFD\uFFFD/*2:1\uFFFD\uFFFD*3:3\uFFFD\uFFFD#1:3\uFFFDSome other content \uFFFD0:3\uFFFD\uFFFD#2:3\uFFFDMore nested levels with bindings \uFFFD1:3\uFFFD\uFFFD/#2:3\uFFFD\uFFFD/#1:3\uFFFD\uFFFD/*3:3\uFFFD");
|
||||||
|
const $_c1$ = [1, "ngIf"];
|
||||||
|
…
|
||||||
|
function MyComponent_div_div_Template_4(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵi18nStart(0, $MSG_APP_SPEC_TS_0$, 2);
|
||||||
|
$r3$.ɵelementStart(1, "div");
|
||||||
|
$r3$.ɵelement(2, "div");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $ctx_r2$ = $r3$.ɵnextContext(2);
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r2$.valueC));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r2$.valueD));
|
||||||
|
$r3$.ɵi18nApply(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function MyComponent_div_Template_2(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵi18nStart(0, $MSG_APP_SPEC_TS_0$, 1);
|
||||||
|
$r3$.ɵelementStart(1, "div");
|
||||||
|
$r3$.ɵelementStart(2, "div");
|
||||||
|
$r3$.ɵpipe(3, "uppercase");
|
||||||
|
$r3$.ɵtemplate(4, MyComponent_div_div_Template_4, 3, 0, null, $_c1$);
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $ctx_r0$ = $r3$.ɵnextContext();
|
||||||
|
$r3$.ɵelementProperty(4, "ngIf", $r3$.ɵbind($ctx_r0$.exists));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($ctx_r0$.valueA));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 0, $ctx_r0$.valueB)));
|
||||||
|
$r3$.ɵi18nApply(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function MyComponent_div_Template_3(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵi18nStart(0, $MSG_APP_SPEC_TS_0$, 3);
|
||||||
|
$r3$.ɵelementStart(1, "div");
|
||||||
|
$r3$.ɵelement(2, "div");
|
||||||
|
$r3$.ɵpipe(3, "uppercase");
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
const $ctx_r1$ = $r3$.ɵnextContext();
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind(($ctx_r1$.valueE + $ctx_r1$.valueF)));
|
||||||
|
$r3$.ɵi18nExp($r3$.ɵbind($r3$.ɵpipeBind1(3, 0, $ctx_r1$.valueG)));
|
||||||
|
$r3$.ɵi18nApply(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div");
|
||||||
|
$r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$);
|
||||||
|
$r3$.ɵtemplate(2, MyComponent_div_Template_2, 5, 3, null, $_c1$);
|
||||||
|
$r3$.ɵtemplate(3, MyComponent_div_Template_3, 4, 2, null, $_c1$);
|
||||||
|
$r3$.ɵi18nEnd();
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵelementProperty(2, "ngIf", $r3$.ɵbind(ctx.visible));
|
||||||
|
$r3$.ɵelementProperty(3, "ngIf", $r3$.ɵbind(!ctx.visible));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect template');
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as o from './output/output_ast';
|
import * as o from './output/output_ast';
|
||||||
|
import {I18nMeta, parseI18nMeta} from './render3/view/i18n';
|
||||||
import {OutputContext, error} from './util';
|
import {OutputContext, error} from './util';
|
||||||
|
|
||||||
const CONSTANT_PREFIX = '_c';
|
const CONSTANT_PREFIX = '_c';
|
||||||
@ -78,6 +79,7 @@ class FixupExpression extends o.Expression {
|
|||||||
export class ConstantPool {
|
export class ConstantPool {
|
||||||
statements: o.Statement[] = [];
|
statements: o.Statement[] = [];
|
||||||
private translations = new Map<string, o.Expression>();
|
private translations = new Map<string, o.Expression>();
|
||||||
|
private deferredTranslations = new Map<o.ReadVarExpr, number>();
|
||||||
private literals = new Map<string, FixupExpression>();
|
private literals = new Map<string, FixupExpression>();
|
||||||
private literalFactories = new Map<string, o.Expression>();
|
private literalFactories = new Map<string, o.Expression>();
|
||||||
private injectorDefinitions = new Map<any, FixupExpression>();
|
private injectorDefinitions = new Map<any, FixupExpression>();
|
||||||
@ -113,6 +115,31 @@ export class ConstantPool {
|
|||||||
return fixup;
|
return fixup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDeferredTranslationConst(suffix: string): o.ReadVarExpr {
|
||||||
|
const index = this.statements.push(new o.ExpressionStatement(o.NULL_EXPR)) - 1;
|
||||||
|
const variable = o.variable(this.freshTranslationName(suffix));
|
||||||
|
this.deferredTranslations.set(variable, index);
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeferredTranslationConst(variable: o.ReadVarExpr, message: string): void {
|
||||||
|
const index = this.deferredTranslations.get(variable) !;
|
||||||
|
this.statements[index] = this.getTranslationDeclStmt(variable, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTranslationDeclStmt(variable: o.ReadVarExpr, message: string): o.DeclareVarStmt {
|
||||||
|
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
|
||||||
|
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTranslationMeta(meta: string|I18nMeta) {
|
||||||
|
const parsedMeta = typeof meta === 'string' ? parseI18nMeta(meta) : meta;
|
||||||
|
const docStmt = i18nMetaToDocStmt(parsedMeta);
|
||||||
|
if (docStmt) {
|
||||||
|
this.statements.push(docStmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generates closure specific code for translation.
|
// Generates closure specific code for translation.
|
||||||
//
|
//
|
||||||
// ```
|
// ```
|
||||||
@ -122,10 +149,11 @@ export class ConstantPool {
|
|||||||
// */
|
// */
|
||||||
// const MSG_XYZ = goog.getMsg('message');
|
// const MSG_XYZ = goog.getMsg('message');
|
||||||
// ```
|
// ```
|
||||||
getTranslation(message: string, meta: {description?: string, meaning?: string}, suffix: string):
|
getTranslation(message: string, meta: string, suffix: string): o.Expression {
|
||||||
o.Expression {
|
const parsedMeta = parseI18nMeta(meta);
|
||||||
|
|
||||||
// The identity of an i18n message depends on the message and its meaning
|
// The identity of an i18n message depends on the message and its meaning
|
||||||
const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message;
|
const key = parsedMeta.meaning ? `${message}\u0000\u0000${parsedMeta.meaning}` : message;
|
||||||
|
|
||||||
const exp = this.translations.get(key);
|
const exp = this.translations.get(key);
|
||||||
|
|
||||||
@ -133,16 +161,9 @@ export class ConstantPool {
|
|||||||
return exp;
|
return exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const docStmt = i18nMetaToDocStmt(meta);
|
|
||||||
if (docStmt) {
|
|
||||||
this.statements.push(docStmt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call closure to get the translation
|
|
||||||
const variable = o.variable(this.freshTranslationName(suffix));
|
const variable = o.variable(this.freshTranslationName(suffix));
|
||||||
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
|
this.appendTranslationMeta(parsedMeta);
|
||||||
const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
this.statements.push(this.getTranslationDeclStmt(variable, message));
|
||||||
this.statements.push(msgStmt);
|
|
||||||
|
|
||||||
this.translations.set(key, variable);
|
this.translations.set(key, variable);
|
||||||
return variable;
|
return variable;
|
||||||
@ -330,14 +351,14 @@ function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
|||||||
return e instanceof o.ReadVarExpr;
|
return e instanceof o.ReadVarExpr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement
|
// Converts i18n meta informations for a message (id, description, meaning)
|
||||||
// formatted as expected by the Closure compiler.
|
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||||
function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}):
|
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
|
||||||
o.JSDocCommentStmt|null {
|
|
||||||
const tags: o.JSDocTag[] = [];
|
const tags: o.JSDocTag[] = [];
|
||||||
|
|
||||||
if (meta.description) {
|
if (meta.id || meta.description) {
|
||||||
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
const text = meta.id ? `[BACKUP_MESSAGE_ID:${meta.id}] ${meta.description}` : meta.description;
|
||||||
|
tags.push({tagName: o.JSDocTagName.Desc, text: text !.trim()});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meta.meaning) {
|
if (meta.meaning) {
|
||||||
|
@ -205,8 +205,8 @@ export function compileComponentFromMetadata(
|
|||||||
|
|
||||||
const template = meta.template;
|
const template = meta.template;
|
||||||
const templateBuilder = new TemplateDefinitionBuilder(
|
const templateBuilder = new TemplateDefinitionBuilder(
|
||||||
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, meta.viewQueries,
|
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, null, null, templateName,
|
||||||
directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
|
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
|
||||||
meta.template.relativeContextFilePath);
|
meta.template.relativeContextFilePath);
|
||||||
|
|
||||||
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
|
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
|
||||||
|
140
packages/compiler/src/render3/view/i18n.ts
Normal file
140
packages/compiler/src/render3/view/i18n.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as o from '../../output/output_ast';
|
||||||
|
|
||||||
|
/** I18n separators for metadata **/
|
||||||
|
const I18N_MEANING_SEPARATOR = '|';
|
||||||
|
const I18N_ID_SEPARATOR = '@@';
|
||||||
|
|
||||||
|
/** Name of the i18n attributes **/
|
||||||
|
export const I18N_ATTR = 'i18n';
|
||||||
|
export const I18N_ATTR_PREFIX = 'i18n-';
|
||||||
|
|
||||||
|
/** Placeholder wrapper for i18n expressions **/
|
||||||
|
export const I18N_PLACEHOLDER_SYMBOL = '<27>';
|
||||||
|
|
||||||
|
// Parse i18n metas like:
|
||||||
|
// - "@@id",
|
||||||
|
// - "description[@@id]",
|
||||||
|
// - "meaning|description[@@id]"
|
||||||
|
export function parseI18nMeta(meta?: string): I18nMeta {
|
||||||
|
let id: string|undefined;
|
||||||
|
let meaning: string|undefined;
|
||||||
|
let description: string|undefined;
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
const idIndex = meta.indexOf(I18N_ID_SEPARATOR);
|
||||||
|
const descIndex = meta.indexOf(I18N_MEANING_SEPARATOR);
|
||||||
|
let meaningAndDesc: string;
|
||||||
|
[meaningAndDesc, id] =
|
||||||
|
(idIndex > -1) ? [meta.slice(0, idIndex), meta.slice(idIndex + 2)] : [meta, ''];
|
||||||
|
[meaning, description] = (descIndex > -1) ?
|
||||||
|
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
|
||||||
|
['', meaningAndDesc];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {id, meaning, description};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isI18NAttribute(name: string): boolean {
|
||||||
|
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wrapI18nPlaceholder(content: string | number, contextId: number = 0): string {
|
||||||
|
const blockId = contextId > 0 ? `:${contextId}` : '';
|
||||||
|
return `${I18N_PLACEHOLDER_SYMBOL}${content}${blockId}${I18N_PLACEHOLDER_SYMBOL}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assembleI18nBoundString(
|
||||||
|
strings: Array<string>, bindingStartIndex: number = 0, contextId: number = 0): string {
|
||||||
|
if (!strings.length) return '';
|
||||||
|
let acc = '';
|
||||||
|
const lastIdx = strings.length - 1;
|
||||||
|
for (let i = 0; i < lastIdx; i++) {
|
||||||
|
acc += `${strings[i]}${wrapI18nPlaceholder(bindingStartIndex + i, contextId)}`;
|
||||||
|
}
|
||||||
|
acc += strings[lastIdx];
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeqNumberGenerator(startsAt: number = 0): () => number {
|
||||||
|
let current = startsAt;
|
||||||
|
return () => current++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type I18nMeta = {
|
||||||
|
id?: string,
|
||||||
|
description?: string,
|
||||||
|
meaning?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* I18nContext is a helper class which keeps track of all i18n-related aspects
|
||||||
|
* (accumulates content, bindings, etc) between i18nStart and i18nEnd instructions.
|
||||||
|
*
|
||||||
|
* When we enter a nested template, the top-level context is being passed down
|
||||||
|
* to the nested component, which uses this context to generate a child instance
|
||||||
|
* of I18nContext class (to handle nested template) and at the end, reconciles it back
|
||||||
|
* with the parent context.
|
||||||
|
*/
|
||||||
|
export class I18nContext {
|
||||||
|
private id: number;
|
||||||
|
private content: string = '';
|
||||||
|
private bindings = new Set<o.Expression>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private index: number, private templateIndex: number|null, private ref: any,
|
||||||
|
private level: number = 0, private uniqueIdGen?: () => number) {
|
||||||
|
this.uniqueIdGen = uniqueIdGen || getSeqNumberGenerator();
|
||||||
|
this.id = this.uniqueIdGen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private wrap(symbol: string, elementIndex: number, contextId: number, closed?: boolean) {
|
||||||
|
const state = closed ? '/' : '';
|
||||||
|
return wrapI18nPlaceholder(`${state}${symbol}${elementIndex}`, contextId);
|
||||||
|
}
|
||||||
|
private append(content: string) { this.content += content; }
|
||||||
|
private genTemplatePattern(contextId: number|string, templateId: number|string): string {
|
||||||
|
return wrapI18nPlaceholder(`tmpl:${contextId}:${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() { return this.id; }
|
||||||
|
getRef() { return this.ref; }
|
||||||
|
getIndex() { return this.index; }
|
||||||
|
getContent() { return this.content; }
|
||||||
|
getTemplateIndex() { return this.templateIndex; }
|
||||||
|
|
||||||
|
getBindings() { return this.bindings; }
|
||||||
|
appendBinding(binding: o.Expression) { this.bindings.add(binding); }
|
||||||
|
|
||||||
|
isRoot() { return this.level === 0; }
|
||||||
|
isResolved() {
|
||||||
|
const regex = new RegExp(this.genTemplatePattern('\\d+', '\\d+'));
|
||||||
|
return !regex.test(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendText(content: string) { this.append(content.trim()); }
|
||||||
|
appendTemplate(index: number) { this.append(this.genTemplatePattern(this.id, index)); }
|
||||||
|
appendElement(elementIndex: number, closed?: boolean) {
|
||||||
|
this.append(this.wrap('#', elementIndex, this.id, closed));
|
||||||
|
}
|
||||||
|
|
||||||
|
forkChildContext(index: number, templateIndex: number) {
|
||||||
|
return new I18nContext(index, templateIndex, this.ref, this.level + 1, this.uniqueIdGen);
|
||||||
|
}
|
||||||
|
reconcileChildContext(context: I18nContext) {
|
||||||
|
const id = context.getId();
|
||||||
|
const content = context.getContent();
|
||||||
|
const templateIndex = context.getTemplateIndex() !;
|
||||||
|
const pattern = new RegExp(this.genTemplatePattern(this.id, templateIndex));
|
||||||
|
const replacement =
|
||||||
|
`${this.wrap('*', templateIndex, id)}${content}${this.wrap('*', templateIndex, id, true)}`;
|
||||||
|
this.content = this.content.replace(pattern, replacement);
|
||||||
|
}
|
||||||
|
}
|
@ -29,8 +29,9 @@ import {Identifiers as R3} from '../r3_identifiers';
|
|||||||
import {htmlAstToRender3Ast} from '../r3_template_transform';
|
import {htmlAstToRender3Ast} from '../r3_template_transform';
|
||||||
|
|
||||||
import {R3QueryMetadata} from './api';
|
import {R3QueryMetadata} from './api';
|
||||||
|
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nContext, assembleI18nBoundString} from './i18n';
|
||||||
import {parseStyle} from './styling';
|
import {parseStyle} from './styling';
|
||||||
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, assembleI18nTemplate, getAttrsForDirectiveMatching, invalid, isI18NAttribute, mapToExpression, trimTrailingNulls, unsupported} from './util';
|
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||||
|
|
||||||
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -85,11 +86,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
private _valueConverter: ValueConverter;
|
private _valueConverter: ValueConverter;
|
||||||
private _unsupported = unsupported;
|
private _unsupported = unsupported;
|
||||||
|
|
||||||
// Whether we are inside a translatable element (`<p i18n>... somewhere here ... </p>)
|
// i18n context local to this template
|
||||||
private _inI18nSection: boolean = false;
|
private i18n: I18nContext|null = null;
|
||||||
private _i18nSectionIndex = -1;
|
|
||||||
// Maps of placeholder to node indexes for each of the i18n section
|
|
||||||
private _phToNodeIdxes: {[phName: string]: number[]}[] = [{}];
|
|
||||||
|
|
||||||
// Number of slots to reserve for pureFunctions
|
// Number of slots to reserve for pureFunctions
|
||||||
private _pureFunctionSlots = 0;
|
private _pureFunctionSlots = 0;
|
||||||
@ -101,7 +99,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
|
private constantPool: ConstantPool, parentBindingScope: BindingScope, private level = 0,
|
||||||
private contextName: string|null, private templateName: string|null,
|
private contextName: string|null, private i18nContext: I18nContext|null,
|
||||||
|
private templateIndex: number|null, private templateName: string|null,
|
||||||
private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null,
|
private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null,
|
||||||
private directives: Set<o.Expression>, private pipeTypeByName: Map<string, o.Expression>,
|
private directives: Set<o.Expression>, private pipeTypeByName: Map<string, o.Expression>,
|
||||||
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference,
|
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference,
|
||||||
@ -176,6 +175,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
this.creationInstruction(null, R3.projectionDef, parameters);
|
this.creationInstruction(null, R3.projectionDef, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.i18nContext) {
|
||||||
|
this.i18nStart();
|
||||||
|
}
|
||||||
|
|
||||||
// This is the initial pass through the nodes of this template. In this pass, we
|
// This is the initial pass through the nodes of this template. In this pass, we
|
||||||
// queue all creation mode and update mode instructions for generation in the second
|
// queue all creation mode and update mode instructions for generation in the second
|
||||||
// pass. It's necessary to separate the passes to ensure local refs are defined before
|
// pass. It's necessary to separate the passes to ensure local refs are defined before
|
||||||
@ -195,6 +198,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
// instructions can be generated with the correct internal const count.
|
// instructions can be generated with the correct internal const count.
|
||||||
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
|
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
|
||||||
|
|
||||||
|
if (this.i18nContext) {
|
||||||
|
this.i18nEnd();
|
||||||
|
}
|
||||||
|
|
||||||
// Generate all the creation mode instructions (e.g. resolve bindings in listeners)
|
// Generate all the creation mode instructions (e.g. resolve bindings in listeners)
|
||||||
const creationStatements = this._creationCodeFns.map((fn: () => o.Statement) => fn());
|
const creationStatements = this._creationCodeFns.map((fn: () => o.Statement) => fn());
|
||||||
|
|
||||||
@ -215,17 +222,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
[renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] :
|
[renderFlagCheckIfStmt(core.RenderFlags.Update, updateVariables.concat(updateStatements))] :
|
||||||
[];
|
[];
|
||||||
|
|
||||||
// Generate maps of placeholder name to node indexes
|
|
||||||
// TODO(vicb): This is a WIP, not fully supported yet
|
|
||||||
for (const phToNodeIdx of this._phToNodeIdxes) {
|
|
||||||
if (Object.keys(phToNodeIdx).length > 0) {
|
|
||||||
const scopedName = this._bindingScope.freshReferenceName();
|
|
||||||
const phMap = o.variable(scopedName).set(mapToExpression(phToNodeIdx, true)).toConstDecl();
|
|
||||||
|
|
||||||
this._prefixCode.push(phMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return o.fn(
|
return o.fn(
|
||||||
// i.e. (rf: RenderFlags, ctx: any)
|
// i.e. (rf: RenderFlags, ctx: any)
|
||||||
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)],
|
[new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)],
|
||||||
@ -243,8 +239,60 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
// LocalResolver
|
// LocalResolver
|
||||||
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
|
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
|
||||||
|
|
||||||
i18nTranslate(label: string, meta?: string): o.Expression {
|
i18nTranslate(label: string, meta: string = ''): o.Expression {
|
||||||
return this.constantPool.getTranslation(label, parseI18nMeta(meta), this.fileBasedI18nSuffix);
|
return this.constantPool.getTranslation(label, meta, this.fileBasedI18nSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nAppendTranslationMeta(meta: string = '') { this.constantPool.appendTranslationMeta(meta); }
|
||||||
|
|
||||||
|
i18nAllocateRef(): o.ReadVarExpr {
|
||||||
|
return this.constantPool.getDeferredTranslationConst(this.fileBasedI18nSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nUpdateRef(context: I18nContext): void {
|
||||||
|
if (context.isRoot() && context.isResolved()) {
|
||||||
|
this.constantPool.setDeferredTranslationConst(context.getRef(), context.getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nStart(span: ParseSourceSpan|null = null, meta?: string): void {
|
||||||
|
const index = this.allocateDataSlot();
|
||||||
|
if (this.i18nContext) {
|
||||||
|
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !);
|
||||||
|
} else {
|
||||||
|
this.i18nAppendTranslationMeta(meta);
|
||||||
|
const ref = this.i18nAllocateRef();
|
||||||
|
this.i18n = new I18nContext(index, this.templateIndex, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate i18nStart instruction
|
||||||
|
const params: o.Expression[] = [o.literal(index), this.i18n.getRef()];
|
||||||
|
if (this.i18n.getId() > 0) {
|
||||||
|
// do not push 3rd argument (sub-block id)
|
||||||
|
// into i18nStart call for top level i18n context
|
||||||
|
params.push(o.literal(this.i18n.getId()));
|
||||||
|
}
|
||||||
|
this.creationInstruction(span, R3.i18nStart, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
i18nEnd(span: ParseSourceSpan|null = null): void {
|
||||||
|
if (this.i18nContext) {
|
||||||
|
this.i18nContext.reconcileChildContext(this.i18n !);
|
||||||
|
this.i18nUpdateRef(this.i18nContext);
|
||||||
|
} else {
|
||||||
|
this.i18nUpdateRef(this.i18n !);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup accumulated bindings
|
||||||
|
const bindings = this.i18n !.getBindings();
|
||||||
|
if (bindings.size) {
|
||||||
|
bindings.forEach(binding => { this.updateInstruction(span, R3.i18nExp, [binding]); });
|
||||||
|
const index: o.Expression = o.literal(this.i18n !.getIndex());
|
||||||
|
this.updateInstruction(span, R3.i18nApply, [index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.creationInstruction(span, R3.i18nEnd);
|
||||||
|
this.i18n = null; // reset local i18n context
|
||||||
}
|
}
|
||||||
|
|
||||||
visitContent(ngContent: t.Content) {
|
visitContent(ngContent: t.Content) {
|
||||||
@ -289,7 +337,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
|
|
||||||
visitElement(element: t.Element) {
|
visitElement(element: t.Element) {
|
||||||
const elementIndex = this.allocateDataSlot();
|
const elementIndex = this.allocateDataSlot();
|
||||||
const wasInI18nSection = this._inI18nSection;
|
|
||||||
|
let isNonBindableMode: boolean = false;
|
||||||
|
let isI18nRootElement: boolean = false;
|
||||||
|
|
||||||
const outputAttrs: {[name: string]: string} = {};
|
const outputAttrs: {[name: string]: string} = {};
|
||||||
const attrI18nMetas: {[name: string]: string} = {};
|
const attrI18nMetas: {[name: string]: string} = {};
|
||||||
@ -298,18 +348,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
const [namespaceKey, elementName] = splitNsName(element.name);
|
const [namespaceKey, elementName] = splitNsName(element.name);
|
||||||
const isNgContainer = checkIsNgContainer(element.name);
|
const isNgContainer = checkIsNgContainer(element.name);
|
||||||
|
|
||||||
// Elements inside i18n sections are replaced with placeholders
|
|
||||||
// TODO(vicb): nested elements are a WIP in this phase
|
|
||||||
if (this._inI18nSection) {
|
|
||||||
const phName = element.name.toLowerCase();
|
|
||||||
if (!this._phToNodeIdxes[this._i18nSectionIndex][phName]) {
|
|
||||||
this._phToNodeIdxes[this._i18nSectionIndex][phName] = [];
|
|
||||||
}
|
|
||||||
this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isNonBindableMode: boolean = false;
|
|
||||||
|
|
||||||
// Handle i18n and ngNonBindable attributes
|
// Handle i18n and ngNonBindable attributes
|
||||||
for (const attr of element.attributes) {
|
for (const attr of element.attributes) {
|
||||||
const name = attr.name;
|
const name = attr.name;
|
||||||
@ -317,13 +355,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
if (name === NON_BINDABLE_ATTR) {
|
if (name === NON_BINDABLE_ATTR) {
|
||||||
isNonBindableMode = true;
|
isNonBindableMode = true;
|
||||||
} else if (name === I18N_ATTR) {
|
} else if (name === I18N_ATTR) {
|
||||||
if (this._inI18nSection) {
|
if (this.i18n) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not mark an element as translatable inside of a translatable section`);
|
`Could not mark an element as translatable inside of a translatable section`);
|
||||||
}
|
}
|
||||||
this._inI18nSection = true;
|
isI18nRootElement = true;
|
||||||
this._i18nSectionIndex++;
|
|
||||||
this._phToNodeIdxes[this._i18nSectionIndex] = {};
|
|
||||||
i18nMeta = value;
|
i18nMeta = value;
|
||||||
} else if (name.startsWith(I18N_ATTR_PREFIX)) {
|
} else if (name.startsWith(I18N_ATTR_PREFIX)) {
|
||||||
attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value;
|
attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value;
|
||||||
@ -486,8 +522,22 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
|
|
||||||
const implicit = o.variable(CONTEXT_NAME);
|
const implicit = o.variable(CONTEXT_NAME);
|
||||||
|
|
||||||
|
if (this.i18n) {
|
||||||
|
this.i18n.appendElement(elementIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChildren = () => {
|
||||||
|
if (!isI18nRootElement && this.i18n) {
|
||||||
|
// we do not append text node instructions inside i18n section, so we
|
||||||
|
// exclude them while calculating whether current element has children
|
||||||
|
return element.children.find(
|
||||||
|
child => !(child instanceof t.Text || child instanceof t.BoundText));
|
||||||
|
}
|
||||||
|
return element.children.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
const createSelfClosingInstruction = !hasStylingInstructions && !isNgContainer &&
|
const createSelfClosingInstruction = !hasStylingInstructions && !isNgContainer &&
|
||||||
element.children.length === 0 && element.outputs.length === 0 && i18nAttrs.length === 0;
|
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
|
||||||
|
|
||||||
if (createSelfClosingInstruction) {
|
if (createSelfClosingInstruction) {
|
||||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
||||||
@ -500,6 +550,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isI18nRootElement) {
|
||||||
|
this.i18nStart(element.sourceSpan, i18nMeta);
|
||||||
|
}
|
||||||
|
|
||||||
// process i18n element attributes
|
// process i18n element attributes
|
||||||
if (i18nAttrs.length) {
|
if (i18nAttrs.length) {
|
||||||
let hasBindings: boolean = false;
|
let hasBindings: boolean = false;
|
||||||
@ -514,7 +568,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
const converted = value.visit(this._valueConverter);
|
const converted = value.visit(this._valueConverter);
|
||||||
if (converted instanceof Interpolation) {
|
if (converted instanceof Interpolation) {
|
||||||
const {strings, expressions} = converted;
|
const {strings, expressions} = converted;
|
||||||
const label = assembleI18nTemplate(strings);
|
const label = assembleI18nBoundString(strings);
|
||||||
i18nAttrArgs.push(
|
i18nAttrArgs.push(
|
||||||
o.literal(name), this.i18nTranslate(label, meta), o.literal(expressions.length));
|
o.literal(name), this.i18nTranslate(label, meta), o.literal(expressions.length));
|
||||||
expressions.forEach(expression => {
|
expressions.forEach(expression => {
|
||||||
@ -690,31 +744,32 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Traverse element child nodes
|
// Traverse element child nodes
|
||||||
if (this._inI18nSection && element.children.length == 1 &&
|
t.visitAll(this, element.children);
|
||||||
element.children[0] instanceof t.Text) {
|
|
||||||
const text = element.children[0] as t.Text;
|
if (!isI18nRootElement && this.i18n) {
|
||||||
this.visitSingleI18nTextChild(text, i18nMeta);
|
this.i18n.appendElement(elementIndex, true);
|
||||||
} else {
|
|
||||||
t.visitAll(this, element.children);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createSelfClosingInstruction) {
|
if (!createSelfClosingInstruction) {
|
||||||
// Finish element construction mode.
|
// Finish element construction mode.
|
||||||
if (isNonBindableMode) {
|
const span = element.endSourceSpan || element.sourceSpan;
|
||||||
this.creationInstruction(element.endSourceSpan || element.sourceSpan, R3.enableBindings);
|
if (isI18nRootElement) {
|
||||||
|
this.i18nEnd(span);
|
||||||
}
|
}
|
||||||
this.creationInstruction(
|
if (isNonBindableMode) {
|
||||||
element.endSourceSpan || element.sourceSpan,
|
this.creationInstruction(span, R3.enableBindings);
|
||||||
isNgContainer ? R3.elementContainerEnd : R3.elementEnd);
|
}
|
||||||
|
this.creationInstruction(span, isNgContainer ? R3.elementContainerEnd : R3.elementEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the state before exiting this node
|
|
||||||
this._inI18nSection = wasInI18nSection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visitTemplate(template: t.Template) {
|
visitTemplate(template: t.Template) {
|
||||||
const templateIndex = this.allocateDataSlot();
|
const templateIndex = this.allocateDataSlot();
|
||||||
|
|
||||||
|
if (this.i18n) {
|
||||||
|
this.i18n.appendTemplate(templateIndex);
|
||||||
|
}
|
||||||
|
|
||||||
let elName = '';
|
let elName = '';
|
||||||
if (template.children.length === 1 && template.children[0] instanceof t.Element) {
|
if (template.children.length === 1 && template.children[0] instanceof t.Element) {
|
||||||
// When the template as a single child, derive the context name from the tag
|
// When the template as a single child, derive the context name from the tag
|
||||||
@ -763,9 +818,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
|
|
||||||
// Create the template function
|
// Create the template function
|
||||||
const templateVisitor = new TemplateDefinitionBuilder(
|
const templateVisitor = new TemplateDefinitionBuilder(
|
||||||
this.constantPool, this._bindingScope, this.level + 1, contextName, templateName, [],
|
this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n,
|
||||||
this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace,
|
templateIndex, templateName, [], this.directiveMatcher, this.directives,
|
||||||
this.fileBasedI18nSuffix);
|
this.pipeTypeByName, this.pipes, this._namespace, this.fileBasedI18nSuffix);
|
||||||
|
|
||||||
// Nested templates must not be visited until after their parent templates have completed
|
// Nested templates must not be visited until after their parent templates have completed
|
||||||
// processing, so they are queued here until after the initial pass. Otherwise, we wouldn't
|
// processing, so they are queued here until after the initial pass. Otherwise, we wouldn't
|
||||||
@ -801,6 +856,22 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
readonly visitBoundEvent = invalid;
|
readonly visitBoundEvent = invalid;
|
||||||
|
|
||||||
visitBoundText(text: t.BoundText) {
|
visitBoundText(text: t.BoundText) {
|
||||||
|
if (this.i18n) {
|
||||||
|
const value = text.value.visit(this._valueConverter);
|
||||||
|
if (value instanceof Interpolation) {
|
||||||
|
const {strings, expressions} = value;
|
||||||
|
const label =
|
||||||
|
assembleI18nBoundString(strings, this.i18n.getBindings().size, this.i18n.getId());
|
||||||
|
const implicit = o.variable(CONTEXT_NAME);
|
||||||
|
expressions.forEach(expression => {
|
||||||
|
const binding = this.convertExpressionBinding(implicit, expression);
|
||||||
|
this.i18n !.appendBinding(binding);
|
||||||
|
});
|
||||||
|
this.i18n.appendText(label);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nodeIndex = this.allocateDataSlot();
|
const nodeIndex = this.allocateDataSlot();
|
||||||
|
|
||||||
this.creationInstruction(text.sourceSpan, R3.text, [o.literal(nodeIndex)]);
|
this.creationInstruction(text.sourceSpan, R3.text, [o.literal(nodeIndex)]);
|
||||||
@ -813,28 +884,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
visitText(text: t.Text) {
|
visitText(text: t.Text) {
|
||||||
|
if (this.i18n) {
|
||||||
|
this.i18n.appendText(text.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.creationInstruction(
|
this.creationInstruction(
|
||||||
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), o.literal(text.value)]);
|
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), o.literal(text.value)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the content of the element is a single text node the translation can be inlined:
|
|
||||||
//
|
|
||||||
// `<p i18n="desc|mean">some content</p>`
|
|
||||||
// compiles to
|
|
||||||
// ```
|
|
||||||
// /**
|
|
||||||
// * @desc desc
|
|
||||||
// * @meaning mean
|
|
||||||
// */
|
|
||||||
// const MSG_XYZ = goog.getMsg('some content');
|
|
||||||
// i0.ɵtext(1, MSG_XYZ);
|
|
||||||
// ```
|
|
||||||
visitSingleI18nTextChild(text: t.Text, i18nMeta: string) {
|
|
||||||
const variable = this.i18nTranslate(text.value, i18nMeta);
|
|
||||||
this.creationInstruction(
|
|
||||||
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), variable]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private allocateDataSlot() { return this._dataIndex++; }
|
private allocateDataSlot() { return this._dataIndex++; }
|
||||||
|
|
||||||
getConstCount() { return this._dataIndex; }
|
getConstCount() { return this._dataIndex; }
|
||||||
@ -1355,31 +1412,6 @@ function createCssSelector(tag: string, attributes: {[name: string]: string}): C
|
|||||||
return cssSelector;
|
return cssSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse i18n metas like:
|
|
||||||
// - "@@id",
|
|
||||||
// - "description[@@id]",
|
|
||||||
// - "meaning|description[@@id]"
|
|
||||||
function parseI18nMeta(i18n?: string): {description?: string, id?: string, meaning?: string} {
|
|
||||||
let meaning: string|undefined;
|
|
||||||
let description: string|undefined;
|
|
||||||
let id: string|undefined;
|
|
||||||
|
|
||||||
if (i18n) {
|
|
||||||
// TODO(vicb): figure out how to force a message ID with closure ?
|
|
||||||
const idIndex = i18n.indexOf(ID_SEPARATOR);
|
|
||||||
|
|
||||||
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
|
|
||||||
let meaningAndDesc: string;
|
|
||||||
[meaningAndDesc, id] =
|
|
||||||
(idIndex > -1) ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
|
|
||||||
[meaning, description] = (descIndex > -1) ?
|
|
||||||
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
|
|
||||||
['', meaningAndDesc];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {description, id, meaning};
|
|
||||||
}
|
|
||||||
|
|
||||||
function interpolate(args: o.Expression[]): o.Expression {
|
function interpolate(args: o.Expression[]): o.Expression {
|
||||||
args = args.slice(1); // Ignore the length prefix added for render2
|
args = args.slice(1); // Ignore the length prefix added for render2
|
||||||
switch (args.length) {
|
switch (args.length) {
|
||||||
|
@ -11,6 +11,7 @@ import * as o from '../../output/output_ast';
|
|||||||
import * as t from '../r3_ast';
|
import * as t from '../r3_ast';
|
||||||
|
|
||||||
import {R3QueryMetadata} from './api';
|
import {R3QueryMetadata} from './api';
|
||||||
|
import {isI18NAttribute} from './i18n';
|
||||||
|
|
||||||
/** Name of the temporary to use during data binding */
|
/** Name of the temporary to use during data binding */
|
||||||
export const TEMPORARY_NAME = '_t';
|
export const TEMPORARY_NAME = '_t';
|
||||||
@ -27,17 +28,6 @@ export const REFERENCE_PREFIX = '_r';
|
|||||||
/** The name of the implicit context reference */
|
/** The name of the implicit context reference */
|
||||||
export const IMPLICIT_REFERENCE = '$implicit';
|
export const IMPLICIT_REFERENCE = '$implicit';
|
||||||
|
|
||||||
/** Name of the i18n attributes **/
|
|
||||||
export const I18N_ATTR = 'i18n';
|
|
||||||
export const I18N_ATTR_PREFIX = 'i18n-';
|
|
||||||
|
|
||||||
/** I18n separators for metadata **/
|
|
||||||
export const MEANING_SEPARATOR = '|';
|
|
||||||
export const ID_SEPARATOR = '@@';
|
|
||||||
|
|
||||||
/** Placeholder wrapper for i18n expressions **/
|
|
||||||
export const I18N_PLACEHOLDER_SYMBOL = '<27>';
|
|
||||||
|
|
||||||
/** Non bindable attribute name **/
|
/** Non bindable attribute name **/
|
||||||
export const NON_BINDABLE_ATTR = 'ngNonBindable';
|
export const NON_BINDABLE_ATTR = 'ngNonBindable';
|
||||||
|
|
||||||
@ -70,25 +60,6 @@ export function invalid<T>(arg: o.Expression | o.Statement | t.Node): never {
|
|||||||
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
|
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isI18NAttribute(name: string): boolean {
|
|
||||||
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wrapI18nPlaceholder(content: string | number): string {
|
|
||||||
return `${I18N_PLACEHOLDER_SYMBOL}${content}${I18N_PLACEHOLDER_SYMBOL}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assembleI18nTemplate(strings: Array<string>): string {
|
|
||||||
if (!strings.length) return '';
|
|
||||||
let acc = '';
|
|
||||||
const lastIdx = strings.length - 1;
|
|
||||||
for (let i = 0; i < lastIdx; i++) {
|
|
||||||
acc += `${strings[i]}${wrapI18nPlaceholder(i)}`;
|
|
||||||
}
|
|
||||||
acc += strings[lastIdx];
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asLiteral(value: any): o.Expression {
|
export function asLiteral(value: any): o.Expression {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return o.literalArr(value.map(asLiteral));
|
return o.literalArr(value.map(asLiteral));
|
||||||
|
70
packages/compiler/test/render3/view/i18n_spec.ts
Normal file
70
packages/compiler/test/render3/view/i18n_spec.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as o from '../../../src/output/output_ast';
|
||||||
|
import {I18nContext} from '../../../src/render3/view/i18n';
|
||||||
|
|
||||||
|
describe('I18nContext', () => {
|
||||||
|
it('should support i18n content collection', () => {
|
||||||
|
const ctx = new I18nContext(5, null, 'myRef');
|
||||||
|
|
||||||
|
// basic checks
|
||||||
|
expect(ctx.isRoot()).toBe(true);
|
||||||
|
expect(ctx.isResolved()).toBe(true);
|
||||||
|
expect(ctx.getId()).toBe(0);
|
||||||
|
expect(ctx.getIndex()).toBe(5);
|
||||||
|
expect(ctx.getTemplateIndex()).toBeNull();
|
||||||
|
expect(ctx.getRef()).toBe('myRef');
|
||||||
|
|
||||||
|
// data collection checks
|
||||||
|
expect(ctx.getContent()).toBe('');
|
||||||
|
ctx.appendText('Foo');
|
||||||
|
ctx.appendElement(1);
|
||||||
|
ctx.appendText('Bar');
|
||||||
|
ctx.appendElement(1, true);
|
||||||
|
expect(ctx.getContent()).toBe('Foo<6F>#1<>Bar<61>/#1<>');
|
||||||
|
|
||||||
|
// binding collection checks
|
||||||
|
expect(ctx.getBindings().size).toBe(0);
|
||||||
|
ctx.appendBinding(o.literal(1));
|
||||||
|
ctx.appendBinding(o.literal(2));
|
||||||
|
expect(ctx.getBindings().size).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support nested contexts', () => {
|
||||||
|
const ctx = new I18nContext(5, null, 'myRef');
|
||||||
|
const templateIndex = 1;
|
||||||
|
|
||||||
|
// set some data for root ctx
|
||||||
|
ctx.appendText('Foo');
|
||||||
|
ctx.appendBinding(o.literal(1));
|
||||||
|
ctx.appendTemplate(templateIndex);
|
||||||
|
expect(ctx.isResolved()).toBe(false);
|
||||||
|
|
||||||
|
// create child context
|
||||||
|
const childCtx = ctx.forkChildContext(6, templateIndex);
|
||||||
|
expect(childCtx.getContent()).toBe('');
|
||||||
|
expect(childCtx.getBindings().size).toBe(0);
|
||||||
|
expect(childCtx.getRef()).toBe(ctx.getRef()); // ref should be passed into child ctx
|
||||||
|
expect(childCtx.isRoot()).toBe(false);
|
||||||
|
|
||||||
|
childCtx.appendText('Bar');
|
||||||
|
childCtx.appendElement(2);
|
||||||
|
childCtx.appendText('Baz');
|
||||||
|
childCtx.appendElement(2, true);
|
||||||
|
childCtx.appendBinding(o.literal(2));
|
||||||
|
childCtx.appendBinding(o.literal(3));
|
||||||
|
|
||||||
|
expect(childCtx.getContent()).toBe('Bar<61>#2:1<>Baz<61>/#2:1<>');
|
||||||
|
expect(childCtx.getBindings().size).toBe(2);
|
||||||
|
|
||||||
|
// reconcile
|
||||||
|
ctx.reconcileChildContext(childCtx);
|
||||||
|
expect(ctx.getContent()).toBe('Foo<6F>*1:1<>Bar<61>#2:1<>Baz<61>/#2:1<><31>/*1:1<>');
|
||||||
|
});
|
||||||
|
});
|
@ -737,6 +737,12 @@
|
|||||||
{
|
{
|
||||||
"name": "I18NHtmlParser"
|
"name": "I18NHtmlParser"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "I18N_ID_SEPARATOR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "I18N_MEANING_SEPARATOR"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "I18nError"
|
"name": "I18nError"
|
||||||
},
|
},
|
||||||
@ -3449,6 +3455,9 @@
|
|||||||
{
|
{
|
||||||
"name": "parseCookieValue"
|
"name": "parseCookieValue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "parseI18nMeta"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "parseIntAutoRadix"
|
"name": "parseIntAutoRadix"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user