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 extends Feature { static REFRESH_STALE_INTERVAL = 30 * 24 * 3600 * 1000; // 正式环境下的主动刷新缓存策略 private cache: Cache; 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, 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 = {}; i18ns.forEach( ({ namespace, data, language }) => { if (dataset[language!]) { dataset[language!]![namespace!] = data as Record; } 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 = {}; 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; } }