Angular 項目 國際化

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

背景

  • Angular: 5.0
  • Angular Cli: 1.6.1(1.5.x也能夠)
  • NG-ZORRO: 0.6.8

Angular i18n

i18n模板翻譯流程有四個階段:
  1. 在組件模板中標記須要翻譯的靜態文本信息(即打上i18n標籤)。
  2. Angular的i18n工具將標記的信息提取到一個行業標準的翻譯源文件(如.xlf文件,使用ng xi18n)。
  3. 翻譯人員編輯該文件,翻譯提取出來的文本信息到目標語言,並將該文件還給你(須要翻譯人員接入,本文采用將xlf文件轉爲json格式文件輸出,最終將json文件轉換回xlf格式文件)。
  4. 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 Project@@stTitle">Angular 國際化項目</h1>
    <p>
      <span i18n="@@agDescription">國際化是一項很具備挑戰性,須要多方面的努力、持久的奉獻和決心的任務。</span>
      <span class="delete" i18n-title="@@agDelete" title="刪除"></span>
    </p>
    <p><ng-container i18n=@@agLetGo>讓咱們如今開始吧!</ng-container>朋友!</p>
</body>
</html>

上述代碼展現了幾種i18n的使用方式:node

一、使用i18n屬性標記(可添加上說明性文案,格式如:title|description@@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>
二、爲title添加i18n屬性
對於html標籤屬性,一樣能夠添加i18n,如
<span class="delete" i18n-title="@@agDelete" title="刪除"></span>
生成的xlf(xml)格式同上
三、翻譯文本,而沒必要建立元素
咱們有時候會出現一句話多個斷句狀況,若是每次都添加span、label這些元素包裹的話,可能嚴重影響頁面佈局,這時候咱們可使用ng-container來包裹須要翻譯的文案。
<p>
    <ng-container i18n=@@agLetGo>讓咱們如今開始吧!</ng-container>朋友!
</p>
在頁面顯示爲
<p>
    <!---->
    LET'S GO朋友!
</p>
* ng-container變爲了註釋塊,這樣作不會影響頁面佈局(尤爲是應用了style樣式的狀況)

打上標籤後,咱們只要執行ng xi18n便可自動建立出xlf文件,一般爲message.xlf,如需自定義,可自行前往 Angular CLI 官網查看。git

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.tsgithub

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文件修改成json

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裏,來單獨打包。bootstrap

AOT模式(推薦)數組

對於AOT模式打包的持續來講,不須要上述複雜的配置,只須要在原有build基礎上,加上相應的i18n文件便可,如:antd

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

這樣打出的包會自動將翻譯文件應用到項目中。app

這樣咱們對靜態文案的翻譯工做基本已經完成了,可是有些動態文案如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的國際化方案彌補了這方面的不足,結合起來能夠很方便地完成項目的國際化。 國際化若是沒有專門的團隊支持,翻譯難度很大,須要考慮的東西不少,好比繁體還有澳門繁體、臺灣繁體等,語法也不盡相同。

參考目錄
相關文章
相關標籤/搜索