From 8a3fd58cad4cb2769724e7c732876e90ac8aee8d Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Fri, 12 Oct 2018 14:34:38 -0700 Subject: [PATCH] feat(ivy): i18n compiler support for i18nStart and i18nEnd instructions (#26442) PR Close #26442 --- .../compliance/r3_view_compiler_i18n_spec.ts | 604 +++++++++++++++--- packages/compiler/src/constant_pool.ts | 57 +- .../compiler/src/render3/view/compiler.ts | 4 +- packages/compiler/src/render3/view/i18n.ts | 140 ++++ .../compiler/src/render3/view/template.ts | 230 ++++--- packages/compiler/src/render3/view/util.ts | 31 +- .../compiler/test/render3/view/i18n_spec.ts | 70 ++ .../hello_world_r2/bundle.golden_symbols.json | 9 + 8 files changed, 913 insertions(+), 232 deletions(-) create mode 100644 packages/compiler/src/render3/view/i18n.ts create mode 100644 packages/compiler/test/render3/view/i18n_spec.ts diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 4f05fbdf7a..4b748509e5 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -9,8 +9,6 @@ import {setup} from '@angular/compiler/test/aot/test_util'; import {compile, expectEmit} from './mock_compile'; -const TRANSLATION_NAME_REGEXP = /^MSG_[A-Z0-9]+/; - describe('i18n support in the view compiler', () => { const angularFiles = setup({ compileAngular: false, @@ -18,56 +16,7 @@ describe('i18n support in the view compiler', () => { compileAnimations: false, }); - describe('single text nodes', () => { - 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: \` -
Hello world
-
&
-
farewell
-
farewell
- \` - }) - 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, - }); - }); - + describe('element attributes', () => { it('should add the meaning and description as JsDoc comments', () => { const files = { app: { @@ -77,7 +26,12 @@ describe('i18n support in the view compiler', () => { @Component({ selector: 'my-component', template: \` -
Hello world
+
Content A
+
Content B
+
Content C
+
Content D
+
Content E
+
Content F
\` }) export class MyComponent {} @@ -90,22 +44,63 @@ describe('i18n support in the view compiler', () => { const template = ` /** - * @desc desc + * @desc [BACKUP_MESSAGE_ID:idA] descA + * @meaning meaningA */ - const $MSG_APP_SPEC_TS_0$ = goog.getMsg("introduction"); - const $_c1$ = ["title", $MSG_APP_SPEC_TS_0$, 0]; - … + const $MSG_APP_SPEC_TS_0$ = goog.getMsg("Content A"); /** - * @desc desc - * @meaning meaning + * @desc [BACKUP_MESSAGE_ID:idB] descB + * @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) { if (rf & 1) { $r3$.ɵelementStart(0, "div"); - $r3$.ɵi18nAttribute(1, $_c1$); - $r3$.ɵtext(2, $MSG_APP_SPEC_TS_2$); + $r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$); + $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(); } } @@ -114,9 +109,6 @@ describe('i18n support in the view compiler', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); }); - }); - - describe('element attributes', () => { it('should translate static attributes', () => { const files = { @@ -127,7 +119,7 @@ describe('i18n support in the view compiler', () => { @Component({ selector: 'my-component', template: \` -
+
\` }) export class MyComponent {} @@ -169,12 +161,12 @@ describe('i18n support in the view compiler', () => { @Component({ selector: 'my-component', template: \` -
-
@@ -184,7 +176,7 @@ describe('i18n support in the view compiler', () => { @NgModule({declarations: [MyComponent]}) export class MyModule {} - ` + ` } }; @@ -307,12 +299,12 @@ describe('i18n support in the view compiler', () => { @Component({ selector: 'my-component', template: \` -
-
@@ -437,9 +429,8 @@ describe('i18n support in the view compiler', () => { }); }); - // TODO(vicb): this feature is not supported yet - xdescribe('nested nodes', () => { - it('should generate the placeholders maps', () => { + describe('nested nodes', () => { + it('should not produce instructions for empty content', () => { const files = { app: { 'spec.ts': ` @@ -448,24 +439,471 @@ describe('i18n support in the view compiler', () => { @Component({ selector: 'my-component', template: \` -
Hello {{name}}!!
-
Other
-
2nd
-
3rd
+
+
\` }) export class MyComponent {} @NgModule({declarations: [MyComponent]}) export class MyModule {} - ` + ` } }; - const template = ` - const $r1$ = {"b":[2], "i":[4, 6]}; - const $r2$ = {"i":[13]}; - `; + const template = String.raw ` + template: function MyComponent_Template(rf, ctx) { + 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: \` +
My i18n block #1
+
My non-i18n block #1
+
My i18n block #2
+
My non-i18n block #2
+
My i18n block #3
+ \` + }) + 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: \` +
My i18n block #{{ one }}
+
My i18n block #{{ two | uppercase }}
+
My i18n block #{{ three + four + five }}
+ \` + }) + 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: \` +
+ My i18n block #{{ one }} + Plain text in nested element +
+
+ My i18n block #{{ two | uppercase }} +
+
+ + More bindings in more nested element: {{ nestedInBlockTwo }} + +
+
+
+ \` + }) + 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: \` +
+ My i18n block #1 with value: {{ valueA }} + + Plain text in nested element (block #1) + +
+
+ My i18n block #2 with value {{ valueD | uppercase }} + + Plain text in nested element (block #2) + +
+ \` + }) + 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: \` +
+ Some content +
+
+ Some other content {{ valueA }} +
+ More nested levels with bindings {{ valueB | uppercase }} +
+
+
+
+ \` + }) + 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: \` +
+ Some content +
+ Some other content {{ valueA }} +
+ More nested levels with bindings {{ valueB | uppercase }} +
+ Content inside sub-template {{ valueC }} +
+ Bottom level element {{ valueD }} +
+
+
+
+
+ Some other content {{ valueE + valueF }} +
+ More nested levels with bindings {{ valueG | uppercase }} +
+
+
+ \` + }) + 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); expectEmit(result.source, template, 'Incorrect template'); diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 33575cc4cf..2f3856ebd7 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -7,6 +7,7 @@ */ import * as o from './output/output_ast'; +import {I18nMeta, parseI18nMeta} from './render3/view/i18n'; import {OutputContext, error} from './util'; const CONSTANT_PREFIX = '_c'; @@ -78,6 +79,7 @@ class FixupExpression extends o.Expression { export class ConstantPool { statements: o.Statement[] = []; private translations = new Map(); + private deferredTranslations = new Map(); private literals = new Map(); private literalFactories = new Map(); private injectorDefinitions = new Map(); @@ -113,6 +115,31 @@ export class ConstantPool { 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. // // ``` @@ -122,10 +149,11 @@ export class ConstantPool { // */ // const MSG_XYZ = goog.getMsg('message'); // ``` - getTranslation(message: string, meta: {description?: string, meaning?: string}, suffix: string): - o.Expression { + getTranslation(message: string, meta: string, suffix: string): o.Expression { + const parsedMeta = parseI18nMeta(meta); + // 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); @@ -133,16 +161,9 @@ export class ConstantPool { 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 fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]); - const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]); - this.statements.push(msgStmt); + this.appendTranslationMeta(parsedMeta); + this.statements.push(this.getTranslationDeclStmt(variable, message)); this.translations.set(key, variable); return variable; @@ -330,14 +351,14 @@ function isVariable(e: o.Expression): e is o.ReadVarExpr { return e instanceof o.ReadVarExpr; } -// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement -// formatted as expected by the Closure compiler. -function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}): - o.JSDocCommentStmt|null { +// Converts i18n meta informations for a message (id, description, meaning) +// to a JsDoc statement formatted as expected by the Closure compiler. +function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null { const tags: o.JSDocTag[] = []; - if (meta.description) { - tags.push({tagName: o.JSDocTagName.Desc, text: meta.description}); + if (meta.id || 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) { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 6595a7c70e..0833790599 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -205,8 +205,8 @@ export function compileComponentFromMetadata( const template = meta.template; const templateBuilder = new TemplateDefinitionBuilder( - constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, meta.viewQueries, - directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML, + constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, null, null, templateName, + meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML, meta.template.relativeContextFilePath); const templateFunctionExpression = templateBuilder.buildTemplateFunction( diff --git a/packages/compiler/src/render3/view/i18n.ts b/packages/compiler/src/render3/view/i18n.ts new file mode 100644 index 0000000000..be03f589f8 --- /dev/null +++ b/packages/compiler/src/render3/view/i18n.ts @@ -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 = '�'; + +// 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, 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(); + + 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); + } +} \ No newline at end of file diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index d81a3681be..918d17a734 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -29,8 +29,9 @@ import {Identifiers as R3} from '../r3_identifiers'; import {htmlAstToRender3Ast} from '../r3_template_transform'; import {R3QueryMetadata} from './api'; +import {I18N_ATTR, I18N_ATTR_PREFIX, I18nContext, assembleI18nBoundString} from './i18n'; 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 { switch (type) { @@ -85,11 +86,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private _valueConverter: ValueConverter; private _unsupported = unsupported; - // Whether we are inside a translatable element (`

... somewhere here ...

) - private _inI18nSection: boolean = false; - private _i18nSectionIndex = -1; - // Maps of placeholder to node indexes for each of the i18n section - private _phToNodeIdxes: {[phName: string]: number[]}[] = [{}]; + // i18n context local to this template + private i18n: I18nContext|null = null; // Number of slots to reserve for pureFunctions private _pureFunctionSlots = 0; @@ -101,7 +99,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver constructor( 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 directives: Set, private pipeTypeByName: Map, private pipes: Set, private _namespace: o.ExternalReference, @@ -176,6 +175,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver 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 // 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 @@ -195,6 +198,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // instructions can be generated with the correct internal const count. this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn()); + if (this.i18nContext) { + this.i18nEnd(); + } + // Generate all the creation mode instructions (e.g. resolve bindings in listeners) const creationStatements = this._creationCodeFns.map((fn: () => o.Statement) => fn()); @@ -215,17 +222,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver [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( // i.e. (rf: RenderFlags, ctx: any) [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)], @@ -243,8 +239,60 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // LocalResolver getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); } - i18nTranslate(label: string, meta?: string): o.Expression { - return this.constantPool.getTranslation(label, parseI18nMeta(meta), this.fileBasedI18nSuffix); + i18nTranslate(label: string, meta: string = ''): o.Expression { + 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) { @@ -289,7 +337,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver visitElement(element: t.Element) { const elementIndex = this.allocateDataSlot(); - const wasInI18nSection = this._inI18nSection; + + let isNonBindableMode: boolean = false; + let isI18nRootElement: boolean = false; const outputAttrs: {[name: string]: string} = {}; const attrI18nMetas: {[name: string]: string} = {}; @@ -298,18 +348,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const [namespaceKey, elementName] = splitNsName(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 for (const attr of element.attributes) { const name = attr.name; @@ -317,13 +355,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (name === NON_BINDABLE_ATTR) { isNonBindableMode = true; } else if (name === I18N_ATTR) { - if (this._inI18nSection) { + if (this.i18n) { throw new Error( `Could not mark an element as translatable inside of a translatable section`); } - this._inI18nSection = true; - this._i18nSectionIndex++; - this._phToNodeIdxes[this._i18nSectionIndex] = {}; + isI18nRootElement = true; i18nMeta = value; } else if (name.startsWith(I18N_ATTR_PREFIX)) { attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value; @@ -486,8 +522,22 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver 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 && - element.children.length === 0 && element.outputs.length === 0 && i18nAttrs.length === 0; + element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren(); if (createSelfClosingInstruction) { this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters)); @@ -500,6 +550,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.creationInstruction(element.sourceSpan, R3.disableBindings); } + if (isI18nRootElement) { + this.i18nStart(element.sourceSpan, i18nMeta); + } + // process i18n element attributes if (i18nAttrs.length) { let hasBindings: boolean = false; @@ -514,7 +568,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const converted = value.visit(this._valueConverter); if (converted instanceof Interpolation) { const {strings, expressions} = converted; - const label = assembleI18nTemplate(strings); + const label = assembleI18nBoundString(strings); i18nAttrArgs.push( o.literal(name), this.i18nTranslate(label, meta), o.literal(expressions.length)); expressions.forEach(expression => { @@ -690,31 +744,32 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); // Traverse element child nodes - if (this._inI18nSection && element.children.length == 1 && - element.children[0] instanceof t.Text) { - const text = element.children[0] as t.Text; - this.visitSingleI18nTextChild(text, i18nMeta); - } else { - t.visitAll(this, element.children); + t.visitAll(this, element.children); + + if (!isI18nRootElement && this.i18n) { + this.i18n.appendElement(elementIndex, true); } if (!createSelfClosingInstruction) { // Finish element construction mode. - if (isNonBindableMode) { - this.creationInstruction(element.endSourceSpan || element.sourceSpan, R3.enableBindings); + const span = element.endSourceSpan || element.sourceSpan; + if (isI18nRootElement) { + this.i18nEnd(span); } - this.creationInstruction( - element.endSourceSpan || element.sourceSpan, - isNgContainer ? R3.elementContainerEnd : R3.elementEnd); + if (isNonBindableMode) { + this.creationInstruction(span, R3.enableBindings); + } + this.creationInstruction(span, isNgContainer ? R3.elementContainerEnd : R3.elementEnd); } - - // Restore the state before exiting this node - this._inI18nSection = wasInI18nSection; } visitTemplate(template: t.Template) { const templateIndex = this.allocateDataSlot(); + if (this.i18n) { + this.i18n.appendTemplate(templateIndex); + } + let elName = ''; 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 @@ -763,9 +818,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Create the template function const templateVisitor = new TemplateDefinitionBuilder( - this.constantPool, this._bindingScope, this.level + 1, contextName, templateName, [], - this.directiveMatcher, this.directives, this.pipeTypeByName, this.pipes, this._namespace, - this.fileBasedI18nSuffix); + this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n, + templateIndex, templateName, [], this.directiveMatcher, this.directives, + this.pipeTypeByName, this.pipes, this._namespace, this.fileBasedI18nSuffix); // 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 @@ -801,6 +856,22 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver readonly visitBoundEvent = invalid; 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(); this.creationInstruction(text.sourceSpan, R3.text, [o.literal(nodeIndex)]); @@ -813,28 +884,14 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } visitText(text: t.Text) { + if (this.i18n) { + this.i18n.appendText(text.value); + return; + } this.creationInstruction( 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: - // - // `

some content

` - // 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++; } getConstCount() { return this._dataIndex; } @@ -1355,31 +1412,6 @@ function createCssSelector(tag: string, attributes: {[name: string]: string}): C 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 { args = args.slice(1); // Ignore the length prefix added for render2 switch (args.length) { diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index 1dbe8da9ac..30996f47c9 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -11,6 +11,7 @@ import * as o from '../../output/output_ast'; import * as t from '../r3_ast'; import {R3QueryMetadata} from './api'; +import {isI18NAttribute} from './i18n'; /** Name of the temporary to use during data binding */ export const TEMPORARY_NAME = '_t'; @@ -27,17 +28,6 @@ export const REFERENCE_PREFIX = '_r'; /** The name of the implicit context reference */ 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 = '�'; - /** Non bindable attribute name **/ export const NON_BINDABLE_ATTR = 'ngNonBindable'; @@ -70,25 +60,6 @@ export function invalid(arg: o.Expression | o.Statement | t.Node): never { `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 { - 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 { if (Array.isArray(value)) { return o.literalArr(value.map(asLiteral)); diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts new file mode 100644 index 0000000000..f7343dd0f2 --- /dev/null +++ b/packages/compiler/test/render3/view/i18n_spec.ts @@ -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�#1�Bar�/#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�#2:1�Baz�/#2:1�'); + expect(childCtx.getBindings().size).toBe(2); + + // reconcile + ctx.reconcileChildContext(childCtx); + expect(ctx.getContent()).toBe('Foo�*1:1�Bar�#2:1�Baz�/#2:1��/*1:1�'); + }); +}); \ No newline at end of file diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index 799d28f8d5..a9cd18fc12 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -737,6 +737,12 @@ { "name": "I18NHtmlParser" }, + { + "name": "I18N_ID_SEPARATOR" + }, + { + "name": "I18N_MEANING_SEPARATOR" + }, { "name": "I18nError" }, @@ -3449,6 +3455,9 @@ { "name": "parseCookieValue" }, + { + "name": "parseI18nMeta" + }, { "name": "parseIntAutoRadix" },