Angular 專案 國際化

NO IMAGE

正如angular官網所說,專案國際化是一件具有挑戰性,需要多方面的努力、持久的奉獻和決心的任務。
本文將介紹angular專案的國際化方案,涉及靜態檔案(html)和ts檔案文案的國際化。

背景

Angular: 5.0
Angular Cli: 1.6.1(1.5.x也可以)
NG-ZORRO: 0.6.8

Angular i18n

i18n模板翻譯流程有四個階段:

在元件模板中標記需要翻譯的靜態文字資訊(即打上i18n標籤)。
Angular的i18n工具將標記的資訊提取到一個行業標準的翻譯原始檔(如.xlf檔案,使用ng xi18n)。
翻譯人員編輯該檔案,翻譯提取出來的文字資訊到目標語言,並將該檔案還給你(需要翻譯人員接入,本文采用將xlf檔案轉為json格式檔案輸出,最終將json檔案轉換回xlf格式檔案)。
Angular編譯器匯入完成翻譯的檔案,使用翻譯的文字替換原始資訊,並生成新的目標語言版本的應用程式。

你可以為每種支援的語言構建和部署單獨的專案版本,僅需替換翻譯後的xlf檔案即可。

如何在模板檔案中使用?

i18n提供了幾種使用方式,還專門為單複數提供了翻譯方式(個人沒有使用,感覺不太方便)。接下來以一個單獨的html檔案來介紹幾種使用方法。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Angular i18n</title>
</head>
<body>
<h1 i18n="Site Header|An introduction header for i18n [email protected]@stTitle">Angular 國際化專案</h1>
<p>
<span i18n="@@agDescription">國際化是一項很具有挑戰性,需要多方面的努力、持久的奉獻和決心的任務。</span>
<span class="delete" i18n-title="@@agDelete" title="刪除"></span>
</p>
<p><ng-container [email protected]@agLetGo>讓我們現在開始吧!</ng-container>朋友!</p>
</body>
</html>

上述程式碼展示了幾種i18n的使用方式:

1、使用i18n屬性標記(可新增上說明性文案,格式如:title|[email protected]@id,title和description可幫助翻譯人員更好地理解文案含義,是否新增取決於自身專案情況)

可以在靜態標籤上直接打上i18n的tag,如
<span i18n="@@agDescription"></span>
生成的xlf(xml)欄位格式為
<trans-unit id="agDescription" datatype="html">
<source>國際化是一項很具有挑戰性,需要多方面的努力、持久的奉獻和決心的任務。</source>
<context-group purpose="location">
<context context-type="sourcefile">xxx.ts</context>
<context context-type="linenumber">linenum</context>
</context-group>
</trans-unit>

2、為title新增i18n屬性

對於html標籤屬性,同樣可以新增i18n,如
<span class="delete" i18n-title="@@agDelete" title="刪除"></span>
生成的xlf(xml)格式同上

3、翻譯文字,而不必建立元素

我們有時候會出現一句話多個斷句情況,如果每次都新增span、label這些元素包裹的話,可能嚴重影響頁面佈局,這時候我們可以使用ng-container來包裹需要翻譯的文案。
<p>
<ng-container [email protected]@agLetGo>讓我們現在開始吧!</ng-container>朋友!
</p>
在頁面顯示為
<p>
<!---->
LET'S GO朋友!
</p>
* ng-container變為了註釋塊,這樣做不會影響頁面佈局(尤其是應用了style樣式的情況)

打上標籤後,我們只要執行ng xi18n即可自動建立出xlf檔案,通常為message.xlf,如需自定義,可自行前往 Angular CLI 官網檢視。

XLF與JSON轉換

xlf轉json方法

我個人是採用xml2js庫進行操作,簡單程式碼如下:
const fs = require('fs');
xml2js = require('xml2js');
var parser = new xml2js.Parser();
fs.readFile(fileName, 'utf8', (err, data) => {
parser.parseString(data, function (err, result) {
// 讀取新檔案全部需要翻譯的資料,並對比已翻譯的進行取捨,具體轉換成的格式結構可自行檢視
result['xliff']['file'][0]['body'][0]['trans-unit'].forEach((item) => {
var itemFormat = {
"key"  : item['$']['id'],
"value": item['source'][0]
};
// 執行相關操作,key-value形式是為了統一翻譯檔案結構,可按需定義
})
});
});

json轉xlf方法

function backToXLF(translatedParams) {
// 檔案格式可自行參考angular.cn官網的例子
var xlfFormat = {
"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": []
}
]
}
]
}
};
if (translatedParams instanceof Array) {
// 獲取原始名稱
translatedParams.forEach((data) => {
var tmp = {
"$"     : {
"id"      : data.key,
"datatype": "html"
},
"source": [i18nItemsOrigin[data.key]], // 這裡的i18nItemsOrigin是json格式,屬性名為key值,表示原始文案
"target": [data.value]
};
// 陣列,json項
xlfFormat['xliff']['file'][0]['body'][0]['trans-unit'].push(tmp);
});
}
var builder = new xml2js.Builder();
var xml = builder.buildObject(xlfFormat);
return xml;
}
這樣提取文案資訊和轉換翻譯後的檔案就完成了,接下來我們需要把翻譯好的文案應用到專案中去。

部署翻譯檔案

JIT模式
src目錄下新建locale資料夾,將翻譯轉換後的demo.en-US.xlf檔案存在改目錄下
app資料夾下新建i18n-providers.ts

import {
LOCALE_ID,
MissingTranslationStrategy,
StaticProvider,
TRANSLATIONS,
TRANSLATIONS_FORMAT
} from '@angular/core';
import { CompilerConfig } from '@angular/compiler';
import { Observable } from 'rxjs/Observable';
import { LOCALE_LANGUAGE } from './app.config'; // 自行定義配置位置
export function getTranslationProviders(): Promise<StaticProvider[]> {
// get the locale string from the document
const locale = LOCALE_LANGUAGE.toString();
// return no providers
const noProviders: StaticProvider[] = [];
// no locale or zh-CN: no translation providers
if (!locale || locale === 'zh-CN') {
return Promise.resolve(noProviders);
}
// Ex: 'locale/demo.zh-MO.xlf`
const translationFile = `./locale/demo.${locale}.xlf`;
return getTranslationsWithSystemJs(translationFile)
.then((translations: string) => [
{ provide: TRANSLATIONS, useValue: translations },
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' },
{ provide: LOCALE_ID, useValue: locale },
{
provide: CompilerConfig,
useValue: new CompilerConfig({ missingTranslation: MissingTranslationStrategy.Error })
}
]).catch(() => noProviders); // ignore if file not found
}
declare var System: any;
// 獲取locale檔案
function getTranslationsWithSystemJs(file: string) {
let text = '';
const fileRequest = new XMLHttpRequest();
fileRequest.open('GET', file, false);
fileRequest.onerror = function (err) {
console.log(err);
};
fileRequest.onreadystatechange = function () {
if (fileRequest.readyState === 4) {
if (fileRequest.status === 200 || fileRequest.status === 0) {
text = fileRequest.responseText;
}
}
};
fileRequest.send();
const observable = Observable.of(text);
const prom = observable.toPromise();
return prom;
}

main.ts檔案修改為

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { getTranslationProviders } from './app/i18n-providers';
if (environment.production) {
enableProdMode();
}
getTranslationProviders().then(providers => {
const options = { providers };
platformBrowserDynamic().bootstrapModule(AppModule, options)
.catch(err => console.log(err));
});

別忘了將locale目錄新增到.angular-cli.json裡,來單獨打包。

AOT模式(推薦)

對於AOT模式打包的持續來說,不需要上述複雜的配置,只需要在原有build基礎上,加上相應的i18n檔案即可,如:

ng build --prod --build-optimizer -bh / --i18n-format=xlf --locale=en --i18n-file=./src/locale/demo.en-US.xlf

這樣打出的包會自動將翻譯檔案應用到專案中。

這樣我們對靜態文案的翻譯工作基本已經完成了,但是有些動態文案如ts檔案裡的文案或者第三方框架屬性該如何翻譯呢?下面會介紹針對 ts 檔案和 NG-ZORRO 框架實現動態文案翻譯的方案。

ts檔案文案和NG-ZORRO框架文案翻譯

具體思路

通過Pipe呼叫Service方法,根據對應的唯一id值匹配json物件裡的翻譯結果,進而返回渲染到前端,參考於NG-ZORRO框架的國際化實現方案。

首先我們定義一下json翻譯物件的格式,全部為三層結構,動態變數需要按%%包裹,這樣做的原因是和專案結構相關聯,也便於後期和i18n方式格式統一。

{
"app": {
"base": {
"hello": "檔案文案",
"userCount": "一共%num%人"
}
}
}

格式已定,我們繼續定義Service處理方式
這裡複用 NG-ZORRO的國際化方案 ,可以簡化我們的開發,有興趣的可以參看一下其原始碼。

*** TranslateService ***
import { Injectable } from '@angular/core';
// 引入語言配置和國際化檔案文案物件
import { LOCALE_LANGUAGE } from '../app.config';
import { enUS } from '../locales/demo.en-US';
import { zhCN } from '../locales/stream.zh-CN';
@Injectable()
export class TranslateService {
private _locale = LOCALE_LANGUAGE.toString() === 'zh-CN' ? zhCN : enUS;
constructor() {
}
// path為app.base.hello格式的字串,這裡按json層級取匹配改變數
translate(path: string, data?: any): string {
let content = this._getObjectPath(this._locale, path) as string;
if (typeof content === 'string') {
if (data) {
Object.keys(data).forEach((key) => content = content.replace(new RegExp(`%${key}%`, 'g'), data[key]));
}
return content;
}
return path;
}
private _getObjectPath(obj: object, path: string): string | object {
let res = obj;
const paths = path.split('.');
const depth = paths.length;
let index = 0;
while (res && index < depth) {
res = res[paths[index  ]];
}
return index === depth ? res : null;
}
}

這樣,只需要在Pipe中呼叫Service的translate方法即可

*** NzTranslateLocalePipe ***
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '../services/translate.service';
@Pipe({
name: 'nzTranslateLocale'
})
export class NzTranslateLocalePipe implements PipeTransform {
constructor(private _locale: TranslateService) {
}
transform(path: string, keyValue?: object): string {
return this._locale.translate(path, keyValue);
}
}

好了,現在我們處理邏輯已經完全結束了,下面介紹一下如何使用

*** NG-ZORRO 控制元件 ***
<nz-input [nzPlaceHolder]="'app.base.hello'|nzTranslateLocale"></nz-input> // 無動態引數
<nz-popconfirm [nzTitle]="'app.base.userCount'|nzTranslateLocale: {num:users.length}" ...>
... // 有動態引數
</nz-popconfirm>
*** ts檔案 ***
export class AppComponent implements OnInit {
demoTitle='';
users = ['Jack', 'Johnson', 'Lucy'];
constructor(privete translateService: TranslateService) {
}
ngOnInit() {
this.demoTitle = this.translateService.translate('app.base.hello');
}
}

以上流程基本上能滿足大部分angular專案的國際化需求,如果需要更加複雜的國際化情況,歡迎討論。

總結

Angular到5.0的國際化已經相對來說簡便了很多,我們只需要在合適的地方打上i18n的tag即可方便快速地提取需要翻譯文案,具體如何處理翻譯後的檔案因人而異,多種方法可幫助我們轉換(如本文通過nodejs)。

複雜一點的是無法通過打i18n標籤來翻譯的文字,NG-ZORRO的國際化方案彌補了這方面的不足,結合起來可以很方便地完成專案的國際化。 國際化如果沒有專門的團隊支援,翻譯難度很大,需要考慮的東西很多,比如繁體還有澳門繁體、臺灣繁體等,語法也不盡相同。

參考目錄

Angular的國際化(i18n)線上例子
NG-ZORRO Locale 國際化