NG客制項目下的I18n國際化標準方案

方案選擇

國際化i18ngit

​ 這個方案是最成熟的,同時也是官方的方案,可是這樣一個標準化的方案同時意味着靈活度不夠。當須要劃分feature module,須要客製化組件的時候,這個方案的實施的成本就會遠遠超過預期,所以在項目中放棄了該方案。github

ngx-translatetypescript

​ 這個方案是目前i18n一個比較優秀的替代方案,由Angular Core Team的成員Olivier Combe開發,能夠看作另外一個維度的i18n,除了使用Json替代xlf外,能夠自定義provider也是這個方案的特點之一,最終選擇了該方案。npm

I18nSelectPipe & I18nPluralPipejson

​ 做爲官方方案,這2個pipe在項目中仍然有機會被用到,特別是處理從API傳入數據時,使用這2個pipe會更便捷。bootstrap

依賴安裝

githubapi

https://github.com/ngx-translate/core瀏覽器

@ngx-translate/core緩存

​ 首先安裝npm包。bash

> npm install @ngx-translate/core --save
複製代碼

​ 若是是NG4則須要指定版本爲7.2.2。

引用ngx-translate

在app.module.ts中,咱們進行引入,並加載。

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {TranslateModule} from '@ngx-translate/core';

@NgModule({
    imports: [
        BrowserModule,
        TranslateModule.forRoot()
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
複製代碼

​ 請不要遺漏forRoot(),全局有且僅有一個forRoot會生效,因此你的feature module在加載TranslateModule時請用這個方法。

@NgModule({
    exports: [
        CommonModule,
        TranslateModule
    ]
})
export class FeatureModule { }
複製代碼

​ 若是你的featureModule是須要被異步加載的那麼你能夠用forChild()來聲明,同時不要忘記設置isolate。

@NgModule({
    imports: [
        TranslateModule.forChild({
            loader: {provide: TranslateLoader, useClass: CustomLoader},
            compiler: {provide: TranslateCompiler, useClass: CustomCompiler},
            parser: {provide: TranslateParser, useClass: CustomParser},
            missingTranslationHandler: {provide: MissingTranslationHandler, useClass: CustomHandler},
            isolate: true
        })
    ]
})
export class LazyLoadedModule { }
複製代碼

​ 其中有些內容是容許咱們本身來定義加載,稍後進行描述。

異步加載Json配置文件

安裝http-loader

​ ngx-translate爲咱們準備了一個異步獲取配置的loader,能夠直接安裝這個loader,方便使用。

> npm install @ngx-translate/http-loader --save
複製代碼

使用http-loader

​ 使用這個加載器仍是很輕鬆愉快的,按照示例作就能夠了。

export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}

TranslateModule.forRoot({
            loader: {
                provide: TranslateLoader,
                useFactory: HttpLoaderFactory,
                deps: [HttpClient]
            }
        })
複製代碼

​ 若是要作AOT,只要稍微修改一下Factory就能夠了。

export function createTranslateLoader(http: HttpClient) {
    return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
複製代碼

i18n Json文件

​ 先創建一個en.json。

{
    "HELLO": "hello {{value}}"
}
複製代碼

​ 再創建一個cn.json。

{
    "HELLO": "歡迎 {{value}}"
}
複製代碼

​ 2個文件都定義了HELLO這個key,當i18n進行處理的時候,會獲取到對應的值。

​ 將這2個文件放到服務器端的/assets/i18n/目錄下,就快要經過http-loader異步獲取到了。

Component中的使用

import {Component} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';

@Component({
    selector: 'app',
    template: ` <div>{{ 'HELLO' | translate:param }}</div> `
})
export class AppComponent {
    param = {value: 'world'};

    constructor(translate: TranslateService) {
        // this language will be used as a fallback when a translation isn't found in the current language
        translate.setDefaultLang('en');

         // the lang to use, if the lang isn't available, it will use the current loader to get them
        translate.use('en');
    }
}
複製代碼

​ template中使用了HELLO這個key,而且經過translatePipe來進行處理,其中的param,使得I18n中的value會被解析成world。

​ 而在constructor中依賴的TranslateService,是咱們用來對i18n進行設置的provider,具體的Methods能夠參照官方文檔。

根據模組來拆分I18n

​ 以上內容都不是重點,若是簡單使用統一的json,很難知足複雜的開發需求。咱們須要更靈活的方案來解決開發中的痛點,這一點ngx-translate也爲咱們準備了改造的方法。

i18n文件跟隨模組和組件

​ 項目的模組和組件隨着項目開發會逐漸增多,統一維護會耗費很多精力,所以選擇使用ts來描述I18n內容,同時在模組中引入。固然,若是有使用json-loader,也可使用json,文件修改成en.ts。

export const langPack = {
    "Workspace@Dashboard@Hello": "hello {{value}}"
}
複製代碼

​ 在組件中將i18n內容合併成組件的langPack,這樣,每一個組件只要維護各自的langPack便可,不須要再過多的關注其餘部分的i18n。

import {langPack as cn} from './cn';
import {langPack as en} from './en';

export const langPack = {
    en,
    cn,
}
複製代碼

命名規則與合併

​ 國際化比較容易碰到的一個問題是,各自維護各自的key,若是出現重名的時候就會出現相互覆蓋或錯誤引用的問題,所以咱們須要定義一個命名規則,來防止串號。目前沒有出現須要根據版本不一樣修改i18n的需求,所以以以下方式定義key。

Project@Feature@Tag
複製代碼

​ 各組件的i18n最終會彙總在module中,所以會經過以下方式進行合併。

import {DashboardLangPack} from './dashboard'
export const WorkspaceLangPack = {
    en: {
      ...DashboardLangPack.en
    },
    cn: {
      ...DashboardLangPack.cn
    }
  }
複製代碼

​ 各module在DI的過程當中也會經過相似的方式進行合併,最終在app module造成一個i18n的彙總,並經過自定義的loader來進行加載。

自定義實施

CustomLoader

​ 想要定義CustomLoader,首先咱們須要加載TranslateLoader。

import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
複製代碼

​ 而後咱們自定義一個CustomLoader。

export class CustomLoader implements TranslateLoader {
    langPack = {};
    constructor(langPack) {
      this.langPack = langPack;
    }
    getTranslation(lang: string): Observable<any> {
      console.log(this.langPack[lang]);
      return Observable.of(this.langPack[lang]);
    }
  }
複製代碼

​ 這樣一個簡單的CustomLoader,就能夠知足咱們對於同步加載i18n的需求,能夠看到,咱們定義了一個Observable的方法getTranslation,經過這個方法,咱們返回了一個數據管道。咱們看一下TranslateLoader的聲明。

export declare abstract class TranslateLoader {
    abstract getTranslation(lang: string): Observable<any>;
}
複製代碼

​ 在ngx-translate使用咱們的loader時,會使用getTranslation方法,因此Loader的關鍵就在於正確的定義getTranslation的數據獲取部分。

​ 咱們再來看一下以前有提到過的TranslateHttpLoader,在定義了getTranslation的同時,從constructor裏獲取了HttpClient。

export declare class TranslateHttpLoader implements TranslateLoader {
    private http;
    prefix: string;
    suffix: string;
    constructor(http: HttpClient, prefix?: string, suffix?: string);
    /**
     * Gets the translations from the server
     * @param lang
     * @returns {any}
     */
    getTranslation(lang: string): any;
}

複製代碼

​ 至此,Loader如何實現已經很清晰了,咱們看一下調用的方式。

TranslateModule.forRoot({
        loader: {
          provide: TranslateLoader,
          useFactory: () => new CustomLoader(option.langPack)
        }
      })
複製代碼

​ loader的用法大體與ng的provider至關,這裏由於要傳值,使用了useFactory,一樣也有useClass和deps,能夠參考ng的相關用法。

​ 當loader被正確配置後,i18n的基礎工做就能被完成了,loader的做用就是爲ngx-translate來獲取i18n的字典,而後經過當前的lang來切換字典。

CustomHandler

​ i18n因爲要維護多語種字典,有時會發生內容缺失的狀況,當這個時候,咱們須要安排錯誤的處理機制。

​ 第一種方式,咱們可使用useDefaultLang,這個配置的默認爲true,所以咱們須要設置默認配置,須要加載TranslateService,並保證默認語言包的完整。

import { TranslateService } from '@ngx-translate/core';

class CoreModule {
    constructor(translate: TranslateService) {
      translate.setDefaultLang('en');
    }
  }
複製代碼

​ 另外一種方式,是咱們對缺乏的狀況進行Handler處理,在這個狀況下,咱們須要預先編寫CustomLoader。

import { MissingTranslationHandler, MissingTranslationHandlerParams } from '@ngx-translate/core';
 
export class CustomHandler implements MissingTranslationHandler {
    handle(params: MissingTranslationHandlerParams) {
        return 'no value';
    }
}
複製代碼

​ 咱們仍是來看一下Handler的相關聲明。

export interface MissingTranslationHandlerParams {
    /** * the key that's missing in translation files * * @type {string} */
    key: string;
    /** * an instance of the service that was unable to translate the key. * * @type {TranslateService} */
    translateService: TranslateService;
    /** * interpolation params that were passed along for translating the given key. * * @type {Object} */
    interpolateParams?: Object;
}
export declare abstract class MissingTranslationHandler {
    /** * A function that handles missing translations. * * @abstract * @param {MissingTranslationHandlerParams} params context for resolving a missing translation * @returns {any} a value or an observable * If it returns a value, then this value is used. * If it return an observable, the value returned by this observable will be used (except if the method was "instant"). * If it doesn't return then the key will be used as a value */
    abstract handle(params: MissingTranslationHandlerParams): any;
}
複製代碼

​ 咱們能很容易的瞭解到,當ngx-translate發現錯誤時,會經過handle丟一個MissingTranslationHandlerParams給咱們,然後咱們能夠根據這個params來安排錯誤處理機制。

​ 在這裏咱們簡單的返回了「no value」來描述丟失數據,再來加載這個handle。

TranslateModule.forRoot({
        missingTranslationHandler: { provide: CustomHandler, useClass: MyMissingTranslationHandler },
        useDefaultLang: false
      })
複製代碼

​ 想要missingTranslationHandler生效,不要忘記useDefaultLang!!!

CustomParser

​ 這個provider須要添加@Injectable裝飾器,仍是先給出code。

import { Injectable } from '@angular/core';
import { TranslateParser, TranslateDefaultParser } from '@ngx-translate/core';

@Injectable()
export class CustomParser extends TranslateDefaultParser {
    public interpolate(expr: string | Function, params?: any): string {

        console.group('interpolate');
        console.log('expr');
        console.log(expr);
        console.log('params');
        console.log(params);
        console.log('super.interpolate(expr, params)');
        console.log(super.interpolate(expr, params));
        console.groupEnd()
        const result: string = super.interpolate(expr, params)

        return result;
    }
    getValue(target: any, key: string): any {
        const keys = super.getValue(target, key);

        console.group('getValue');
        console.log('target');
        console.log(target);
        console.log('key');
        console.log(key);
        console.log('super.getValue(target, key)');
        console.log(super.getValue(target, key));
        console.groupEnd()
        return keys;
    }
}
複製代碼

​ 顧名思義Parse負責ngx-translate的解析,getValue進行解析,interpolate替換變量。看一下聲明的部分,註釋得至關清晰了。

export declare abstract class TranslateParser {
    /** * Interpolates a string to replace parameters * "This is a {{ key }}" ==> "This is a value", with params = { key: "value" } * @param expr * @param params * @returns {string} */
    abstract interpolate(expr: string | Function, params?: any): string;
    /** * Gets a value from an object by composed key * parser.getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA') ==> 'valueI' * @param target * @param key * @returns {string} */
    abstract getValue(target: any, key: string): any;
}
export declare class TranslateDefaultParser extends TranslateParser {
    templateMatcher: RegExp;
    interpolate(expr: string | Function, params?: any): string;
    getValue(target: any, key: string): any;
    private interpolateFunction(fn, params?);
    private interpolateString(expr, params?);
}

複製代碼

​ 個人示例代碼中只是簡單的將過程給打印了出來,在實際操做中,Parse能夠對數據進行至關程度的操做,包括單複數和一些特別處理,咱們應該在這個provider中去進行定義,能夠考慮經過curry(柯里化)的純函數疊加一系列處理功能。

​ 引用也是一樣的簡單。

TranslateModule.forRoot({
        parser: { provide: TranslateParser, useClass: CustomParser },
      }),
複製代碼

CustomCompiler

​ 這個provider也須要添加@Injectable裝飾器,先看一下代碼。

@Injectable()
export class CustomCompiler extends TranslateCompiler {
    compile(value: string, lang: string): string | Function {

        console.group('compile');
        console.log('value');
        console.log(value);
        console.log('lang');
        console.log(lang);
        console.groupEnd()
        return value;
    }


    compileTranslations(translations: any, lang: string): any {

        console.group('compileTranslations');
        console.log('translations');
        console.log(translations);
        console.log('lang');
        console.log(lang);
        console.groupEnd()
        return translations;
    }
}
複製代碼

​ 在運行過程當中,咱們會發現compileTranslations被正常觸發了,而compile並未被觸發。而且經過translate.use()方式更新lang的時候compileTranslations只會觸發一次,Parse會屢次觸發,所以能夠斷定translations加載後lang會被緩存。先看一下聲明。

export declare abstract class TranslateCompiler {
    abstract compile(value: string, lang: string): string | Function;
    abstract compileTranslations(translations: any, lang: string): any;
}
/** * This compiler is just a placeholder that does nothing, in case you don't need a compiler at all */
export declare class TranslateFakeCompiler extends TranslateCompiler {
    compile(value: string, lang: string): string | Function;
    compileTranslations(translations: any, lang: string): any;
}

複製代碼

​ 而後看一下官方的描述。

How to use a compiler to preprocess translation values

By default, translation values are added "as-is". You can configure a compiler that implements TranslateCompiler to pre-process translation values when they are added (either manually or by a loader). A compiler has the following methods:

  • compile(value: string, lang: string): string | Function: Compiles a string to a function or another string.
  • compileTranslations(translations: any, lang: string): any: Compiles a (possibly nested) object of translation values to a structurally identical object of compiled translation values.

Using a compiler opens the door for powerful pre-processing of translation values. As long as the compiler outputs a compatible interpolation string or an interpolation function, arbitrary input syntax can be supported.

​ 大部分時候咱們不會用到compiler,當咱們須要預處理翻譯值的時候,你會感覺到這個設計的強大之處。

TranslateService

​ 單獨列出這個service是由於你必定會用到它,並且它真的頗有用。

Methods:

  • setDefaultLang(lang: string): 設置默認語言
  • getDefaultLang(): string: 獲取默認語言
  • use(lang: string): Observable<any>: 設置當前使用語言
  • getTranslation(lang: string): Observable<any>:獲取語言的Observable對象
  • setTranslation(lang: string, translations: Object, shouldMerge: boolean = false): 爲語言設置一個對象
  • addLangs(langs: Array<string>): 添加新的語言到語言列表
  • getLangs(): 獲取語言列表,會根據default和use的使用狀況發生變化
  • get(key: string|Array<string>, interpolateParams?: Object): Observable<string|Object>: 根據key得到了一個ScalarObservable對象
  • stream(key: string|Array<string>, interpolateParams?: Object): Observable<string|Object>: 根據key返回一個Observable對象,有翻譯值返回翻譯值,沒翻譯值返回key,lang變動也會返回相應內容。
  • instant(key: string|Array<string>, interpolateParams?: Object): string|Object: 根據key返回相應內容,注意這是個同步的方法,若是不能確認是否是應該使用,請用get。
  • set(key: string, value: string, lang?: string): 根據key設置翻譯值
  • reloadLang(lang: string): Observable<string|Object>: 從新加載語言
  • resetLang(lang: string): 重置語言
  • getBrowserLang(): string | undefined: 得到瀏覽器語言(好比zh)
  • getBrowserCultureLang(): string | undefined: 得到瀏覽器語言(標準,好比zh-CN)

API、state的i18n處理方案

​ ngx-translate已經足夠強大,但咱們仍須要拾遺補缺,在咱們獲取數據的時候對某些須要i18n的內容進行處理,這個時候咱們可使用I18nSelectPipe和I18nPluralPipe。

​ 具體的使用方法在官網已有明確的描述,能夠參考具體的使用方式。

​ https://angular.cn/api/common/I18nSelectPipe

​ https://angular.cn/api/common/I18nPluralPipe

I18nSelectPipe

​ 這裏以I18nSelectPipe的使用進行簡單的描述,I18nPluralPipe大體相同。

​ 若是數據在傳入時或根節點就已經區分了語言,那麼咱們其實不須要使用pipe,就能夠直接使用了。pipe會使用的狀況大體是當咱們遇到以下數據結構時,咱們會指望進行自動處理。

data = {
    'cn': '中文管道',
    'en': 'English Pipe',
    'other': 'no value'
  }
複製代碼

​ 其中other是當語言包沒有正確命中時顯示的內容,正常的數據處理時其實不會有這部份內容,當未命中時,pipe會處理爲不顯示,若是有須要添加other,建議使用自定義pipe來封裝這個操做。

​ 設置當前lang。

lang = 'en';
複製代碼

​ 固然,若是你還記得以前咱們介紹過的TranslateService,它有一個屬性叫currentLang,能夠經過這個屬性獲取當前的語言,如果但願更換語言的時候就會同步更換,還可使用onLangChange。

this.lang = this.translate.currentLang;
//or
this.translate.onLangChange.subscribe((params: LangChangeEvent) => {
  this.lang = params.lang;
});
複製代碼

​ 最後,咱們在Component里加上pipe,這個工做就完成了

<div>{{lang | i18nSelect: data}} </div>
複製代碼

總結

​ i18n的方案其實更可能是基於項目來進行選擇的,某一項目下合適的方案,換到其餘項目下可能就會變得不可控制。而項目的複雜度也會對i18n的進行產生影響,因此儘量的,在項目早期把i18n的方案落實下去,調整以後的策略去匹配i18n方案。

相關文章
相關標籤/搜索