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方案。

相关文章
相关标签/搜索