在之前的文章中,已不止一次的给大家介绍了开源国际化开发框架——Singleton。对于企业级大型应用,其优势的确是肉眼可见的。然后,这是否意味着只要引入了Singleton框架,国际化问题或者说国际化中的三层问题就彻底被解决了么?答案是否定的。在接下来的几篇短文中,我想给大家分享的内容就是即便在import Singleton后,因为种种不规范、不推荐的写法和配置下,直接引发了多种奇葩的国际化问题。
问题
首先,让我们一起来关注一个字符串读取问题,现象如下:
在产品的中文主页面上不断的点击导航栏,发现UI大部分内容都已经被本地化,但依然存在少量模块(例如Inventory模块)中的小部分UI 内容依旧保持英文。检查代码中的资源文件,确定该模块对应的string已经被本地化,不存在遗漏问题。回到中文导航栏继续点击,Inventory模块的内容莫名的又变成了中文,问题不再重现。然而与此同时会产生新的随机事件,原先已经显示中文内容的某些模块(例如Deploy模块)会发生同样的问题,UI中小部分内容发生了回退,变成了英文。这里需要注意的是,该问题出现的相当随机,没有必现的步骤可以遵循。
分析
遇到这个问题后,个人的第一反应是该问题是与多线程相关的国际化问题,我们在之前的文章《国际化与多线程》和《国际化与多线程—续》中总结过Python和C#中的异步方法带来的locale混乱问题,而眼下的问题则同之前总结过的有着极其高的相似度。然而事实究竟如何呢?本项目中已经引入了Singleton框架,而Singleton中Lazy Load Module中也确实提供了异步非阻塞load string的方案,官方文档中进行了清晰的说明,这里需要格外注意的是——我们务必要使用stream订阅,以确保在使用同步API时加载了数据。如果我们不想实现locale切换或订阅流事件,也可以通过相应路由守卫中的I18nDataGuard以阻塞的方式加载数据,然后直接使用同步API。
这里的路由守卫实现了访问特定路由组件前的翻译检查功能,检查通过则放行,否则就阻止访问。通过从路由参数中读取精简的Singleton配置,将i18nDataGuard配置为路由对象的一部分,为Lazy Module加载i18n资源,并在模块激活和初始化时做好准备。
// Sample code for I18nDataGuard
const routes: Routes = [
{
path: '',
component: SampleComponent,
canActivate: [I18nDataGuard],
data: {
// 'vipConfig' is specified keyword of vip configuration.
// In the simplified configuration of lazy loading module,
// only the component name is required, and other fields are optional.
vipConfig: {
component: 'sample',
sourceBundles: [ENGLISH]
}
}
}
]
而本案的症结就在于开发人员既没有进行stream订阅,同时也未按照文档要求来实现i18n路由守卫。
解决方案
这里我们尝试选择第二种方式——路由守卫,来解决问题。首先在Lazy Load Module中,添加独立的了l10n.guard.ts文件。其次,在路由辞典中添加i18n路由守卫。
// Sample fixing code
// shared/guards/src/lib/ll10n/l10n.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { L10nService } from '@vmw/ngx-vip';
@Injectable()
export class L10nGuard implements CanActivate {
constructor(private l10nService: L10nService) {}
public canActivate(): Observable<boolean> {
return this.l10nService.stream.pipe(map(() => true));
}
}
// subscriptions/src/lib/subscriptions-routing.module.ts
@Injectable()
export class SubscriptionGuard implements CanActivate {
….
{
path: 'list',
component: Component1,
canActivate: [L10nGuard],
},
{
path: 'create',
component: Component2,
canActivate: [SubscriptionGuard, L10nGuard],
},
…
再次刷新已经添加了canActivate[… L10nGuard]的模块,问题烟消云散,而未添加的模块老问题依然随机出现。
总结
归根结底,引入该问题的根源在于开发人员引入Lazy Load Module时,务必要用stream订阅来确保翻译内容加载完毕,或者在需要进行守护的模块中调用I18nDataGuard,这样不规范或者说不正确的使用直接引发了本案中的奇怪现象。其实,这种随意甚至有些想当然的情况在日常国际化研发过程中屡见不鲜,对于我辈一线编码人员来说,不断的阅读和准确的理解编程语言和Singleton这样开源框架对应的文档,严格的按照不同模式下(如Singleton中的Root和Lazy)其所推荐的方法论进行编码,则是避免“千里之堤毁于蚁穴”的不二法门。