From a7932be9cef2f53e83b8088df4eb21ed34257c4b Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Sun, 11 Sep 2016 21:09:22 +0200 Subject: [PATCH] docs(guide): document internationalization (i18n) --- public/docs/_examples/i18n/e2e-spec.ts | 19 ++ .../i18n/ts-snippets/i18n.config.snippets.ts | 130 ++++++++ public/docs/dart/latest/guide/i18n.jade | 1 + public/docs/js/latest/guide/i18n.jade | 1 + public/docs/ts/latest/guide/_data.json | 5 + public/docs/ts/latest/guide/i18n.jade | 306 ++++++++++++++++++ 6 files changed, 462 insertions(+) create mode 100644 public/docs/_examples/i18n/e2e-spec.ts create mode 100644 public/docs/_examples/i18n/ts-snippets/i18n.config.snippets.ts create mode 100644 public/docs/dart/latest/guide/i18n.jade create mode 100644 public/docs/js/latest/guide/i18n.jade create mode 100644 public/docs/ts/latest/guide/i18n.jade diff --git a/public/docs/_examples/i18n/e2e-spec.ts b/public/docs/_examples/i18n/e2e-spec.ts new file mode 100644 index 0000000000..85dbcd8626 --- /dev/null +++ b/public/docs/_examples/i18n/e2e-spec.ts @@ -0,0 +1,19 @@ +/// +'use strict'; +describe('QuickStart E2E Tests', function () { + + let expectedMsg = 'Hello from Angular 2 App with i18n'; + + beforeEach(function () { + browser.get(''); + }); + + it(`should display: ${expectedMsg}`, function () { + expect(element(by.css('h1')).getText()).toEqual(expectedMsg); + }); + + it('should display an image', function () { + expect(element(by.css('img')).isPresent()).toBe(true); + }); + +}); diff --git a/public/docs/_examples/i18n/ts-snippets/i18n.config.snippets.ts b/public/docs/_examples/i18n/ts-snippets/i18n.config.snippets.ts new file mode 100644 index 0000000000..34b8b04c7c --- /dev/null +++ b/public/docs/_examples/i18n/ts-snippets/i18n.config.snippets.ts @@ -0,0 +1,130 @@ +/* tslint:disable */ +// #docregion i18n-directive +

Hello i18n

+// #enddocregion i18n-directive + +// #docregion i18n-directive-desc +

Hello i18n

+// #enddocregion i18n-directive-desc + +// #docregion i18n-directive-meaning +

Hello i18n

+// #enddocregion i18n-directive-meaning + +// #docregion i18n-plural-pipe +@Component({ + selector: 'app', + template: ` +
+ {{ messages.length | i18nPlural: messageMapping }} +
+ `, +}) +class MyApp { + messages: any[]; + messageMapping: {[k:string]: string} = { + '=0': 'No messages.', + '=1': 'One message.', + 'other': '# messages.' + } +} +// #enddocregion i18n-plural-pipe + +// #docregion i18n-select-pipe +@Component({ + selector: 'app', + template: ` +
+ {{ gender | i18nSelect: inviteMap }} +
+ `, +}) +class MyApp { + gender: string = 'male'; + inviteMap: any = { + 'male': 'Invite him.', + 'female': 'Invite her.', + 'other': 'Invite them.' + } +} +// #enddocregion i18n-select-pipe + +// #docregion tsconfig1 +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "module": "commonjs", + "outDir": "./dist/out-tsc" + }, + "files": [ + "./src/main.ts" + ] +} +// #enddocregion tsconfig1 + +// #docregion tsconfig2 +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "module": "commonjs", + "outDir": "./dist/out-tsc" + }, + "files": [ + "./src/main.ts" + ], + "angularCompilerOptions": { + "genDir": "./src/i18n" + } +} +// #enddocregion tsconfig2 + +// #docregion bootstrap +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from '@angular/core'; +import { environment } from './environments/environment'; +import { AppModule } from './app/'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule); +// #enddocregion bootstrap + +// #docregion bootstrap-i18n +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode, TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID } from '@angular/core'; +import { environment } from './environments/environment'; +import { AppModule } from './app/'; +import { TRANSLATION } from './i18n/messages.fr'; + +if (environment.production) { + enableProdMode(); +} + +// Compile using french translations +platformBrowserDynamic().bootstrapModule( + AppModule, + { + providers: [ + {provide: TRANSLATIONS, useValue: TRANSLATION}, + {provide: TRANSLATIONS_FORMAT, useValue: 'xlf'}, + {provide: LOCALE_ID, useValue: 'fr'} + ] + } +); +// #enddocregion bootstrap-i18n + + +// #docregion messages-ts +export const TRANSLATION = ` + + + ... + + `; +// #enddocregion messages-ts diff --git a/public/docs/dart/latest/guide/i18n.jade b/public/docs/dart/latest/guide/i18n.jade new file mode 100644 index 0000000000..6778b6af28 --- /dev/null +++ b/public/docs/dart/latest/guide/i18n.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/js/latest/guide/i18n.jade b/public/docs/js/latest/guide/i18n.jade new file mode 100644 index 0000000000..6778b6af28 --- /dev/null +++ b/public/docs/js/latest/guide/i18n.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index ee6a11e51b..b5b9c756dd 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -167,5 +167,10 @@ "webpack": { "title": "Webpack: an introduction", "intro": "Create your Angular 2 applications with a Webpack based tooling" + }, + + "i18n": { + "title": "Internationalization (i18n)", + "intro": "Easily translate your website into multiple languages" } } diff --git a/public/docs/ts/latest/guide/i18n.jade b/public/docs/ts/latest/guide/i18n.jade new file mode 100644 index 0000000000..814609ee11 --- /dev/null +++ b/public/docs/ts/latest/guide/i18n.jade @@ -0,0 +1,306 @@ +include ../_util-fns + +:marked + With internationalization, also known as i18n, we can display our website in multiple languages. There are two different + approaches to internationalization: with AoT compilation which requires a build step and a full reload when the language changes, + or with JiT which doesn't require a full reload and uses bindings to determine when a translation is needed. + + + + + ## Table of contents + + [Different approaches to internationalization](#approaches-internationalization) + + * [JiT](#jit) + * [AoT](#aot) + + [Angular 2 and i18n](#angular2-i18n) + + * [i18n directive](#i18n-directive) + * [i18n plural pipe](#i18n-plural-pipe) + * [i18n select pipe](#i18n-select-pipe) + + [Extract and use translations](#extract) + + * [Extract messages with ng-xi18n](#ng-xi18n) + * [Translate messages](#translate) + * [Use translations with the JiT approach](#use-jit) + * [Use translations with the AoT approach](#use-aot) + + +.l-main-section + +:marked + ## Different approaches to internationalization + + +a(id="jit") +.l-main-section +:marked + ### Just in time (JiT) approach + + The JiT approach relies on providing the translations to our application and binding them to our templates using directives, pipes and interpolations. + + Its main features are: + * Minimal server side support required: The same version of the application code is served by the server. + However, the server must also serve translated message bundles back to the application or have tools that embed all + translations in the application code at build time. + * Our application must track all the pieces of the UI that need to be updated when the locale changes. + In addition, if the new language strings are being loaded over the network, this could take time and the UI needs to indicate this in some way to the user. + * Allows one to support multiple languages in the same view. + As an example, a page could display a table showing how the user's advertising message might look in different locales. + This is fairly easy to do with this approach since it's fairly simple to have one locale per root node. + * The server is not required to determine the locale from the request – the client side can use cookies, the browser + language, and JS APIs to determine the language. + However, it's still be beneficial for the server to do some of this (e.g. to serve the likely language pack together with the application) + but that can be done at a later stage in development or for production. + + +a(id="aot") +.l-main-section +:marked + ### Ahead of time (AoT) compilation approach + + The AoT approach relies on precompiling the templates using the `ngc` compiler and then injecting the translations into these files. + + Its main features are: + * Performance win: by reloading the entire app, there is no need to track and update the bindings/UI that change as a result of the locale change. + * The locale changes so rarely that the cost of the reload is usually incurred only once. + * No information/state is (typically) lost. The user is presumably changing the language because he could not understand the earlier language. + This means that he doesn't have unsaved information in the application. + * Tools support: the strings to translate are easily extracted from our templates. + The resulting translations are in a generic format that can be consumed both by Angular 2 and one of the many translation softwares available. + * Cannot support multiple languages in the same application view. + * We have to reload the app to change the language. + * Extra server side support is needed: Since we generate different precompiled files for each language, the server must perform cookie/user agent analysis + to decide which localized version of the application code should be returned to the user. This also causes a cache miss. + * The server is now responsible for determining the default localized version to serve. (e.g. cookies / geo-ip / Accept-Language header, etc.) + + +a(id="angular2-i18n") +.l-main-section +:marked + ## Angular 2 and i18n + + Whatever approach we decide to use, we must update our templates to define which strings will be translated. + + +a(id="i18n-directive") +.l-main-section +:marked + ### i18n directive + + The i18n directive is what tells Angular where it should inject the translations. + It is used by both the JiT and the AoT approaches. + + We simply add the attribute `i18n` to an element and the content string will be replaced when necessary. + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'i18n-directive')(format=".") + +:marked + To help the translators, we can add more information about the meaning and context of this string. + Simply add a description to the i18n attribute: + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'i18n-directive-desc')(format=".") + +:marked + We can add some meaning as well, separate the meaning from the description with a pipe `|`: + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'i18n-directive-meaning')(format=".") + +:marked + Both the meaning and the description will be extracted by our messages extractor and added to the messages file. + It will help our translators to translate our application with a better understanding of what our text means. + + +a(id="i18n-plural-pipe") +.l-main-section +:marked + ### i18n plural pipe + + When we need to pluralize some terms based on values, we can use the plural pipe. + + This pipe provides different translations based on a number and on a mapping (which is an object + that mimics the [ICU format](http://userguide.icu-project.org/formatparse/messages)): + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'i18n-plural-pipe')(format=".") + +.alert.is-important + :marked + The plural pipe is not yet supported by the ng-xi18n messages extractor. + + +a(id="i18n-select-pipe") +.l-main-section +:marked + ### i18n select pipe + + Just like the plural pipe displays different translations based on a number, the select pipe is used to display different + translations based on a string that matches the current value. + + It also uses a mapping object that mimics the [ICU format](http://userguide.icu-project.org/formatparse/messages): + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'i18n-select-pipe')(format=".") +.alert.is-important + :marked + The select pipe is not yet supported by the ng-xi18n messages extractor. + + +a(id="extract") +.l-main-section +:marked + ## Extract and use translations + +a(id="ng-xi18n") +.l-main-section +:marked + ### Extract messages with ng-xi18n + + Now that our templates have been updated to support i18n translations, we can use the `ng-xi18n` messages extractor. + This cli tool is based on `ngc`, and is available in the `@angular/compiler-cli` npm package. + + To use it, the first thing that we have to do is to install it: + +code-example(language="sh"). + npm install @angular/compiler-cli --save + +:marked + Like `ngc`, `ng-xi18n` is based on `tsc`, the TypeScript compiler. It uses the file `tsconfig.json` to determine where + are our files. We have to make sure that we have defined the parameter `files` so that it knows what is the entry point + of our application. + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'tsconfig1')(format=".") + +:marked + We can then use the `ng-xi18n` binary to generate a file named `messages.xlf` at the root of our application. + +code-example(language="sh"). + ./node_modules/.bin/ng-xi18n + +.alert.is-helpful + :marked + It is considered good practice to create a new npm command that will be used to run `ng-xi18n`. + + Edit package.json and add the following command in the `scripts` property: `"extract": "ng-xi18n"`. + We can now generate our translations using the command `npm run extract`. + +:marked + If we want to generate this `messages.xlf` file somewhere in particular, `ng-xi18n` uses the same parameters as `ngc`. + + Let's edit our `tsconfig.json` file and add the `angularCompilerOptions` with `genDir` pointing to the folder where we + want `ng-xi18n` to generate the file. + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'tsconfig2')(format=".") + +:marked + The generated `messages.xlf` file uses by default the format `XML Localisation Interchange File Format` (XLIFF, version 1.2). + + `ng-x18n` and Angular 2 also support the `XML Message Bundle`(XMB) format. We can switch to this format by setting + the cli option `i18nFormat` to the value `xmb`: + +code-example(language="sh"). + ./node_modules/.bin/ng-xi18n --i18nFormat=xmb + +a(id="translate") +.l-main-section +:marked + ### Translate messages + + Now that we have generated a `messages` file, we have to make a copy by language that we want to support. + + If we want to support french for example, we can copy the file as `messages.fr`. We can start translating the messages, + or send those files to our translators. + + You can find a list of editors supporting the xlf format [here](https://en.wikipedia.org/wiki/XLIFF#Editors). + +.alert.is-helpful + :marked + If we choose the default `XLIFF` format, we have to add the translations into the `` elements of our `messages.xlf` + file, such as: `Bonjour i18n`. + +:marked + Whenever we add new messages in our application, we should run `ng-xi18n` again, and copy the added translations into + each of our localization files. + +.alert.is-helpful + :marked + Using a versionning system such as `GIT` can help us find out easily what new translations have been generated by + the messages extractor. + +a(id="use-jit") +.l-main-section +:marked + ### Use translations with the JiT approach + + Now that we have a localized file, we can tell Angular 2 to use it for all of our elements that have the `i18n` directive attribute. + + Angular 2 understand `xlf` and `xmb` formats, but we have to provide these message into our application. + To do that we will have to define a few providers at bootstrap time. + + Let's say that we have the following bootstrap file: + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'bootstrap')(format=".") + +:marked + We have to provide three values: `TRANSLATIONS`, `TRANSLATIONS_FORMAT` and `LOCALE_ID`: + * `TRANSLATIONS` is a string containing the content of our `messages` file for the chosen localization. + * `TRANSLATIONS_FORMAT` is either `xlf` or `xmb` depending on the format of our `messages` file. + * `LOCALE_ID` is a string representing the locale of our chosen language. + + For our `messages.fr.xlf` file, we would change the bootstrap like this: + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'bootstrap-i18n')(format=".") + +:marked + Since TypeScript is unable to import an `xlf` file, we have to create a `.ts` file that exports the content of our + `messages.fr.xlf` file. + + ++makeExample('i18n/ts-snippets/i18n.config.snippets.ts', 'messages-ts')(format=".") + +.alert.is-helpful + :marked + If you use Webpack you can use the [raw loader](https://github.com/webpack/raw-loader) to import your translations + file directly like this: `const TRANSLATION = require("raw!./i18n/messages.fr.xlf");` + +:marked + That's it, our application is now internationalized! Angular 2 will replace the content of our elements using + the `i18n` attribute directive with the french translations that we provided at bootstrap. + +.alert.is-important + :marked + Angular 2 is focused on providing the best AoT approach as possible which means that even if you can use i18n with the + JiT approach, the mecanism behind i18n in Angular 2 requires a full reload to change the language, you won't be able + to do it at runtime. + +a(id="use-aot") +.l-main-section +:marked + ### Use translations with the AoT approach + + Using the AoT approach requires a little bit of setup to make the `ngc` compiler work. Refer to the + [AoT cookbook](../cookbook/aot-compiler.html) to learn more about it. + + Once our project is ngc-ready, we will use `ngc` to compile a version of our application per locale. To do that + we will add three arguments to the cli command: + * `--i18nFile`: the path to our localization file + * `--locale`: the name of the locale + * `--i18nFormat`: the format of our localization file + + If we want to generate precompiled files for our french translations, we will use: + +code-example(language="sh"). + ngc --i18nFile=./src/i18n/messages.fr.xlf --locale=fr --i18nFormat=xlf + +.alert.is-helpful + :marked + Since you're supposed to generate precompiled file for each locale, you should probably use different `tsconfig.json` + files with a different `genDir` for each, and different npm commands pointing to each locale. + + You could also write a script that directly calls the `CodeGenerator` class (exported by the package `@angular/compiler-cli`) + for each locale. + +:marked + That's all that we have to do, the `ngc` compiler will replace the content of our elements using the i18n attribute + directive with our translations in the AoT precompiled files.