工作中经常遇到需要进行多种语言切换的项目。本文记录了一种在Angular页面中通过使用管道和自定义指令实现的语言切换方案。
1、实现效果
页面显示文字根据选择的语言自动进行翻译切换,如下图所示:
此时,页面模板的字符串全部按照管道格式书写:
<h1>{{ "app.title" | translate}}</h1>
<div>
<span>{{ "app.demo.text" | translate}}</span>
<input type="text" placeholder="{{ 'app.demo.placeholder' | translate}}">
<button (click)="showMessage()">{{"app.demo.btn" | translate}}</button>
</div>
<button *ngFor="let lan of languages" (click)="setLanguage(lan.code)">
{{lan.icon + " " + lan.text}}
</button>
其中 "app.title","app.demo.text",'app.demo.placeholder',"app.demo.btn" 等为各字符串的key,translate为管道选择器。translate管道通过key查找对应翻译进行翻译。用于查找翻译的字典通过加载文件方式加载到页面内存中。翻译文件格式如下:
管道通过调用语言服务类(LanService)中的翻译方法查找对应翻译。
2、语言服务类
语言服务类(LanService)中主要包含加载翻译文件、同步翻译、异步翻译、设置语言方法。
2.1、加载翻译文件方法
通过HttpClient.get()加载翻译文件:
/**
* 加载翻译文件
* @returns
*/
loadTranslation(languageCode: string = this.curLanguage) {
const jsonFile = `assets/language/${languageCode}.json`;
this.isLoading = true;
return new Promise<void>((resolve, reject) => {
this.http.get<any>(jsonFile).subscribe(data => {
this.translation = data;
this.lanChange.next(languageCode);
this.isLoading = false;
resolve();
}, err => {
reject(`load translation json error: ${err}`);
})
});
}
文件加载完成后会发出一个语言切换的通知(this.lanChange.next(languageCode);),订阅了该通知的方法将执行对应订阅逻辑。
2.2、同步翻译方法
通过key获取当前字典中对应的翻译,未查找到对应key的翻译直接返回key,如果服务正在加载翻译文件也直接返回key:
/**
* 获取单个文本的翻译 同步方法
* @param key 文本资源key
* @returns 对应翻译结果字符串,未查找到返回key
*/
translate(key: string) {
if (this.isLoading) return key;
else return this.translation[key] || key;
}
对于一些在确定语言包json已加载完成的场景中可以使用同步翻译方法获取翻译。比如页面中点击按钮提示的文字就可以采用同步方法获取翻译:
/**
* 弹出提示文字
*/
showMessage() {
alert(this.lanService.translate("app.welcome"));
}
2.3、异步翻译方法
通过key获取当前字典中对应的翻译,返回数据以Observable格式封装。如果服务正在加载翻译文件,则订阅语言切换的通知,待文件加载完成发出通知后,再查找对应翻译;相反则直接查找翻译:
/**
* 获取单个文本的翻译 异步方法
* @param key 文本资源key
* @returns 对应翻译结果字符串,未查找到返回key
*/
get(key: string) {
if (this.isLoading) {
return from(new Promise<string>(resolve => {
this.lanChange.subscribe(lan => {
resolve(this.translation[key] || key);
})
}));
} else {
return of(this.translation[key] || key);
}
}
2.4、设置语言方法
方法调用时,根据传入的语言重新加载翻译文件,加载后更新当前语言:
/**
* 设置页面语言
* @param languageCode 语言代码 zh-CN | en-US
*/
setLanguage(languageCode: string) {
if (this.curLanguage != languageCode) {
this.loadTranslation(languageCode).then(() => {
this.curLanguage = languageCode;
});
}
}
3、页面启动加载翻译文件
页面启动时需要加载翻译文件,可以在AppModule中注册页面启动的加载方法:
export function StartUpServiceFactory(startUpService: StartUpService) {
return () => startUpService.load();
}
const APP_INIT_PROVIDERS = [
StartUpService,
{
provide: APP_INITIALIZER,
useFactory: StartUpServiceFactory,
deps: [StartUpService],
multi: true
}
];
@NgModule({
//...
providers: [
...APP_INIT_PROVIDERS
],
// ...
})
export class AppModule { }
在启动服务类(StartUpService)中调用语言服务类(LanService)的加载翻译文件方法:
/**
* 启动加载项
*/
load() {
this.lan.loadTranslation();
}
4、管道方法
管道方法中采用异步获取翻译的方式进行文本转换,会将传入的key及其翻译进行缓存,每次查询会优先查询缓存值。没有缓存才会进行异步获取翻译。管道还会订阅语言服务类(LanService)的语言切换通知,收到通知后重新获取翻译,更新翻译结果:
transform(key: string) {
if (this.cacheKey == key) return this.translation;
this.cacheKey = key;
this.updateTranslation(key);
this.unsubscribe();
if (!this.lanChange$) {
this.lanChange$ = this.lanService.lanChange.subscribe(lan => {
if (this.cacheKey) {
this.cacheKey = "";
this.updateTranslation(key);
}
});
}
return this.translation;
}
其中updateTranslation()方法完成了异步获取翻译的操作:
/**
* 异步更新翻译
* @param key 字符串key
*/
updateTranslation(key: string) {
let callback = (tran: string) => {
this.translation = tran || key;
this.cacheKey = key;
this._ref.markForCheck();
}
this.lanService.get(key).subscribe(callback);
}
为了使Angular状态检查能够检测到管道值的变化,管道装饰器的pure字段必须设置为false,否则页面不会渲染更新修改语言而导致的文本变化:
@Pipe({
name: 'translate',
pure: false
})
export class TranslatePipe implements PipeTransform{}
5、自定义指令实现
使用上述管道就可以完全满足语言切换的需求,但是使用管道的时候,模板HTML页面代码中只能看到各字符串对应的key,如果是团队合作的代码或者长时间之前写的代码,就不能马上理解到key对应的翻译文本。这时候如果采用指令的形式实现,将key通过参数传入指令,通过指令修改元素innnerText。此时模板HTML页面中就可以保留任意语言对应的翻译,当然汉语最熟悉:
<h1 [lan]="'app.title'">多语言切换演示</h1>
<span [lan]="'app.demo.text'">文本内容演示</span>
<button [lan]="'app.demo.btn'" (click)="showMessage()">点击提示文字</button>
这样就既能满足切换语言需求,又能一眼看到中文信息,提高代码可读性。指令代码:
/**
* 传入的字符串key
*/
@Input('lan')
set lan(value: string) {
this.key = value;
this.setText(value);
}
/**
* 异步获取翻译设置元素内容文本
* @param key
*/
setText(key: string) {
key = key || '';
if (key.length > 0) {
const el = this.el.nativeElement;
this.lanService.get(key).subscribe(tran => {
el.innerText = tran;
});
}
}
//订阅服务类通知更新翻译
this.lanChange$ = this.lanService.lanChange.subscribe((lan) => {
this.changeDetectorRef.detectChanges();
if (this.key.length > 0) {
this.setText(this.key);
}
});
但是像placeholder这种还是只能使用管道。