方案选择
国际化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: `{ { 'HELLO' | translate:param }}`})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{ 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;}复制代码
在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;//orthis.translate.onLangChange.subscribe((params: LangChangeEvent) => { this.lang = params.lang;});复制代码
最后,我们在Component里加上pipe,这个工作就完成了
{ {lang | i18nSelect: data}}复制代码
总结
i18n的方案其实更多是基于项目来进行选择的,某一项目下合适的方案,换到其他项目下可能就会变得不可控制。而项目的复杂度也会对i18n的进行产生影响,所以尽可能的,在项目早期把i18n的方案落实下去,调整之后的策略去匹配i18n方案。