238 lines
7.7 KiB
TypeScript
238 lines
7.7 KiB
TypeScript
import { EntityDict } from 'oak-domain/lib/types';
|
||
import { Feature } from '../types/Feature';
|
||
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
||
import { Cache } from './cache';
|
||
import { LocalStorage } from './localStorage';
|
||
import { Environment } from './environment';
|
||
import { assert } from 'oak-domain/lib/utils/assert';
|
||
import { I18n, Scope, TranslateOptions } from 'i18n-js';
|
||
|
||
import { LOCAL_STORAGE_KEYS } from '../constant/constant';
|
||
|
||
export class Locales<ED extends EntityDict & BaseEntityDict> extends Feature {
|
||
static REFRESH_STALE_INTERVAL = 30 * 24 * 3600 * 1000; // 正式环境下的主动刷新缓存策略
|
||
private cache: Cache<ED>;
|
||
private localStorage: LocalStorage;
|
||
private environment: Environment;
|
||
private language: string;
|
||
private defaultLng: string;
|
||
private i18n: I18n;
|
||
|
||
private async initializeLng() {
|
||
const savedLng = await this.localStorage.load(LOCAL_STORAGE_KEYS.localeLng);
|
||
if (savedLng) {
|
||
this.language = savedLng;
|
||
}
|
||
else {
|
||
await this.detectLanguage();
|
||
}
|
||
}
|
||
|
||
constructor(
|
||
cache: Cache<ED>,
|
||
localStorage: LocalStorage,
|
||
environment: Environment,
|
||
defaultLng: string
|
||
) {
|
||
super();
|
||
this.cache = cache;
|
||
this.localStorage = localStorage;
|
||
this.defaultLng = defaultLng;
|
||
this.environment = environment;
|
||
this.language = defaultLng;
|
||
// 也是异步行为,不知道是否有影响 by Xc
|
||
this.initializeLng();
|
||
this.i18n = new I18n(undefined, {
|
||
defaultLocale: defaultLng,
|
||
locale: this.language,
|
||
});
|
||
this.reloadDataset();
|
||
|
||
// i18n miss的默认策略
|
||
this.i18n.missingBehavior = 'loadData';
|
||
this.i18n.missingTranslation.register("loadData", (i18n, scope, options) => {
|
||
this.loadData(scope);
|
||
assert(typeof scope === 'string');
|
||
return scope.split('.').pop()!;
|
||
});
|
||
// 同时注册一个返回空字符串的策略
|
||
this.i18n.missingTranslation.register("returnNull", (i18n, scope, options) => {
|
||
return '';
|
||
});
|
||
}
|
||
|
||
private async detectLanguage() {
|
||
const env = await this.environment.getEnv();
|
||
const { language } = env;
|
||
this.language = language;
|
||
await this.localStorage.save(LOCAL_STORAGE_KEYS.localeLng, language);
|
||
}
|
||
|
||
private async reloadDataset() {
|
||
await this.cache.onInitialized();
|
||
const i18ns = this.cache.get('i18n', {
|
||
data: {
|
||
id: 1,
|
||
data: 1,
|
||
namespace: 1,
|
||
language: 1,
|
||
$$updateAt$$: 1,
|
||
},
|
||
});
|
||
const dataset: Record<string, any> = {};
|
||
i18ns.forEach(
|
||
({ namespace, data, language }) => {
|
||
if (dataset[language!]) {
|
||
dataset[language!]![namespace!] = data as Record<string, any>;
|
||
}
|
||
else {
|
||
dataset[language!] = {
|
||
[namespace!]: data,
|
||
};
|
||
}
|
||
}
|
||
);
|
||
this.i18n.store(dataset);
|
||
|
||
if (i18ns.length > 0) {
|
||
/**
|
||
* 前台启动时的数据刷新策略:
|
||
* dev环境无条件刷新,production环境只会主动刷新超过REFRESH_STALE_INTERVAL(30天)的i18n数据,
|
||
* 这样会导致在此期间内,如果不发生key miss,数据不会更新
|
||
* 程序员要谨慎对待这一特性,对于重要的i18n,在版本间尽量不要更新原有的key值。
|
||
*/
|
||
if (process.env.NODE_ENV === 'development') {
|
||
const nss = i18ns.map(ele => ele.namespace!);
|
||
await this.loadServerData(nss);
|
||
}
|
||
else {
|
||
const now = Date.now();
|
||
const nss = i18ns.filter(
|
||
ele => now - (ele.$$updateAt$$ as number) > Locales.REFRESH_STALE_INTERVAL
|
||
).map(ele => ele.namespace!);
|
||
if (nss.length > 0) {
|
||
await this.loadServerData(nss);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async loadServerData(nss: string[]) {
|
||
const { data: newI18ns } = await this.cache.refresh('i18n', {
|
||
data: {
|
||
id: 1,
|
||
data: 1,
|
||
namespace: 1,
|
||
language: 1,
|
||
$$createAt$$: 1,
|
||
$$updateAt$$: 1,
|
||
},
|
||
filter: {
|
||
namespace: {
|
||
$in: nss,
|
||
},
|
||
},
|
||
}, undefined, undefined, {
|
||
dontPublish: true,
|
||
useLocalCache: {
|
||
keys: nss,
|
||
gap: process.env.NODE_ENV === 'development' ? 10 * 1000 : 3600 * 1000,
|
||
onlyReturnFresh: true,
|
||
},
|
||
ignoreContext: true,
|
||
});
|
||
|
||
if (newI18ns.length > 0) {
|
||
const dataset: Record<string, any> = {};
|
||
newI18ns.forEach(
|
||
({ namespace, data, language }) => {
|
||
if (dataset[language!]) {
|
||
dataset[language!][namespace!] = data;
|
||
}
|
||
else {
|
||
dataset[language!] = {
|
||
[namespace!]: data,
|
||
};
|
||
}
|
||
}
|
||
);
|
||
this.i18n.store(dataset);
|
||
this.publish();
|
||
}
|
||
}
|
||
/**
|
||
* 当发生key缺失时,向服务器请求最新的i18n数据,对i18n缓存数据的行为优化放在cache中统一进行
|
||
* @param ns
|
||
*/
|
||
private async loadData(key: Scope) {
|
||
assert(typeof key === 'string');
|
||
const [ ns ] = key.split('.');
|
||
|
||
if (process.env.NODE_ENV === 'development') {
|
||
assert(!['undefined', 'notExist'].includes(ns));
|
||
}
|
||
|
||
await this.loadServerData([ns]);
|
||
if (!this.hasKey(key)) {
|
||
console.warn(`命名空间${ns}中的${key}缺失且可能请求不到更新的数据`);
|
||
if (process.env.NODE_ENV === 'development') {
|
||
console.warn('请增加好相应的键值后执行make:locale');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 暴露给小程序的Wxs调用
|
||
* @param key
|
||
*/
|
||
loadMissedLocale(key: string) {
|
||
this.loadData(key);
|
||
}
|
||
|
||
/**
|
||
* translate函数,这里编译器会在params里注入两个参数 #oakNamespace 和 #oakModule,用以标识文件路径
|
||
* @param key
|
||
* @param params
|
||
* @returns
|
||
*/
|
||
t(key: string, params?: TranslateOptions) {
|
||
// return key as string;
|
||
const ns = params!['#oakNamespace'];
|
||
const module = params!['#oakModule'];
|
||
|
||
let key2 = key;
|
||
if (key.includes('::')) {
|
||
// 公共模块
|
||
key2 = `${module}-l-${key}`.replace('::', '.');
|
||
}
|
||
else if (key.includes(':')) {
|
||
// entity
|
||
key2 = key.replace(':', '.');
|
||
}
|
||
else {
|
||
// 自身模块
|
||
key2 = `${ns}.${key}`;
|
||
}
|
||
|
||
return this.i18n.t(key2, params);
|
||
}
|
||
|
||
// 获得当前locales的状态,小程序需要dataset去Wxs里渲染,同时reRender也要利用version触发render
|
||
getState() {
|
||
return {
|
||
lng: this.language,
|
||
defaultLng: this.defaultLng,
|
||
dataset: this.i18n.translations,
|
||
version: this.i18n.version,
|
||
};
|
||
}
|
||
|
||
// 查看有无某值,不触发获取数据
|
||
hasKey(key: Scope, params?: TranslateOptions) {
|
||
this.i18n.missingBehavior = 'returnNull';
|
||
const result = this.i18n.t(key, params);
|
||
this.i18n.missingBehavior = 'loadData';
|
||
return result;
|
||
}
|
||
}
|