docs(i18n): add internationalization (i18n) guide (#2491)

* docs(i18n): add internationalization (i18n) guide

* docs(cb-i18n): revamped to System.import the translation file
This commit is contained in:
Ward Bell 2016-09-29 11:15:55 -07:00 committed by GitHub
parent 64d5b3dc23
commit e6199a8dfb
27 changed files with 590 additions and 5 deletions

View File

@ -297,7 +297,7 @@ function runE2eTsTests(appDir, outputFile) {
} catch (e) { } catch (e) {
exampleConfig = {}; exampleConfig = {};
} }
var config = { var config = {
build: exampleConfig.build || 'tsc', build: exampleConfig.build || 'tsc',
run: exampleConfig.run || 'http-server:e2e' run: exampleConfig.run || 'http-server:e2e'
@ -1263,7 +1263,7 @@ function apiExamplesWatch(postShredAction) {
} }
function devGuideExamplesWatch(shredOptions, postShredAction, focus) { function devGuideExamplesWatch(shredOptions, postShredAction, focus) {
var watchPattern = focus ? '**/' + focus + '/**/*.*' : '**/*.*'; var watchPattern = focus ? '**/{' + focus + ',cb-' + focus+ '}/**/*.*' : '**/*.*';
var includePattern = path.join(shredOptions.examplesDir, watchPattern); var includePattern = path.join(shredOptions.examplesDir, watchPattern);
// removed this version because gulp.watch has the same glob issue that dgeni has. // removed this version because gulp.watch has the same glob issue that dgeni has.
// var excludePattern = '!' + path.join(shredOptions.examplesDir, '**/node_modules/**/*.*'); // var excludePattern = '!' + path.join(shredOptions.examplesDir, '**/node_modules/**/*.*');

View File

@ -0,0 +1,13 @@
/// <reference path='../_protractor/e2e.d.ts' />
'use strict';
describe('i18n E2E Tests', () => {
beforeEach(function () {
browser.get('');
});
it('should display i18n translated welcome: Bonjour i18n!', function () {
expect(element(by.css('h1')).getText()).toEqual('Bonjour i18n!');
});
});

View File

@ -0,0 +1,6 @@
**/*.ngfactory.ts
**/*.metadata.json
**/messages.xlf
dist
!app/tsconfig.json
!rollup.js

View File

@ -0,0 +1,11 @@
<!--#docregion greeting-->
<h1>Hello i18n!</h1>
<!--#enddocregion greeting-->
<!--#docregion i18n-attribute-->
<h1 i18n>Hello i18n!</h1>
<!--#enddocregion i18n-attribute-->
<!--#docregion i18n-attribute-desc-->
<h1 i18n="An introduction header for this sample">Hello i18n!</h1>
<!--#enddocregion i18n-attribute-desc-->

View File

@ -0,0 +1,4 @@
<!--#docregion-->
<!--#docregion i18n-attribute-meaning-->
<h1 i18n="User welcome|An introduction header for this sample">Hello i18n!</h1>
<!--#enddocregion i18n-attribute-meaning-->

View File

@ -0,0 +1,10 @@
// #docregion
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'my-app',
templateUrl: 'app.component.html'
})
export class AppComponent { }

View File

@ -0,0 +1,13 @@
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -0,0 +1,33 @@
// #docregion
import { TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID } from '@angular/core';
export function getTranslationProviders(): Promise<Object[]> {
// Get the locale id from the global
const locale = document['locale'] as string;
// return no providers if fail to get translation file for locale
const noProviders: Object[] = [];
// No locale or English: no translation providers
if (!locale || locale === 'en') {
return Promise.resolve(noProviders);
}
// Ex: 'i18n/fr/messages.fr.xlf`
const translationFile = `./i18n/${locale}/messages.${locale}.xlf`;
return getTranslationsWithSystemJs(translationFile)
.then( (translations: string ) => [
{ provide: TRANSLATIONS, useValue: translations },
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
{ provide: LOCALE_ID, useValue: locale }
])
.catch(() => noProviders); // ignore if file not found
}
declare var System: any;
function getTranslationsWithSystemJs(file: string) {
return System.import(file + '!text'); // relies on text plugin
}

View File

@ -0,0 +1,6 @@
// #docregion
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@ -0,0 +1,10 @@
// #docregion
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { getTranslationProviders } from './i18n-providers';
import { AppModule } from './app.module';
getTranslationProviders().then(providers => {
const options = { providers };
platformBrowserDynamic().bootstrapModule(AppModule, options);
});

View File

@ -0,0 +1,13 @@
<?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>

View File

@ -0,0 +1,17 @@
<!-- 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>

View File

@ -0,0 +1,7 @@
<!-- #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>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<title>Angular i18n example</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<!-- #docregion i18n -->
<script>
// Get the locale id somehow
document.locale = 'fr';
// Map to the text plugin
System.config({
map: {
text: 'systemjs-text-plugin.js'
}
});
// Launch the app
System.import('app').catch(function(err){ console.error(err); });
</script>
<!-- #enddocregion i18n -->
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -0,0 +1,17 @@
{
"description": "i18n",
"files": [
"app/**/*.css",
"app/**/*.html",
"app/**/*.ts",
"i18n/messages.xlf",
"i18n/fr/messages.fr.xlf",
"!**/*.[1].*",
"styles.css",
"systemjs-text-plugin.js",
"index.html"
],
"tags": ["i18n"]
}

View File

@ -0,0 +1,14 @@
// #docregion
/*
SystemJS Text plugin from
https://github.com/systemjs/plugin-text/blob/master/text.js
*/
exports.translate = function(load) {
if (this.builder && this.transpiler) {
load.metadata.format = 'esm';
return 'exp' + 'ort var __useDefault = true; exp' + 'ort default ' + JSON.stringify(load.source) + ';';
}
load.metadata.format = 'amd';
return 'def' + 'ine(function() {\nreturn ' + JSON.stringify(load.source) + ';\n});';
}

View File

@ -20,7 +20,8 @@
"test:webpack": "karma start karma.webpack.conf.js", "test:webpack": "karma start karma.webpack.conf.js",
"build:webpack": "rimraf dist && webpack --config config/webpack.prod.js --bail", "build:webpack": "rimraf dist && webpack --config config/webpack.prod.js --bail",
"build:cli": "ng build", "build:cli": "ng build",
"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup.js" "build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup.js",
"extract": "ng-xi18n"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -47,6 +47,12 @@
"hide": true "hide": true
}, },
"i18n": {
"title": "Internationalization (i18n)",
"intro": "Translate the app's template text into multiple languages",
"hide": true
},
"rc4-to-rc5": { "rc4-to-rc5": {
"title": "RC4 to RC5 Migration", "title": "RC4 to RC5 Migration",
"intro": "Migrate your RC4 app to RC5 in minutes.", "intro": "Migrate your RC4 app to RC5 in minutes.",

View File

@ -0,0 +1 @@
include ../../../_includes/_ts-temp

View File

@ -0,0 +1 @@
!= partial("../../../_includes/_ts-temp")

View File

@ -42,6 +42,12 @@
"intro": "Validate user's form entries" "intro": "Validate user's form entries"
}, },
"i18n": {
"title": "Internationalization (i18n)",
"intro": "Translate the app's template text into multiple languages",
"hide": true
},
"rc4-to-rc5": { "rc4-to-rc5": {
"title": "RC4 to RC5 Migration", "title": "RC4 to RC5 Migration",
"intro": "Migrate your RC4 app to RC5 in minutes.", "intro": "Migrate your RC4 app to RC5 in minutes.",

View File

@ -0,0 +1 @@
include ../../../_includes/_ts-temp

View File

@ -0,0 +1 @@
!= partial("../../../_includes/_ts-temp")

View File

@ -46,6 +46,11 @@
"intro": "Validate user's form entries" "intro": "Validate user's form entries"
}, },
"i18n": {
"title": "Internationalization (i18n)",
"intro": "Translate the app's template text into multiple languages"
},
"rc4-to-rc5": { "rc4-to-rc5": {
"title": "RC4 to RC5 Migration", "title": "RC4 to RC5 Migration",
"intro": "Migrate your RC4 app to RC5 in minutes." "intro": "Migrate your RC4 app to RC5 in minutes."

View File

@ -0,0 +1,350 @@
include ../_util-fns
:marked
Angular's _internationalization_ ("_i18n_") tools help make your app availble in multiple languages.
<a id="top"></a>
## Table of contents
* [Angular and i18n template translation](#angular-i18n)
* [Mark text with the _i18n_ attribute](#i18n-attribute)
* [Extract text with ng-xi18n](#ng-xi18n)
* [Translate](#translate)
* [Merge the translation file into the app](#merge)
* [JiT configuration](#jit)
* [AoT configuration](#aot)
:marked
**Try this** <live-example>live example</live-example> of a JiT-compiled app, translated into French.
a#angular-i18n
.l-main-section
:marked
## Angular and _i18n_ template translation
Application internationalization is a challenging, many-faceted effort that
takes dedication and enduring commitment.
Angular's _i18n_ internationalization facilities can help.
This page describes the _i18n_ tools to assist translation of component template text
into multiple languages.
.l-sub-section
:marked
Practitioners of _internationalization_ refer to a translatable text as a "_message_".
This page uses the words "_text_" and "_message_" interchangably and in the combination, "_text message_".
:marked
The _i18n_ template translation process has four phases:
1. Mark static text messages in your component templates for translation.
1. An angular _i18n_ tool extracts the marked messages into an industry standard translation source file.
1. A translator edits that file, translating the extracted text messages into the target language,
and returns the file to you.
1. The Angular compiler imports the completed translation files,
replaces the original messages with translated text, and generates a new version of the application
in the target language.
You build and deploy a separate version of the application for each supported language.
a#i18n-attribute
.l-main-section
:marked
## Mark text with the _i18n_ attribute
The Angular `i18n` attribute is a marker for translatable content.
Place it on every element tag whose fixed text should be translated.
.alert.is-helpful
:marked
`i18n` is not an Angular _directive_. It's a custom _attribute_, recognized by Angular tools and compilers.
:marked
In the accompanying sample, an `<h1>` tag displays a simple English language greeting which you will translate to French:
+makeExample('cb-i18n/ts/app/app.component.1.html', 'greeting', 'app/app.component.html')(format=".")
:marked
Add the `i18n` attribute to the tag to mark it for translation.
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute', 'app/app.component.html')(format=".")
:marked
The translator may need a description of the message to translate it accurately.
Assign a description to the i18n attribute:
+makeExample('cb-i18n/ts/app/app.component.1.html', 'i18n-attribute-desc', 'app/app.component.html')(format=".")
:marked
The true _meaning_ of the text may require some application context.
Add a contextual meaning to the assigned string, separating it from the description with the `|` character:
+makeExample('cb-i18n/ts/app/app.component.html', 'i18n-attribute-meaning', 'app/app.component.html')(format=".")
:marked
While all appearance of a message with the _same_ meaning should have the _same_ translation,
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
to facilitiate contextually-specific transations.
a#ng-xi18n
.l-main-section
:marked
## Extract translatable text with the _ng-xi18n_ command
Use the `ng-xi18n` extraction tool to generate a translation source file in an industry standard format.
This is an Angular CLI tool based on the `ngc` compiler in the `@angular/compiler-cli` npm package.
If you haven't already installed the CLI and its `platform-server` peer dependency, do so now:
code-example(language="sh" class="code-shell").
npm install @angular/compiler-cli @angular/platform-server --save
:marked
Open a terminal window at the root of the application project and enter the `ng-xi18n` command:
code-example(language="sh" class="code-shell").
./node_modules/.bin/ng-xi18n
:marked
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>.
code-example(language="sh" class="code-shell").
./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
### Other translation formats
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
by adding the `--i18nFormat=xmb` switch.
This sample sticks with the _XLIFF_ format.
a#translate
.l-main-section
:marked
## Translate _le message textuel_
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
files. The cookbook sample creates a French translation file.
a#i18n-file-structure
:marked
### Create an i18n project structure
You will probably translate into more than one other language so it's a good idea
for the project structure to reflect your entire internationalization effort.
One approach is to create an `i18n` folder with subfolders for each language or locale, e.g. `i18n/fr` for French.
This sample puts `i18n/fr` under the project root.
Move the `messages.xlf` into the `i18n` folder where it will become the source for all language-specific translations.
Then copy `messages.xlf` into `i18n/fr` and rename it as `messages.fr.xlf` .
Follow the same convention for each target language.
### Translate
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>.
This sample file is easy to translate without a special editor or knowledge of French.
Open `messages.fr.xlf` and find the `<trans-unit>` section:
+makeExample('cb-i18n/ts/i18n/trans-unit.html', '', 'i18n/messages.fr.xlf (<trans-unit>)')(format=".")
:marked
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,
replace the `<target/>` tag with the French greeting:
+makeExample('cb-i18n/ts/i18n/fr/messages.fr.xlf.html', 'trans-unit', 'i18n/fr/messages.fr.xlf (<trans-unit>, after translation)')(format=".")
:marked
Note that the `id` is generated by the tool. Don't touch it.
Its value depends on the content of the message, its meaning, and its description.
Change any of these factors and the `id` changes as well.
.alert.is-helpful
:marked
Repeat the extraction process whenever you add new app messages or edit existing ones.
Be careful not to lose the previous translations.
Specialized software can help manage the change process.
#app-pre-translation
:marked
### The app before translation
After the previous steps, the sample app _and_ its translation file are as follows:
+makeTabs(`
cb-i18n/ts/app/app.component.html,
cb-i18n/ts/app/app.component.ts,
cb-i18n/ts/app/app.module.ts,
cb-i18n/ts/app/main.1.ts,
cb-i18n/ts/i18n/fr/messages.fr.xlf.html
`, '', `
app/app.component.html,
app/app.component.ts,
app/app.module.ts,
app/main.ts,
i18n/fr/messages.fr.xlf
`)
a#merge
.l-main-section
:marked
## Merge the translation file
To merge the translated text into component templates,
you compile the application with the completed translation file.
The process is the same whether the file is in `.xlf` format or
in one of the other formats (`.xlif` and `.xtb`) that Angular understands.
You provide the Angular compiler with three new pieces of information:
* the translation file
* the translation file format
* the _Locale ID_ (`en-US` for instance)
_How_ you provide this information depends upon whether you compile with
the JiT (_Just-in-Time_) compiler or the AoT (_Ahead-of-Time_) compiler.
* with [JiT](#jit), you provide the information at bootstrap time.
* with [AoT](#aot), you pass the information as `ngc` options.
a#jit
.l-main-section
:marked
### Merge with the JiT compiler
The JiT (_Just-in-Time_) compiler compiles the application in the browser as the application loads.
Translation with the JiT compiler is a dynamic process of ...
1. determining the language version for the current user,
2. importing the appropriate language translation file as a string constant,
3. creating corresponding translation providers to guide the JiT compiler,
4. bootstrapping the application with those providers.
Open `index.html` and revise the launch script as shown here:
+makeExample('cb-i18n/ts/index.html', 'i18n', 'index.html (launch script)')(format='.')
:marked
In this sample, the user's language is hardcoded as a global `document.locale` variable
in the `index.html`.
a#text-plugin
:marked
### SystemJS Text plugin
Notice the SystemJS mapping of `text` to a `systemjs-text-plugin.js`.
With the help of a text pluglin, SystemJS can read any file as raw text and
return the contents as a string.
You'll need it to import the language translation file.
SystemJS doesn't ship with a raw text plugin but it's easy to add.
Create the following `systemjs-text-plugin.js` in the root folder:
+makeExample('cb-i18n/ts/systemjs-text-plugin.js', null, 'systemjs-text-plugin.js')(format='.')
:marked
### Create translation providers
Three providers tell the JiT compiler how to translate the template texts for a particular language
while compiling the application:
* `TRANSLATIONS` is a string containing the content of the translation file.
* `TRANSLATIONS_FORMAT` is the format of the file: `xlf`, `xlif` or `xtb`
* `LOCALE_ID` is the locale of the target language.
The `getTranslationProviders` function in the following `app/i18n-providers.ts`
creates those providers based on the user's _locale_
and the corresponding translation file:
+makeExample('cb-i18n/ts/app/i18n-providers.ts', null, 'app/i18n-providers.ts')
:marked
* It gets the locale from the global `document.locale` variable that was set in `index.html`.
* If there is no locale or the language is English, there is no need to translate.
The function returns an empty `noProviders` array as a `Promise`.
It must return a `Promise` because this function could read a translation file asynchronously from the server.
* It creates a transaction filename from the locale according to the name and location convention
[described earlier](#i18n-file-structure).
* The `getTranslationsWithSystemJs` method reads the translation and returns the contents as a string.
Notice that it appends `!text` to the filename, telling SystemJS to use the [text plugin](#text-plugin).
* The callback composes a providers array with the three translation providers.
* Finally, `getTranslationProviders` returns the entire effort as a promise.
### Bootstrap the app with translation providers
The Angular `bootstrapModule` method has a second, _options_ parameter
that can influence the behavior of the compiler.
You'll create an _options_ object with the translation providers from `getTranslationProviders`
and pass it to `bootstrapModule`.
Open the `app/main.ts` and modify the bootstrap code as follows:
+makeExample('cb-i18n/ts/app/main.ts', null, 'app/main.ts')(format=".")
:marked
Notice that it waits for the `getTranslationProviders` promise to resolve before
bootstrapping the app.
The app is now _internationalized_ for English and French and there is a clear path for adding
more languages.
a#aot
.l-main-section
:marked
### _Internationalize_ with the AoT compiler
The JiT compiler translates the application into the target language while compiling dynamically in the browser.
That's flexible but may not be fast enough for your users.
The AoT (_Ahead-of-Time_) compiler is part of a build process that produces a small, fast, ready-to-run application package.
When you internationalize with the AoT compiler, you pre-build a separate application package for each
language. Then in the host web page (`index.html`), you determine which language the user needs
and serve the appropriate application package.
This cookbook doesn't cover how to build multiple application packages and
serve them according to the user's language preference.
It does explain the few steps necessary to tell the AoT to apply a translations file.
Internationalization with the AoT compiler requires some setup specifically for AoT.
Start with application project as shown [just before merging the translation file](#app-pre-translationStart)
and refer to the [AoT cookbook](aot-compiler.html) to make it _AoT-ready_.
Next, issue an `ngc` compile command for each supported language (including English).
The result is a separate version of the application for each language.
Tell AoT how to translate by adding three options to the `ngc` command:
* `--i18nFile`: the path to the translation file
* `--locale`: the name of the locale
* `--i18nFormat`: the format of the localization file
For this sample, the French language command would be
code-example(language="sh" class="code-shell").
./node_modules/.bin/ngc --i18nFile=./i18n/fr/messages.fr.xlf --locale=fr --i18nFormat=xlf
.l-sub-section
:marked
Windows users may have to quote the command:
code-example(language="sh" class="code-shell").
"./node_modules/.bin/ngc" --i18nFile=./i18n/fr/messages.fr.xlf --locale=fr --i18nFormat=xlf

View File

@ -147,11 +147,11 @@
"upgrade": { "upgrade": {
"title": "Upgrading from 1.x", "title": "Upgrading from 1.x",
"intro": "Angular 1 applications can be incrementally upgraded to Angular 2." "intro": "Incrementally upgrade an Angular 1 application to Angular 2."
}, },
"webpack": { "webpack": {
"title": "Webpack: an introduction", "title": "Webpack: an introduction",
"intro": "Create your Angular applications with a Webpack based tooling" "intro": "Create Angular applications with a Webpack based tooling"
} }
} }