docs(i18n): add new features (#2740)
This commit is contained in:
parent
4a536732bc
commit
9fd3fa037d
|
@ -1,4 +1,4 @@
|
||||||
'use strict'; // necessary for es6 output in node
|
'use strict'; // necessary for es6 output in node
|
||||||
|
|
||||||
import { browser, element, by } from 'protractor';
|
import { browser, element, by } from 'protractor';
|
||||||
|
|
||||||
|
@ -8,8 +8,26 @@ describe('i18n E2E Tests', () => {
|
||||||
browser.get('');
|
browser.get('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display i18n translated welcome: Bonjour i18n!', function () {
|
it('should display i18n translated welcome: ¡Hola i18n!', function () {
|
||||||
expect(element(by.css('h1')).getText()).toEqual('Bonjour i18n!');
|
expect(element(by.css('h1')).getText()).toEqual('¡Hola i18n!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the node texts without elements', function () {
|
||||||
|
expect(element(by.css('my-app')).getText()).toContain('No genero ningún elemento');
|
||||||
|
expect(element(by.css('my-app')).getText()).toContain('Yo tampoco genero ningún elemento');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the translated title attribute', function () {
|
||||||
|
const title = element(by.css('img')).getAttribute('title');
|
||||||
|
expect(title).toBe('Logo de Angular 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the plural of: a horde of wolves', function () {
|
||||||
|
expect(element.all(by.css('span')).get(0).getText()).toBe('ningún lobo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the select of gender', function () {
|
||||||
|
expect(element.all(by.css('span')).get(1).getText()).toBe('El heroe es mujer');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
**/*.ngfactory.ts
|
**/*.ngfactory.ts
|
||||||
**/*.metadata.json
|
**/*.metadata.json
|
||||||
**/messages.xlf
|
|
||||||
dist
|
dist
|
||||||
!app/tsconfig.json
|
!app/tsconfig.json
|
||||||
!rollup.js
|
!rollup.js
|
||||||
|
|
|
@ -9,3 +9,7 @@
|
||||||
<!--#docregion i18n-attribute-desc-->
|
<!--#docregion i18n-attribute-desc-->
|
||||||
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
|
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
|
||||||
<!--#enddocregion i18n-attribute-desc-->
|
<!--#enddocregion i18n-attribute-desc-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-title-->
|
||||||
|
<img [src]="logo" title="Angular 2 logo">
|
||||||
|
<!--#enddocregion i18n-title-->
|
||||||
|
|
|
@ -1,4 +1,34 @@
|
||||||
<!--#docregion-->
|
<!--#docregion-->
|
||||||
<!--#docregion i18n-attribute-meaning-->
|
<!--#docregion i18n-attribute-meaning-->
|
||||||
<h1 i18n="User welcome|An introduction header for this sample">Hello i18n!</h1>
|
<h1 i18n="User welcome|An introduction header for this sample">Hello i18n!</h1>
|
||||||
<!--#enddocregion i18n-attribute-meaning-->
|
<!--#enddocregion i18n-attribute-meaning-->
|
||||||
|
|
||||||
|
<!--#docregion i18n-ng-container-->
|
||||||
|
<ng-container i18n>I don't output any element</ng-container>
|
||||||
|
<!--#enddocregion i18n-ng-container-->
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<!--#docregion i18n-with-comment-->
|
||||||
|
<!--i18n: optional meaning|optional description -->
|
||||||
|
I don't output any element either
|
||||||
|
<!--/i18n-->
|
||||||
|
<!--#enddocregion i18n-with-comment-->
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<!--#docregion i18n-title-translate-->
|
||||||
|
<img [src]="logo" i18n-title title="Angular 2 logo" />
|
||||||
|
<!--#enddocregion i18n-title-translate-->
|
||||||
|
<br>
|
||||||
|
<button (click)="inc(1)">+</button> <button (click)="inc(-1)">-</button>
|
||||||
|
<!--#docregion i18n-plural-->
|
||||||
|
<span i18n>{wolves, plural, =0 {no wolves} =1 {one wolf} =2 {two wolves} other {a wolf pack}}</span>
|
||||||
|
<!--#enddocregion i18n-plural-->
|
||||||
|
({{wolves}})
|
||||||
|
<br><br>
|
||||||
|
<button (click)="male()">♂</button> <button (click)="female()">♀</button>
|
||||||
|
<!--#docregion i18n-select-->
|
||||||
|
<span i18n>The hero is {gender, select, m {male} f {female}}</span>
|
||||||
|
<!--#enddocregion i18n-select-->
|
||||||
|
<br>
|
||||||
|
|
|
@ -6,5 +6,15 @@ import { Component } from '@angular/core';
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
templateUrl: 'app.component.html'
|
templateUrl: 'app.component.html'
|
||||||
})
|
})
|
||||||
export class AppComponent { }
|
export class AppComponent {
|
||||||
|
wolves = 0;
|
||||||
|
gender = 'f';
|
||||||
|
fly = true;
|
||||||
|
logo = 'https://angular.io/resources/images/logos/angular2/angular.png';
|
||||||
|
inc(i: number) {
|
||||||
|
this.wolves = Math.min(5, Math.max(0, this.wolves + i));
|
||||||
|
}
|
||||||
|
male() { this.gender = 'm'; }
|
||||||
|
female() { this.gender = 'f'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function getTranslationProviders(): Promise<Object[]> {
|
||||||
return Promise.resolve(noProviders);
|
return Promise.resolve(noProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ex: 'locale/messages.fr.xlf`
|
// Ex: 'locale/messages.es.xlf`
|
||||||
const translationFile = `./locale/messages.${locale}.xlf`;
|
const translationFile = `./locale/messages.${locale}.xlf`;
|
||||||
|
|
||||||
return getTranslationsWithSystemJs(translationFile)
|
return getTranslationsWithSystemJs(translationFile)
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<!-- #docregion i18n -->
|
<!-- #docregion i18n -->
|
||||||
<script>
|
<script>
|
||||||
// Get the locale id somehow
|
// Get the locale id somehow
|
||||||
document.locale = 'fr';
|
document.locale = 'es';
|
||||||
|
|
||||||
// Map to the text plugin
|
// Map to the text plugin
|
||||||
System.config({
|
System.config({
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
||||||
|
<source>Hello i18n!</source>
|
||||||
|
<target>¡Hola i18n!</target>
|
||||||
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
|
<note priority="1" from="meaning">User welcome</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
||||||
|
<source>I don't output any element</source>
|
||||||
|
<target>No genero ningún elemento</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="df3cf8b55cb528cf8c00167e0b119bfb828538b5" datatype="html">
|
||||||
|
<source>
|
||||||
|
I don't output any element either
|
||||||
|
</source>
|
||||||
|
<target>Yo tampoco genero ningún elemento</target>
|
||||||
|
<note priority="1" from="description">optional description</note>
|
||||||
|
<note priority="1" from="meaning">optional meaning</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="35ab5d6121ecbe0decdda638571a5a55ac77d5c4" datatype="html">
|
||||||
|
<source>Angular 2 logo</source>
|
||||||
|
<target>Logo de Angular 2</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6e22e74e8cbd3095560cfe08993c4fdfa3c50eb0" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>{wolves, plural, =0 {ningún lobo} =1 {un lobo} =2 {dos lobos} other {una horda de lobos}}</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="61cafedb85466ab789b3ae817bba1a545468ee1c" datatype="html">
|
||||||
|
<source>The hero is <x id="ICU"/></source>
|
||||||
|
<target>El heroe es <x id="ICU"/></target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="14c7055d67771a3b7b6888d282ac092896be06b6" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>{gender, select, m {hombre} f {mujer}}</target>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<!-- The `messages.es.xlf` after translation for documentation purposes -->
|
||||||
|
<!-- #docregion -->
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
|
<body>
|
||||||
|
<!-- #docregion translated-hello -->
|
||||||
|
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
||||||
|
<source>Hello i18n!</source>
|
||||||
|
<target>¡Hola i18n!</target>
|
||||||
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
|
<note priority="1" from="meaning">User welcome</note>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translated-hello -->
|
||||||
|
<!-- #docregion translated-other-nodes -->
|
||||||
|
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
||||||
|
<source>I don't output any element</source>
|
||||||
|
<target>No genero ningún elemento</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="df3cf8b55cb528cf8c00167e0b119bfb828538b5" datatype="html">
|
||||||
|
<source>I don't output any element either</source>
|
||||||
|
<target>Yo tampoco genero ningún elemento</target>
|
||||||
|
<note priority="1" from="description">optional description</note>
|
||||||
|
<note priority="1" from="meaning">optional meaning</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="35ab5d6121ecbe0decdda638571a5a55ac77d5c4" datatype="html">
|
||||||
|
<source>Angular 2 logo</source>
|
||||||
|
<target>Logo de Angular 2</target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translated-other-nodes -->
|
||||||
|
<!-- #docregion translated-plural -->
|
||||||
|
<trans-unit id="6e22e74e8cbd3095560cfe08993c4fdfa3c50eb0" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>{wolves, plural, =0 {ningún lobo} =1 {un lobo} =2 {dos lobos} other {una horda de lobos}}</target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translated-plural -->
|
||||||
|
<!-- #docregion translated-select -->
|
||||||
|
<!-- #docregion translate-select-1 -->
|
||||||
|
<trans-unit id="61cafedb85466ab789b3ae817bba1a545468ee1c" datatype="html">
|
||||||
|
<source>The hero is <x id="ICU"/></source>
|
||||||
|
<target>El heroe es <x id="ICU"/></target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translate-select-1 -->
|
||||||
|
<!-- #docregion translate-select-2 -->
|
||||||
|
<trans-unit id="14c7055d67771a3b7b6888d282ac092896be06b6" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target>{gender, select, m {hombre} f {mujer}}</target>
|
||||||
|
</trans-unit>
|
||||||
|
<!-- #enddocregion translate-select-2 -->
|
||||||
|
<!-- #enddocregion translated-select -->
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
|
||||||
<body>
|
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
|
||||||
<source>Hello i18n!</source>
|
|
||||||
<target>Bonjour i18n!</target>
|
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
|
||||||
<note priority="1" from="meaning">User welcome</note>
|
|
||||||
</trans-unit>
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
|
@ -1,17 +0,0 @@
|
||||||
<!-- The `messages.fr.xlf` after translation for documentation purposes -->
|
|
||||||
<!-- #docregion -->
|
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
||||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
|
||||||
<body>
|
|
||||||
<!-- #docregion trans-unit -->
|
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
|
||||||
<source>Hello i18n!</source>
|
|
||||||
<target>Bonjour i18n!</target>
|
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
|
||||||
<note priority="1" from="meaning">User welcome</note>
|
|
||||||
</trans-unit>
|
|
||||||
<!-- #enddocregion trans-unit -->
|
|
||||||
</body>
|
|
||||||
</file>
|
|
||||||
</xliff>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<!-- #docregion -->
|
|
||||||
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
|
||||||
<source>Hello i18n!</source>
|
|
||||||
<target/>
|
|
||||||
<note priority="1" from="description">An introduction header for this sample</note>
|
|
||||||
<note priority="1" from="meaning">User welcome</note>
|
|
||||||
</trans-unit>
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||||
|
<body>
|
||||||
|
<trans-unit id="af2ccf4b5dba59616e92cf1531505af02da8f6d2" datatype="html">
|
||||||
|
<source>Hello i18n!</source>
|
||||||
|
<target/>
|
||||||
|
<note priority="1" from="description">An introduction header for this sample</note>
|
||||||
|
<note priority="1" from="meaning">User welcome</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
|
||||||
|
<source>I don't output any element</source>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="df3cf8b55cb528cf8c00167e0b119bfb828538b5" datatype="html">
|
||||||
|
<source>
|
||||||
|
I don't output any element either
|
||||||
|
</source>
|
||||||
|
<target/>
|
||||||
|
<note priority="1" from="description">optional description</note>
|
||||||
|
<note priority="1" from="meaning">optional meaning</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="35ab5d6121ecbe0decdda638571a5a55ac77d5c4" datatype="html">
|
||||||
|
<source>Angular 2 logo</source>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6e22e74e8cbd3095560cfe08993c4fdfa3c50eb0" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="61cafedb85466ab789b3ae817bba1a545468ee1c" datatype="html">
|
||||||
|
<source>The hero is <x id="ICU"/></source>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="14c7055d67771a3b7b6888d282ac092896be06b6" datatype="html">
|
||||||
|
<source/>
|
||||||
|
<target/>
|
||||||
|
</trans-unit>
|
||||||
|
</body>
|
||||||
|
</file>
|
||||||
|
</xliff>
|
|
@ -4,8 +4,8 @@
|
||||||
"app/**/*.css",
|
"app/**/*.css",
|
||||||
"app/**/*.html",
|
"app/**/*.html",
|
||||||
"app/**/*.ts",
|
"app/**/*.ts",
|
||||||
"locale/messages.xlf",
|
"messages.xlf",
|
||||||
"locale/messages.fr.xlf",
|
"locale/messages.*.xlf",
|
||||||
|
|
||||||
"!**/*.[1].*",
|
"!**/*.[1].*",
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
include ../_util-fns
|
include ../_util-fns
|
||||||
|
|
||||||
|
a#top
|
||||||
:marked
|
:marked
|
||||||
Angular's _internationalization_ ("_i18n_") tools help make your app available in multiple languages.
|
Angular's _internationalization_ ("_i18n_") tools help make your app available in multiple languages.
|
||||||
|
|
||||||
<a id="top"></a>
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
* [Angular and i18n template translation](#angular-i18n)
|
* [Angular and i18n template translation](#angular-i18n)
|
||||||
* [Mark text with the _i18n_ attribute](#i18n-attribute)
|
* [Mark text with the _i18n_ attribute](#i18n-attribute)
|
||||||
* [Create a translation source file with the _ng-xi18n_ tool](#ng-xi18n)
|
* [Add _i18n-..._ translation attributes](#translate-attributes)
|
||||||
|
* [Handle singular and plural](#cardinality)
|
||||||
|
* [Select among alternative texts](#select)
|
||||||
|
* [Use the **_ng-xi18n_ extraction tool** to create a translation source file](#ng-xi18n)
|
||||||
* [Translate](#translate)
|
* [Translate](#translate)
|
||||||
* [Merge the completed translation file into the app](#merge)
|
* [Merge the completed translation file into the app](#merge)
|
||||||
* [JiT configuration](#jit)
|
* [JiT configuration](#jit)
|
||||||
* [AoT configuration](#aot)
|
* [AoT configuration](#aot)
|
||||||
|
* [Translation file maintenance and _id_ changes](#maintenance)
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
**Try this** <live-example name="cb-i18n">live example</live-example> of a JiT-compiled app, translated into French.
|
**Try this** <live-example name="cb-i18n" title="i18n Example in Spanish">live example</live-example>
|
||||||
|
of a JiT-compiled app, translated into Spanish.
|
||||||
|
|
||||||
|
|
||||||
a#angular-i18n
|
a#angular-i18n
|
||||||
|
@ -67,7 +72,7 @@ a#i18n-attribute
|
||||||
It will be removed by the compiler _after_ translation.
|
It will be removed by the compiler _after_ translation.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
In the accompanying sample, an `<h1>` tag displays a simple English language greeting which you will translate to French:
|
In the accompanying sample, an `<h1>` tag displays a simple English language greeting which you will translate to Spanish:
|
||||||
+makeExample('cb-i18n/ts/app/app.component.1.html', 'greeting', 'app/app.component.html')(format=".")
|
+makeExample('cb-i18n/ts/app/app.component.1.html', 'greeting', 'app/app.component.html')(format=".")
|
||||||
:marked
|
:marked
|
||||||
Add the `i18n` attribute to the tag to mark it for translation.
|
Add the `i18n` attribute to the tag to mark it for translation.
|
||||||
|
@ -75,29 +80,132 @@ a#i18n-attribute
|
||||||
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute', 'app/app.component.html')(format=".")
|
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
|
### Help the translator with a _description_ and _intent_
|
||||||
|
|
||||||
The translator may need a description of the message to translate it accurately.
|
The translator may need a description of the message to translate it accurately.
|
||||||
Assign a description to the i18n attribute:
|
Assign a description to the i18n attribute:
|
||||||
|
|
||||||
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute-desc', 'app/app.component.html')(format=".")
|
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute-desc', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The true _meaning_ of the text may require some application context.
|
The translator may need to know your _intent_ —
|
||||||
Add a contextual meaning to the assigned string, separating it from the description with the `|` character:
|
the true _meaning_ of the text within _this particular_ application context —
|
||||||
|
in order to deliver a correct translation.
|
||||||
|
Add some contextual meaning to the assigned string, in front of the description,
|
||||||
|
separating it from the description with the `|` character (`<meaning>|<description>`):
|
||||||
|
|
||||||
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-attribute-meaning', 'app/app.component.html')(format=".")
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-attribute-meaning', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
While all appearance of a message with the _same_ meaning should have the _same_ translation,
|
While all appearances of a message with the _same_ meaning have the _same_ translation,
|
||||||
a message with *different meanings* could have different translations.
|
a message with *different meanings* could have different translations.
|
||||||
The Angular extraction tool preserves both the _meaning_ and the _description_ in the translation source file
|
The Angular extraction tool preserves both the _meaning_ and the _description_ in the translation source file
|
||||||
to facilitiate contextually-specific translations.
|
to facilitiate contextually-specific translations.
|
||||||
|
|
||||||
|
### Translate text without creating an element
|
||||||
|
|
||||||
|
Suppose there is a stretch of text that you'd like to translate.
|
||||||
|
You could wrap it in a `<span>` but for some reason (CSS comes to mind)
|
||||||
|
you don't want to create a new DOM element merely to facilitate translation.
|
||||||
|
|
||||||
|
Here are two techniques to try.
|
||||||
|
|
||||||
|
(1) Wrap the text in an `<ng-container>` element. The `<ng-container>` is never renderered:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-ng-container', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
(2) Wrap the text in a pair of HTML comments:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-with-comment', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
.l-main-section
|
||||||
|
a#translate-attributes
|
||||||
|
:marked
|
||||||
|
## Add _i18n-..._ translation attributes
|
||||||
|
You've added an image to your template. You care about accessibility too so you add a `title` tag:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-title', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
The `title` attribute needs to be translated.
|
||||||
|
Angular i18n support has more translation attributes in the form,`i18n-x`, where `x` is the
|
||||||
|
name of the attribute to translate.
|
||||||
|
|
||||||
|
To translate the `title` on the `img` tag from the previous example, write:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-title-translate', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
You can also assign a meaning and a description with the `i18n-x="<meaning>|<description>"` syntax.
|
||||||
|
|
||||||
|
.l-main-section
|
||||||
|
a#cardinality
|
||||||
|
:marked
|
||||||
|
## Handle singular and plural
|
||||||
|
|
||||||
|
Different languages have different pluralization rules.
|
||||||
|
|
||||||
|
Suppose your application says something about a collection of wolves.
|
||||||
|
In English, depending upon the number of wolves, you could display "no wolves", "one wolf", "two wolves", or "a wolf pack".
|
||||||
|
Other languages might express the _cardinality_ differently.
|
||||||
|
|
||||||
|
Here's how you could markup the component template to display the phrase appropriate to the number of wolves:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-plural', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
* The first parameter is the key. It will be bound to the component property (`wolves`) that determines the number of wolves.
|
||||||
|
* The second parameter indentifies this as a `plural` translation type.
|
||||||
|
* The third parameter defines a pluralization pattern consisting of pluralization categories and their matching values.
|
||||||
|
|
||||||
|
Pluralization categories include:
|
||||||
|
* =0
|
||||||
|
* =1
|
||||||
|
* =5
|
||||||
|
* few
|
||||||
|
* other
|
||||||
|
|
||||||
|
Put the default _English_ translation in braces (`{}`) next to the pluralization category.
|
||||||
|
* When you're talking about one wolf, you could write `=1 {one wolf}`.
|
||||||
|
* For zero wolves, you could write `=0 {no wolves}`.
|
||||||
|
* For two wolves, you could write `=2 {two wolves}`.
|
||||||
|
|
||||||
|
You could keep this up for three, four, and every other number of wolves.
|
||||||
|
Or you could specify the **`other`** category as a catch-all for any unmatched cardinality
|
||||||
|
and write something like: `other {a wolf pack}`.
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
This syntax conforms to the
|
||||||
|
<a href="http://userguide.icu-project.org/formatparse/messages" target="_blank" title="ICU Message Format">ICU Message Format</a>
|
||||||
|
which derives from the
|
||||||
|
<a href="http://cldr.unicode.org/" target="_blank" title="CLDR">Common Locale Data Repository (CLDR)</a>
|
||||||
|
which specifies the
|
||||||
|
<a href="http://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules" target="_blank" title="Pluralization Rules">pluralization rules</a>.
|
||||||
|
|
||||||
|
a#select
|
||||||
|
:marked
|
||||||
|
## Select
|
||||||
|
The application displays different text depending upon whether the hero is male or female.
|
||||||
|
These text alternatives require translation too.
|
||||||
|
|
||||||
|
You can handle this with a `select` translation.
|
||||||
|
A `select` also follows the
|
||||||
|
<a href="http://userguide.icu-project.org/formatparse/messages" target="_blank" title="ICU Message Format">ICU message syntax</a>.
|
||||||
|
You choose among alternative translation based on a string value instead of a number.
|
||||||
|
|
||||||
|
The following format message in the component template binds to the component's `gender` property which outputs either an 'm' or an 'f'.
|
||||||
|
The message maps those values to the appropriate translation:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-select', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
a#ng-xi18n
|
a#ng-xi18n
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Create a translation source file with the _ng-xi18n_ tool
|
## Create a translation source file with the _ng-xi18n_ tool
|
||||||
|
|
||||||
Use the `ng-xi18n` extraction tool to extract the `i18n`-marked texts
|
Use the **_ng-xi18n_ extraction tool** to extract the `i18n`-marked texts
|
||||||
into a translation source file in an industry standard format.
|
into a translation source file in an industry standard format.
|
||||||
|
|
||||||
This is an Angular CLI tool in the `@angular/compiler-cli` npm package.
|
This is an Angular CLI tool in the `@angular/compiler-cli` npm package.
|
||||||
|
@ -112,48 +220,68 @@ code-example(language="sh" class="code-shell").
|
||||||
code-example(language="sh" class="code-shell").
|
code-example(language="sh" class="code-shell").
|
||||||
./node_modules/.bin/ng-xi18n
|
./node_modules/.bin/ng-xi18n
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
Windows users may have to quote the command: like this `"./node_modules/.bin/ng-xi18n"`
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
By default the tool generates a translation file named **`messages.xlf`** in the
|
By default the tool generates a translation file named **`messages.xlf`** in the
|
||||||
<a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">XML Localisation Interchange File Format (XLIFF, version 1.2)</a>.
|
<a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">XML Localisation Interchange File Format (XLIFF, version 1.2)</a>.
|
||||||
|
|
||||||
code-example(language="sh" class="code-shell").
|
a#other-formats
|
||||||
./node_modules/.bin/ng-xi18n --i18nFormat=xmb
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Windows users may have to quote the command:
|
|
||||||
code-example(language="sh").
|
|
||||||
"./node_modules/.bin/ng-xi18n"
|
|
||||||
:marked
|
|
||||||
Consider adding a convenience shortcut to the `scripts` section of the `package.json`
|
|
||||||
to make the command easier to remember and run:
|
|
||||||
code-example(format='.').
|
|
||||||
"scripts": {
|
|
||||||
"i18n": "ng-xi18n",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
:marked
|
|
||||||
Now you can enter:
|
|
||||||
code-example(language="sh" class="code-shell").
|
|
||||||
npm run i18n
|
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
### Other translation formats
|
### Other translation formats
|
||||||
|
|
||||||
You can generate a file named **`messages.xmb`** in the
|
You can generate a file named **`messages.xmb`** in the
|
||||||
<a href="http://cldr.unicode.org/development/development-process/design-proposals/xmb" target="_blank">XML Message Bundle (XMB)</a> format
|
<a href="http://cldr.unicode.org/development/development-process/design-proposals/xmb" target="_blank">XML Message Bundle (XMB)</a> format
|
||||||
by adding the `--i18nFormat=xmb` switch.
|
by adding the `--i18nFormat=xmb` flag.
|
||||||
|
|
||||||
|
code-example(language="sh" class="code-shell").
|
||||||
|
./node_modules/.bin/ng-xi18n --i18nFormat=xmb
|
||||||
|
|
||||||
|
:marked
|
||||||
This sample sticks with the _XLIFF_ format.
|
This sample sticks with the _XLIFF_ format.
|
||||||
|
|
||||||
|
a#ng-xi18n-options
|
||||||
|
:marked
|
||||||
|
### Other options
|
||||||
|
You may have to specify additional options.
|
||||||
|
For example, if the `tsconfig.json` TypeScript configuration file is located other than in the root folder,
|
||||||
|
you must identify the path to it with the `-p` option:
|
||||||
|
code-example(language="sh" class="code-shell").
|
||||||
|
./node_modules/.bin/ng-xi18n -p path/to/tsconfig.json
|
||||||
|
./node_modules/.bin/ng-xi18n --i18nFormat=xmb -p path/to/tsconfig.json
|
||||||
|
|
||||||
|
|
||||||
|
a#npm-i18n-script
|
||||||
|
:marked
|
||||||
|
### Add an _npm_ script for convenience
|
||||||
|
|
||||||
|
Consider adding a convenience shortcut to the `scripts` section of the `package.json`
|
||||||
|
to make the command easier to remember and run:
|
||||||
|
code-example(format='.' language='sh' ).
|
||||||
|
"scripts": {
|
||||||
|
"i18n": "ng-xi18n",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
:marked
|
||||||
|
Now you can issue command variations such as these:
|
||||||
|
code-example(language="sh" class="code-shell").
|
||||||
|
npm run i18n
|
||||||
|
npm run i18n -- -p path/to/tsconfig.json
|
||||||
|
npm run i18n -- --i18nFormat=xmb -p path/to/tsconfig.json
|
||||||
|
:marked
|
||||||
|
Note the `--` flag before the options.
|
||||||
|
It tells _npm_ that every flag thereafter should be passed through to `ng-xi18n`.
|
||||||
|
|
||||||
a#translate
|
a#translate
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Translate _le message textuel_
|
## Translate _los mensajes de texto_
|
||||||
|
|
||||||
The `ng-xi18n` command generated a translation source file in the project root folder named `messages.xlf`.
|
The `ng-xi18n` command generated a translation source file in the project root folder named `messages.xlf`.
|
||||||
The next step is to translate the English language template text into the specific language translation
|
The next step is to translate the English language template text into the specific language translation
|
||||||
files. The cookbook sample creates a French translation file.
|
files. The cookbook sample creates a Spanish translation file.
|
||||||
|
|
||||||
a#localization-folder
|
a#localization-folder
|
||||||
:marked
|
:marked
|
||||||
|
@ -169,39 +297,88 @@ a#localization-folder
|
||||||
Localization and internationalization are
|
Localization and internationalization are
|
||||||
<a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank">different but closely related terms</a>.
|
<a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank">different but closely related terms</a>.
|
||||||
:marked
|
:marked
|
||||||
This sample follows that suggestion. It has `locale` folder immediately under the project root.
|
This sample follows that suggestion. It has a `locale` folder under the project root.
|
||||||
Assets within the folder carry a filename extension that matches a language-culture code from a
|
Assets within the folder carry a filename extension that matches a language-culture code from a
|
||||||
<a href="https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx" target="_blank">well-known codeset</a>.
|
<a href="https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx" target="_blank">well-known codeset</a>.
|
||||||
|
|
||||||
Move `messages.xlf` into the `locale` folder where it will become the source for all language-specific translations.
|
Make a copy of the `messages.xlf` file in the `locale` folder and
|
||||||
Then make a copy for the French language named `messages.fr.xlf` .
|
rename it `messages.es.xlf`for the Spanish language translation.
|
||||||
|
Do the same for each target language.
|
||||||
|
|
||||||
Follow the same convention for each target language.
|
### Translate text nodes
|
||||||
|
In the real world, you send the `messages.es.xlf` file to a Spanish translator who fills in the translations
|
||||||
### Translate
|
using one of the
|
||||||
In the real world, you send the `messages.fr.xlf` file to a French translator who would fill in the translations
|
|
||||||
using one of the
|
|
||||||
<a href="https://en.wikipedia.org/wiki/XLIFF#Editors" target="_blank">many XLIFF file editors</a>.
|
<a href="https://en.wikipedia.org/wiki/XLIFF#Editors" target="_blank">many XLIFF file editors</a>.
|
||||||
|
|
||||||
This sample file is easy to translate without a special editor or knowledge of French.
|
This sample file is easy to translate without a special editor or knowledge of Spanish.
|
||||||
Open `messages.fr.xlf` and find the `<trans-unit>` section:
|
Open `messages.es.xlf` and find the first `<trans-unit>` section:
|
||||||
|
|
||||||
+makeExample('cb-i18n/ts/locale/trans-unit.html', '', 'locale/messages.fr.xlf (<trans-unit>)')(format=".")
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translated-hello', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
:marked
|
:marked
|
||||||
This XML element represents the translation of the `<h1>` greeting tag you marked with the `i18n` attribute.
|
This XML element represents the translation of the `<h1>` greeting tag you marked with the `i18n` attribute.
|
||||||
|
|
||||||
Using the _source_, _description_, and _meaning_ elements to guide your translation,
|
Using the _source_, _description_, and _meaning_ elements to guide your translation,
|
||||||
replace the `<target/>` tag with the French greeting:
|
replace the `<target/>` tag with the Spanish greeting:
|
||||||
+makeExample('cb-i18n/ts/locale/messages.fr.xlf.html', 'trans-unit', 'locale/messages.fr.xlf (<trans-unit>, after translation)')(format=".")
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translated-hello', 'locale/messages.es.xlf (<trans-unit>, after translation)')(format=".")
|
||||||
:marked
|
|
||||||
Note that the `id` is generated by the tool. Don't touch it.
|
.alert.is-important
|
||||||
Its value depends on the content of the message and its assigned meaning.
|
|
||||||
Change either factor and the `id` changes as well.
|
|
||||||
.alert.is-helpful
|
|
||||||
:marked
|
:marked
|
||||||
Repeat the extraction process whenever you add new app messages or edit existing ones.
|
Note that the `id` is generated by the tool. **Don't touch it.**
|
||||||
Be careful not to lose the previous translations.
|
Its value depends on the content of the message and its assigned meaning.
|
||||||
Specialized software can help manage the change process.
|
Change either factor and the `id` changes as well.
|
||||||
|
See the **[translation file maintenance discussion](#maintenance)** below.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Translate the other text nodes the same way:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translated-other-nodes', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
|
|
||||||
|
a#translate-plural-select
|
||||||
|
:marked
|
||||||
|
## Translate _plural_ and _select_
|
||||||
|
Translating _plural_ and _select_ messages is a little tricky.
|
||||||
|
|
||||||
|
The `<source>` tag is empty for `plural` and `select` translation units which makes them hard to correlate with the original template.
|
||||||
|
The ICU rules are not yet supported in the `XLIFF` format; they soon will be.
|
||||||
|
The ICU rules _are_ supported in the `XMB` format.
|
||||||
|
|
||||||
|
You'll just have to look for them in relation to other translation units that you recognize from elsewhere in the source template.
|
||||||
|
In this example, you know the translation unit for the `select` must be just below the translation unit for the logo.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
### Translate _plural_
|
||||||
|
To translate a `plural`, translate its ICU format match values:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translated-plural', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
### Translate _select_
|
||||||
|
The `select` behaves a little differently. Here again is the ICU format message in the component template:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-select', 'app/app.component.html')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
The extraction tool broke that into _two_ translation units.
|
||||||
|
|
||||||
|
The first unit contains the text that was _outside_ the `select`.
|
||||||
|
In place of the `select` is a placeholder, `<x id="ICU">`, that represents the `select` message.
|
||||||
|
Translate the text and leave the placeholder where it is.
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translate-select-1', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
The second translation unit, immediately below the first one, contains the `select` message. Translate that.
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translate-select-2', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Here they are together, after translation:
|
||||||
|
|
||||||
|
+makeExample('cb-i18n/ts/locale/messages.es.xlf.html', 'translated-select', 'locale/messages.es.xlf (<trans-unit>)')(format=".")
|
||||||
|
|
||||||
|
.l-main-content
|
||||||
|
:marked
|
||||||
|
The entire template translation is complete. It's time to incorporate that translation in the application.
|
||||||
|
|
||||||
#app-pre-translation
|
#app-pre-translation
|
||||||
:marked
|
:marked
|
||||||
|
@ -214,13 +391,13 @@ a#localization-folder
|
||||||
cb-i18n/ts/app/app.component.ts,
|
cb-i18n/ts/app/app.component.ts,
|
||||||
cb-i18n/ts/app/app.module.ts,
|
cb-i18n/ts/app/app.module.ts,
|
||||||
cb-i18n/ts/app/main.1.ts,
|
cb-i18n/ts/app/main.1.ts,
|
||||||
cb-i18n/ts/locale/messages.fr.xlf.html
|
cb-i18n/ts/locale/messages.es.xlf.html
|
||||||
`, '', `
|
`, '', `
|
||||||
app/app.component.html,
|
app/app.component.html,
|
||||||
app/app.component.ts,
|
app/app.component.ts,
|
||||||
app/app.module.ts,
|
app/app.module.ts,
|
||||||
app/main.ts,
|
app/main.ts,
|
||||||
locale/messages.fr.xlf
|
locale/messages.es.xlf
|
||||||
`)
|
`)
|
||||||
|
|
||||||
a#merge
|
a#merge
|
||||||
|
@ -237,7 +414,7 @@ a#merge
|
||||||
* the translation file
|
* the translation file
|
||||||
* the translation file format
|
* the translation file format
|
||||||
* the <a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">_Locale ID_</a>
|
* the <a href="https://en.wikipedia.org/wiki/XLIFF" target="_blank">_Locale ID_</a>
|
||||||
(`fr` or `en-US` for instance)
|
(`es` or `en-US` for instance)
|
||||||
|
|
||||||
_How_ you provide this information depends upon whether you compile with
|
_How_ you provide this information depends upon whether you compile with
|
||||||
the JiT (_Just-in-Time_) compiler or the AoT (_Ahead-of-Time_) compiler.
|
the JiT (_Just-in-Time_) compiler or the AoT (_Ahead-of-Time_) compiler.
|
||||||
|
@ -320,7 +497,7 @@ a#text-plugin
|
||||||
Notice that it waits for the `getTranslationProviders` promise to resolve before
|
Notice that it waits for the `getTranslationProviders` promise to resolve before
|
||||||
bootstrapping the app.
|
bootstrapping the app.
|
||||||
|
|
||||||
The app is now _internationalized_ for English and French and there is a clear path for adding
|
The app is now _internationalized_ for English and Spanish and there is a clear path for adding
|
||||||
more languages.
|
more languages.
|
||||||
|
|
||||||
a#aot
|
a#aot
|
||||||
|
@ -352,12 +529,27 @@ a#aot
|
||||||
* `--locale`: the name of the locale
|
* `--locale`: the name of the locale
|
||||||
* `--i18nFormat`: the format of the localization file
|
* `--i18nFormat`: the format of the localization file
|
||||||
|
|
||||||
For this sample, the French language command would be
|
For this sample, the Spanish language command would be
|
||||||
code-example(language="sh" class="code-shell").
|
code-example(language="sh" class="code-shell").
|
||||||
./node_modules/.bin/ngc --i18nFile=./locale/messages.fr.xlf --locale=fr --i18nFormat=xlf
|
./node_modules/.bin/ngc --i18nFile=./locale/messages.es.xlf --locale=es --i18nFormat=xlf
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Windows users may have to quote the command:
|
Windows users may have to quote the command:
|
||||||
code-example(language="sh" class="code-shell").
|
code-example(language="sh" class="code-shell").
|
||||||
"./node_modules/.bin/ngc" --i18nFile=./locale/messages.fr.xlf --locale=fr --i18nFormat=xlf
|
"./node_modules/.bin/ngc" --i18nFile=./locale/messages.es.xlf --locale=es --i18nFormat=xlf
|
||||||
|
|
||||||
|
a#maintenance
|
||||||
|
:marked
|
||||||
|
## Translation file maintenance and _id_ changes
|
||||||
|
|
||||||
|
As the application evolves, you will change the _i18n_ markup and re-run the `ng-xi18n` extraction tool many times.
|
||||||
|
The _new_ markup that you add is not a problem.
|
||||||
|
But _most_ changes to _existing_ markup will trigger generation of _new_ `id`s for the affected translation units.
|
||||||
|
|
||||||
|
After an `id` changes, the translation files are no longer in-sync.
|
||||||
|
**All translated versions of the application will fail** during re-compilation.
|
||||||
|
The error messages identify the old `id`s that are no longer valid but they don't tell you what the new `id`s should be.
|
||||||
|
|
||||||
|
**Commit all translation message files to source control**, especially the English source `messages.xlf`.
|
||||||
|
The difference between the old and the new `messages.xlf` file will help you find and update `id` changes across your translation files.
|
||||||
|
|
|
@ -5,6 +5,11 @@ block includes
|
||||||
The Angular documentation is a living document with continuous improvements.
|
The Angular documentation is a living document with continuous improvements.
|
||||||
This log calls attention to recent significant changes.
|
This log calls attention to recent significant changes.
|
||||||
|
|
||||||
|
## Internationalization: pluralization and _select_ (2016-11-30)
|
||||||
|
The [Internationalization (i18n)](i18n.html) guide explains how to handle pluralization and
|
||||||
|
translation of alternative texts with `select`.
|
||||||
|
The sample demonstrates these features too.
|
||||||
|
|
||||||
## Testing: karma file updates (2016-11-30)
|
## Testing: karma file updates (2016-11-30)
|
||||||
* karma.config + karma-test-shim can handle multiple spec source paths;
|
* karma.config + karma-test-shim can handle multiple spec source paths;
|
||||||
see quickstart issue: [angular/quickstart#294](https://github.com/angular/quickstart/issues/294)
|
see quickstart issue: [angular/quickstart#294](https://github.com/angular/quickstart/issues/294)
|
||||||
|
|
Loading…
Reference in New Issue