diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index daa8221a91..9128b9901d 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -7,7 +7,7 @@
*/
import {LifecycleHooksFeature, renderComponent, whenRendered} from './component';
import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
-import {getHostElement, getRenderedText} from './discovery_utils';
+import {getComponent, getHostElement, getRenderedText} from './discovery_utils';
import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {ProvidersFeature} from './features/providers_feature';
@@ -169,6 +169,7 @@ export {
defineBase,
definePipe,
getHostElement,
+ getComponent,
getRenderedText,
renderComponent,
whenRendered,
diff --git a/packages/core/test/bundling/todo/index.ts b/packages/core/test/bundling/todo/index.ts
index eef6e234ae..a16c76ecd3 100644
--- a/packages/core/test/bundling/todo/index.ts
+++ b/packages/core/test/bundling/todo/index.ts
@@ -179,5 +179,4 @@ class ToDoAppComponent {
class ToDoAppModule {
}
-// TODO(misko): create cleaner way to publish component into global location for tests.
-(window as any).toDoAppComponent = renderComponent(ToDoAppComponent);
+renderComponent(ToDoAppComponent);
diff --git a/packages/core/test/bundling/todo/todo_e2e_spec.ts b/packages/core/test/bundling/todo/todo_e2e_spec.ts
index 924210b50e..2beca5a55e 100644
--- a/packages/core/test/bundling/todo/todo_e2e_spec.ts
+++ b/packages/core/test/bundling/todo/todo_e2e_spec.ts
@@ -8,6 +8,7 @@
import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
+import {getComponent} from '@angular/core/src/render3';
import {withBody} from '@angular/private/testing';
import * as path from 'path';
@@ -22,8 +23,7 @@ describe('functional test for todo', () => {
describe(bundle, () => {
it('should render todo', withBody('', async() => {
require(path.join(PACKAGE, bundle));
- // TODO(misko): have cleaner way to do this for tests.
- const toDoAppComponent = (window as any).toDoAppComponent;
+ const toDoAppComponent = getComponent(document.querySelector('todo-app') !);
expect(document.body.textContent).toContain('todos');
expect(document.body.textContent).toContain('Demonstrate Components');
expect(document.body.textContent).toContain('4 items left');
diff --git a/packages/core/test/bundling/todo_i18n/BUILD.bazel b/packages/core/test/bundling/todo_i18n/BUILD.bazel
new file mode 100644
index 0000000000..d2dea295a8
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/BUILD.bazel
@@ -0,0 +1,115 @@
+package(default_visibility = ["//visibility:public"])
+
+load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ng_rollup_bundle", "ts_library")
+load("//tools/http-server:http_server.bzl", "http_server")
+load("@build_bazel_rules_typescript//:defs.bzl", "ts_devserver")
+
+ng_module(
+ name = "todo_i18n",
+ srcs = [
+ "index.ts",
+ "translations.ts",
+ ],
+ tags = [
+ "ivy-only",
+ ],
+ deps = [
+ "//packages/common",
+ "//packages/core",
+ "//packages/core/test/bundling/util:reflect_metadata",
+ ],
+)
+
+ng_rollup_bundle(
+ name = "bundle",
+ # TODO(alexeagle): This is inconsistent.
+ # We try to teach users to always have their workspace at the start of a
+ # path, to disambiguate from other workspaces.
+ # Here, the rule implementation is looking in an execroot where the layout
+ # has an "external" directory for external dependencies.
+ # This should probably start with "angular/" and let the rule deal with it.
+ entry_point = "packages/core/test/bundling/todo_i18n/index.js",
+ tags = [
+ "ivy-only",
+ ],
+ deps = [
+ ":todo_i18n",
+ "//packages/common",
+ "//packages/core",
+ "//packages/core/test/bundling/util:reflect_metadata",
+ ],
+)
+
+ts_library(
+ name = "test_lib",
+ testonly = True,
+ srcs = glob(["*_spec.ts"]),
+ tags = [
+ "ivy-only",
+ ],
+ deps = [
+ "//packages:types",
+ "//packages/compiler",
+ "//packages/core",
+ "//packages/core/testing",
+ "//packages/private/testing",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ data = [
+ ":bundle",
+ ":bundle.js",
+ ":bundle.min.js",
+ ":bundle.min_debug.js",
+ ],
+ tags = [
+ "ivy-only",
+ ],
+ deps = [":test_lib"],
+)
+
+genrule(
+ name = "tslib",
+ srcs = [
+ "@ngdeps//node_modules/tslib:tslib.js",
+ ],
+ outs = [
+ "tslib.js",
+ ],
+ cmd = "cp $< $@",
+ tags = [
+ "ivy-only",
+ ],
+)
+
+ts_devserver(
+ name = "devserver",
+ entry_module = "angular/packages/core/test/bundling/todo_i18n/index",
+ serving_path = "/bundle.min.js",
+ static_files = [
+ "index.html",
+ ":tslib",
+ "todo.css",
+ "base.css",
+ ],
+ tags = [
+ "ivy-only",
+ ],
+ deps = [":todo_i18n"],
+)
+
+http_server(
+ name = "prodserver",
+ data = [
+ "base.css",
+ "index.html",
+ "todo.css",
+ ":bundle.min.js.br",
+ ":bundle.min_debug.js",
+ ],
+ tags = [
+ "ivy-only",
+ ],
+)
diff --git a/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md
new file mode 100644
index 0000000000..c2baf47676
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md
@@ -0,0 +1,36 @@
+# Outstanding on the `Todo` app
+
+## `Todo` app
+- [X] Clicking archive removes todo item.
+- [X] Update `Todo` app to match http://todomvc.com/.
+- [ ] Make it work with `[(ngModel)]`.
+- [ ] Make it work with `(keyup.Enter)`.
+
+## Compiler
+- [ ] Remove ` tslib_1.__decorate([core_1.Input(), tslib_1.__metadata("design:type", Object)], TodoComponent.prototype, "todo", void 0);` from generated output.
+- [ ] Allow compilation of `@angular/common` through ivy.
+
+## Ivy Runtime
+- [X] Work on `ViewContainerRef` needs to cause change detection so that `todo` app renders correctly on first render.
+- [X] The todo input value box is not correctly rendering to checked for completed tasks.
+- [ ] `ViewContainerRef` must separate creation mode from update mode otherwise {{todo.done}} fails for `NgFor` because `todo` is not set during creation mode.
+- [ ] Injector should be optional
+
+## Testing
+- [ ] Create a debug mode which would publish components into DOM for easier writing of tests.
+
+
+## Bazel
+
+- [ ] Have action verb on the `ng_rollup_bundle` to display source maps.
+
+# NOTES
+
+## Killing hung `iblaze` server
+
+At times the `iblaze run packages/core/test/bundling/todo:devserver` keeps running and holding onto
+ports even after `ctrl-c`. This command kills the outstanding processes.
+
+```
+kill -9 $(ps aux | grep ibazel\\\|devserver | cut -c 17-23)
+```
\ No newline at end of file
diff --git a/packages/core/test/bundling/todo_i18n/base.css b/packages/core/test/bundling/todo_i18n/base.css
new file mode 100644
index 0000000000..9f6ac1bd74
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/base.css
@@ -0,0 +1,141 @@
+hr {
+ margin: 20px 0;
+ border: 0;
+ border-top: 1px dashed #c5c5c5;
+ border-bottom: 1px dashed #f7f7f7;
+}
+
+.learn a {
+ font-weight: normal;
+ text-decoration: none;
+ color: #b83f45;
+}
+
+.learn a:hover {
+ text-decoration: underline;
+ color: #787e7e;
+}
+
+.learn h3,
+.learn h4,
+.learn h5 {
+ margin: 10px 0;
+ font-weight: 500;
+ line-height: 1.2;
+ color: #000;
+}
+
+.learn h3 {
+ font-size: 24px;
+}
+
+.learn h4 {
+ font-size: 18px;
+}
+
+.learn h5 {
+ margin-bottom: 0;
+ font-size: 14px;
+}
+
+.learn ul {
+ padding: 0;
+ margin: 0 0 30px 25px;
+}
+
+.learn li {
+ line-height: 20px;
+}
+
+.learn p {
+ font-size: 15px;
+ font-weight: 300;
+ line-height: 1.3;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#issue-count {
+ display: none;
+}
+
+.quote {
+ border: none;
+ margin: 20px 0 60px 0;
+}
+
+.quote p {
+ font-style: italic;
+}
+
+.quote p:before {
+ content: '“';
+ font-size: 50px;
+ opacity: .15;
+ position: absolute;
+ top: -20px;
+ left: 3px;
+}
+
+.quote p:after {
+ content: '”';
+ font-size: 50px;
+ opacity: .15;
+ position: absolute;
+ bottom: -42px;
+ right: 3px;
+}
+
+.quote footer {
+ position: absolute;
+ bottom: -40px;
+ right: 0;
+}
+
+.quote footer img {
+ border-radius: 3px;
+}
+
+.quote footer a {
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+.speech-bubble {
+ position: relative;
+ padding: 10px;
+ background: rgba(0, 0, 0, .04);
+ border-radius: 5px;
+}
+
+.speech-bubble:after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ right: 30px;
+ border: 13px solid transparent;
+ border-top-color: rgba(0, 0, 0, .04);
+}
+
+.learn-bar > .learn {
+ position: absolute;
+ width: 272px;
+ top: 8px;
+ left: -300px;
+ padding: 10px;
+ border-radius: 5px;
+ background-color: rgba(255, 255, 255, .6);
+ transition-property: left;
+ transition-duration: 500ms;
+}
+
+@media (min-width: 899px) {
+ .learn-bar {
+ width: auto;
+ padding-left: 300px;
+ }
+
+ .learn-bar > .learn {
+ left: 8px;
+ }
+}
\ No newline at end of file
diff --git a/packages/core/test/bundling/todo_i18n/index.html b/packages/core/test/bundling/todo_i18n/index.html
new file mode 100644
index 0000000000..f902b92519
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ Angular Todo Example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/test/bundling/todo_i18n/index.ts b/packages/core/test/bundling/todo_i18n/index.ts
new file mode 100644
index 0000000000..1fe523c042
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/index.ts
@@ -0,0 +1,181 @@
+/**
+ * @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 '@angular/core/test/bundling/util/src/reflect_metadata';
+import {CommonModule} from '@angular/common';
+import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
+// TODO(ocombe): replace this with the real runtime i18n service
+import {localize} from './translations';
+
+class Todo {
+ editing: boolean;
+
+ // TODO(issue/24571): remove '!'.
+ private _title !: string;
+ get title() { return this._title; }
+ set title(value: string) { this._title = value.trim(); }
+
+ constructor(title: string, public completed: boolean = false) {
+ this.editing = false;
+ this.title = title;
+ }
+}
+
+@Injectable({providedIn: 'root'})
+class TodoStore {
+ todos: Array = [
+ new Todo(localize('Demonstrate Components')),
+ new Todo(localize('Demonstrate Structural Directives'), true),
+ // Using a placeholder
+ new Todo(localize('Demonstrate {$value}', {value: 'NgModules'})),
+ new Todo(localize('Demonstrate zoneless change detection')),
+ new Todo(localize('Demonstrate internationalization')),
+ ];
+
+ private getWithCompleted(completed: boolean) {
+ return this.todos.filter((todo: Todo) => todo.completed === completed);
+ }
+
+ allCompleted() { return this.todos.length === this.getCompleted().length; }
+
+ setAllTo(completed: boolean) { this.todos.forEach((t: Todo) => t.completed = completed); }
+
+ removeCompleted() { this.todos = this.getWithCompleted(false); }
+
+ getRemaining() { return this.getWithCompleted(false); }
+
+ getCompleted() { return this.getWithCompleted(true); }
+
+ toggleCompletion(todo: Todo) { todo.completed = !todo.completed; }
+
+ remove(todo: Todo) { this.todos.splice(this.todos.indexOf(todo), 1); }
+
+ add(title: string) { this.todos.push(new Todo(title)); }
+}
+
+@Component({
+ selector: 'todo-app',
+ // TODO(misko): make this work with `[(ngModel)]`
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+
+
+
+
+ `,
+ // TODO(misko): switch over to OnPush
+ // changeDetection: ChangeDetectionStrategy.OnPush
+})
+class ToDoAppComponent {
+ newTodoText = '';
+
+ constructor(public todoStore: TodoStore) {}
+
+ cancelEditingTodo(todo: Todo) {
+ todo.editing = false;
+ markDirty(this);
+ }
+
+ finishUpdatingTodo(todo: Todo, editedTitle: string) {
+ editedTitle = editedTitle.trim();
+
+ if (editedTitle.length === 0) {
+ this.remove(todo);
+ }
+
+ todo.title = editedTitle;
+ this.cancelEditingTodo(todo);
+ }
+
+ editTodo(todo: Todo) {
+ todo.editing = true;
+ markDirty(this);
+ }
+
+ removeCompleted() {
+ this.todoStore.removeCompleted();
+ markDirty(this);
+ }
+
+ toggleCompletion(todo: Todo) {
+ this.todoStore.toggleCompletion(todo);
+ markDirty(this);
+ }
+
+ remove(todo: Todo) {
+ this.todoStore.remove(todo);
+ markDirty(this);
+ }
+
+ addTodo() {
+ if (this.newTodoText.trim().length) {
+ this.todoStore.add(this.newTodoText);
+ this.newTodoText = '';
+ }
+ markDirty(this);
+ }
+
+ toggleAllTodos(checked: boolean) {
+ this.todoStore.setAllTo(checked);
+ markDirty(this);
+ }
+
+ updateEditedTodoValue(todo: Todo, value: string) {
+ todo.title = value;
+ markDirty(this);
+ }
+
+ updateNewTodoValue(value: string) {
+ this.newTodoText = value;
+ markDirty(this);
+ }
+}
+
+@NgModule({declarations: [ToDoAppComponent], imports: [CommonModule]})
+class ToDoAppModule {
+}
+
+renderComponent(ToDoAppComponent);
diff --git a/packages/core/test/bundling/todo_i18n/todo.css b/packages/core/test/bundling/todo_i18n/todo.css
new file mode 100644
index 0000000000..19dadcf7e4
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/todo.css
@@ -0,0 +1,378 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ font-weight: 300;
+}
+
+button,
+input[type="checkbox"] {
+ outline: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 80px;
+ line-height: 80px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+.toggle-all {
+ position: absolute;
+ top: -55px;
+ left: -12px;
+ width: 60px;
+ height: 34px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+ content: '❯';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li .toggle:checked:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li label {
+ white-space: pre-line;
+ word-break: break-all;
+ padding: 15px 60px 15px 15px;
+ margin-left: 45px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: '×';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a.selected,
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ font-size: 10px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+
+ .toggle-all {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-appearance: none;
+ appearance: none;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
diff --git a/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts
new file mode 100644
index 0000000000..81d97c21ce
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/todo_e2e_spec.ts
@@ -0,0 +1,36 @@
+/**
+ * @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 '@angular/compiler';
+import {ɵwhenRendered as whenRendered} from '@angular/core';
+import {getComponent} from '@angular/core/src/render3';
+import {withBody} from '@angular/private/testing';
+import * as path from 'path';
+
+const PACKAGE = 'angular/packages/core/test/bundling/todo_i18n';
+const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js'];
+
+describe('functional test for todo i18n', () => {
+ BUNDLES.forEach(bundle => {
+ describe(bundle, () => {
+ it('should render todo i18n', withBody('', async() => {
+ require(path.join(PACKAGE, bundle));
+ const toDoAppComponent = getComponent(document.querySelector('todo-app') !);
+ expect(document.body.textContent).toContain('liste de tâches');
+ expect(document.body.textContent).toContain('Démontrer les components');
+ expect(document.body.textContent).toContain('Démontrer NgModules');
+ expect(document.body.textContent).toContain('4 tâches restantes');
+ expect(document.querySelector('.new-todo') !.getAttribute('placeholder'))
+ .toEqual(`Qu'y a-t-il à faire ?`);
+ document.querySelector('button') !.click();
+ await whenRendered(toDoAppComponent);
+ expect(document.body.textContent).toContain('3 tâches restantes');
+ }));
+ });
+ });
+});
diff --git a/packages/core/test/bundling/todo_i18n/translations.ts b/packages/core/test/bundling/todo_i18n/translations.ts
new file mode 100644
index 0000000000..05de50a8c9
--- /dev/null
+++ b/packages/core/test/bundling/todo_i18n/translations.ts
@@ -0,0 +1,42 @@
+/**
+ * @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
+ */
+
+declare var global: any;
+declare var window: any;
+
+export const translations: {[key: string]: string} = {
+ 'What needs to be done?': `Qu'y a-t-il à faire ?`,
+ '{$startHeadingLevel1}todos{$closeHeadingLevel1}{$tagInput}':
+ '{$startHeadingLevel1}liste de tâches{$closeHeadingLevel1}{$tagInput}',
+ '{VAR_PLURAL, plural, =1 {item left} other {items left}}':
+ '{VAR_PLURAL, plural, =1 {tâche restante} other {tâches restantes}}',
+ '{$startTagStrong}{$interpolation}{$closeTagStrong}{$icu}':
+ '{$startTagStrong}{$interpolation}{$closeTagStrong} {$icu}',
+ ' Clear completed ': ' Effacer terminés ',
+ 'Demonstrate Components': 'Démontrer les components',
+ 'Demonstrate Structural Directives': 'Démontrer les directives structurelles',
+ 'Demonstrate {$value}': 'Démontrer {$value}',
+ 'Demonstrate zoneless change detection': 'Démontrer la détection des changements sans zonejs',
+ 'Demonstrate internationalization': `Démontrer l'internationalisation`
+};
+
+// Runtime i18n uses Closure goog.getMsg for now
+// It will be replaced by the runtime service for external people
+const glob = typeof global !== 'undefined' ? global : window;
+glob.goog = glob.goog || {};
+glob.goog.getMsg =
+ glob.goog.getMsg || function(input: string, placeholders: {[key: string]: string} = {}) {
+ if (typeof translations[input] !== 'undefined') { // to account for empty string
+ input = translations[input];
+ }
+ return Object.keys(placeholders).length ?
+ input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
+ input;
+ };
+
+export const localize = goog.getMsg;
diff --git a/packages/goog.d.ts b/packages/goog.d.ts
index 69473ca3d3..f347fc0f85 100644
--- a/packages/goog.d.ts
+++ b/packages/goog.d.ts
@@ -16,6 +16,7 @@ declare namespace goog {
* as it is sometimes true.
*/
export const DEBUG: boolean;
+ export const getMsg: (input: string, placeholders?: {[key: string]: string}) => string;
}
/**
diff --git a/packages/private/testing/src/goog_get_msg.ts b/packages/private/testing/src/goog_get_msg.ts
index a4b63dcaf8..902339e955 100644
--- a/packages/private/testing/src/goog_get_msg.ts
+++ b/packages/private/testing/src/goog_get_msg.ts
@@ -13,11 +13,14 @@
* running outside of Closure Compiler. This method will not be needed once runtime translation
* service support is introduced.
*/
-export function polyfillGoogGetMsg(): void {
+export function polyfillGoogGetMsg(translations: {[key: string]: string} = {}): void {
const glob = (global as any);
glob.goog = glob.goog || {};
glob.goog.getMsg =
glob.goog.getMsg || function(input: string, placeholders: {[key: string]: string} = {}) {
+ if (typeof translations[input] !== 'undefined') { // to account for empty string
+ input = translations[input];
+ }
return Object.keys(placeholders).length ?
input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
input;