方案選擇
國際化i18n
這個方案是最成熟的,同時也是官方的方案,但是這樣一個標準化的方案同時意味著靈活度不夠。當需要劃分feature module,需要客製化元件的時候,這個方案的實施的成本就會遠遠超過預期,因此在專案中放棄了該方案。
ngx-translate
這個方案是目前i18n一個比較優秀的替代方案,由Angular Core Team的成員Olivier Combe開發,可以看做另一個維度的i18n,除了使用Json替代xlf外,可以自定義provider也是這個方案的特色之一,最終選擇了該方案。
I18nSelectPipe & I18nPluralPipe
作為官方方案,這2個pipe在專案中仍然有機會被用到,特別是處理從API傳入資料時,使用這2個pipe會更便捷。
依賴安裝
github
https://github.com/ngx-translate/core
@ngx-translate/core
首先安裝npm包。
> 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方案。