NG客制项目下的I18n国际化标准方案

方案选择

国际化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 languagetranslate.setDefaultLang('en');// the lang to use, if the lang isn't available, it will use the current loader to get themtranslate.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方案。

转载于:https://juejin.im/post/5a716902518825733201ee4c

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452773.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flsak项目--图片验证码

0. 图片验证码的使用流程 2.后端接口编写 verify_code.py中编写接口代码&#xff1a; # coding:utf-8from . import api from ihome.utils.captcha.captcha import captcha from ihome import redis_store, constants, db from flask import current_app, jsonify, make_respo…

数据库与数据库管理系统

数据库是长期存储在计算机内有组织的大量的共享的数据集合。可以供各种用户共享&#xff0c;具有最小冗余度和较高的数据独立性。数据库管理系统在数据库建立、运用和维护时对数据库进行统一控制&#xff0c;以保证数据的完整性、安全性&#xff0c;并在多用户同时使用数据库时…

如何提高团队情商

在公司发展中&#xff0c;总裁&#xff0c;总监&#xff0c;经理&#xff0c;项目经理&#xff0c;他们对团队的建设意义重大&#xff0c;工作很重要&#xff0c;但团队的情商才更重要&#xff0c;笔者公司的一个团队&#xff0c;三十多个人就像一个人&#xff0c;命令所到之处…

ubuntu java classpath 设置_在Ubuntu中正确设置java classpath和java_home

我有错误Exception in thread"main" java.lang.NoClassDefFoundError:当我尝试在Ubuntu上运行编译类时。我使用的是一个非常简单的helloworld示例&#xff0c;互联网上已有数百万的响应表明我的classpath和java_home变量设置错误。但是&#xff0c;我已经将etc/envir…

Polo the Penguin and Matrix

Little penguin Polo has an n  m matrix, consisting of integers. Lets index the matrix rows from 1 to n from top to bottom and lets index the columns from 1 to m from left to right. Lets represent the matrix element on the intersection of row i and column…

趣解 XSS和CSRF的原理

参考文章&#xff1a;趣解 XSS和CSRF的原理 推荐网站&#xff1a;古黑论 感谢作者分享&#xff01;

js异步解决方案 --- 回调函数 vs promise vs generater/yield vs async/await

javascript -- 深度解析异步解决方案 高级语言层出不穷, 然而唯 js 鹤立鸡群, 这要说道js的设计理念, js天生为异步而生, 正如布道者朴灵在 node深入浅出--(有兴趣的可以读一下, 很有意思^_^) , 异步很早就存在于操作系统的底层, 意外的是&#xff0c;在绝大多数高级编程语言中…

什么是TPDU

TPDU,全称Transport Protocol Data Unit&#xff0c;是指传送协议数据单元。代表从一个传输实体发送至另一个传输实体的消息。 我们需要为传输实体之间交换的数据单元起一个更加一般化的名字&#xff0c;TCP的术语是数据段&#xff0c;它很容易混淆&#xff0c;而且在TCP领域之…

sql注入基本原理

1. 参考文献&#xff1a; 趣解SQL注入原理 Sql注入基本原理 2.参考书籍

项目管理杂谈-员工的积极性在哪里?

项目开发过程中&#xff0c;每每有人感叹&#xff0c;曾几何时&#xff0c;队伍如何好带&#xff0c;如何好用&#xff0c;而如今&#xff0c;人心繁杂&#xff0c;队伍不好带了。很多人的想法是“人望高处走”&#xff0c;不停的寻找待遇及其他方面更好的单位。其实&#xff0…

centos7硬盘分区

首先在虚拟机的设置中为系统添加硬盘 使用fdisk -l /dev/sdb 查看未分区的硬盘 fdisk -l /dev/sda 这是已经分区好得 接下来我们就要对sdb进行分区: 首先使用fdisk /dev/sdb 接着输入m可以看到详细命令 进行添加分区 已经建立好4个主分区&#xff0c;在建立时会看到以下 删除…

java上传rar文件_java实现上传zip/rar压缩文件,自动解压

在pom中添加解压jar依赖4.0.0org.springframework.bootspring-boot-starter-parent2.1.2.RELEASEcom.hfuncompress0.0.1-SNAPSHOTuncompress上传压缩文件(rar或者zip格式),解压1.8org.springframework.bootspring-boot-starter-weborg.projectlomboklomboktrueorg.springframew…

从MapReduce的执行来看如何优化MaxCompute(原ODPS) SQL

摘要&#xff1a; SQL基础有这些操作&#xff08;按照执行顺序来排列&#xff09;&#xff1a; from join(left join, right join, inner join, outer join ,semi join) where group by select sum distinct count order by 如果我们能理解mapreduce是怎么实现这些SQL中的基本操…

套接字(socket)基本知识与工作原理

套接字&#xff08;socket&#xff09;基本知识与工作原理 一、Socket相关概念 Socket通常也称作“套接字”&#xff0c;用于描述IP地址和端口&#xff0c;是一个通信链的句柄。&#xff08;其实就是两个程序通信用的。&#xff09; SOCKET用于在两个基于TCP/IP协议的应用程序之…

python 多线程--重点知识

1.全局变量global的用法 2.多线程共享全局变量-args参数 注意args参数类型为元组&#xff0c;逗号不能少&#xff01;

Flask WTForm表单的使用

运行环境&#xff1a; python2.7 flask 0.11 flask-wtf 0.14.2 wtform能够通过一个类定义一些字段&#xff0c;这些字段会在前端生成标签&#xff0c;并且通过设置字段的验证规则&#xff0c;自动判断前端输入数据的格式。 一般用于用户登录&#xff0c;用户注册等信息录入。…

Java与C#个人之比较

网上这方面的比较文章已经有不少了&#xff0c;不过大都是要么从很高的角度说的&#xff0c;要么就是从底层说的&#xff0c;本人就以自己这几年的编程经历中的感受&#xff0c;来谈谈自己的体会。 相似性&#xff1a; Java和C#都是一门面向对象的语言&#xff0c;Java更多地…

java利用子类求正方形_Java程序设计实验2011

(2)掌握对象的声明和使用&#xff1b;(3)掌握构造方法的概念和使用&#xff1b;(4)掌握类及成员的访问控制符。2、实验任务(1)阅读下面的程序&#xff0c;在main()方法里添加语句完成如下的功能&#xff1a;①创建一个MyV alue类的对象myV alue。②为myV alue对象中的value域赋…

当导用模块与包的import与from的问题(模块与包的调用)

当在views.py里写impor models会不会报错呢&#xff1f; 1、Python里面的py文件都是每一行的代码。2、Python解释器去找一个模块的时候&#xff0c;只去sys.path的路径里找3、django项目启动&#xff08;django项目的启动文件是manage.py&#xff09;启动项目是将manage.py的路…

ack和seq

ACK (Acknowledgement&#xff09;&#xff0c;即确认字符&#xff0c;在数据通信中&#xff0c;接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。 seq是序列号&#xff0c;这是为了连接以后传送数据用的&#xff0c;ack是对收到的数据包的确认&#xff…