import { format } from './string-format'; import debounce from 'lodash.debounce'; import localforage from 'localforage'; import React from 'react'; localforage.config({ driver: [localforage.WEBSQL, localforage.LOCALSTORAGE], name: 'rrcTranslation', storeName: 'rrcTranslationStore', }); const translateStorage: TranslateStorage = {}; let enableMissingReport = true; let enableUsedReport = false; let cloudSetting; /** 缺失词条存储 */ const missingSet = new Set(); /** 已使用词条存储 */ const usedMap = new Map(); export interface Ref { current: T; } export interface PlainObject { [name: string]: any; } export interface TranslateStorage { [name: string]: PlainObject; } export interface ConfigurationBBLOptions { /** 本地化语种 */ locale?: string; /** 没有找到对应翻译信息的情况下采用的语种 */ fallback?: string; /** 系统标识 */ npid: number; /** bbl数据词典请求地址 */ url?: string; /** 支持多词典策略 */ uniqId?: string; /** 系统名称 */ projectName: string; /** 轮询时间间隔,秒为单位 */ seconds?: number; /** 是否开启字条缺失上报功能 */ enableMissingReport?: boolean; /** 是否开启字条是否已使用功能 */ enableUsedReport?: boolean; /** 初始化词条 */ initialTransData?: { [name: string]: any }; /** 对初始化错误进行拦截 */ onError?: (error: Error) => void; } /** 默认存储键的前缀 */ export const DEFAULT_STORAGE_KEY = '@@SESSION/TRANSLATIONS'; /** 默认轮询间隔 */ export const DEFAULT_LOOP_MS = 5 * 60; export interface FetchBBLDictionaryRequest { npid: number; url?: string; callback: (data: PlainObject) => void; onError?: (error: Error) => void; } async function fetchBBLDictionary(request: FetchBBLDictionaryRequest) { const { npid } = request; window['i18n_callback'] = request.callback; // 内网地址 : 'https://bbl.biz.sheincorp.cn/api/translation'}?npid=${npid}&callback=i18n_callback` fetch(`${request.url ?? 'https://cloud-now.sheincorp.cn/trans/api/translation'}?npid=${npid}&callback=i18n_callback`, { method: 'GET', mode: 'cors', credentials: 'omit', }) .then((x) => x.text()) .then((x) => { const sum = new Function(`${x}`); sum(); }) .catch((error) => { if (request.onError) { request.onError?.(error); } else { throw error; } }); } export interface PersistBBLDomain { transData: PlainObject | null | undefined; locale: string; fallback: string; projectName: string; } export interface PersistBBLRequest { domainData: PlainObject; } export interface SynchronizeRequest { ms: number; npid: number; url: string; persistBBL: (data: PlainObject) => Promise; callback?: (domainData: PlainObject) => void; onError?: (error: Error) => void; } async function synchronizeBBL(request: SynchronizeRequest) { const { npid, url, persistBBL, ms, callback, onError } = request; fetchBBLDictionary({ npid, url, callback: (data) => { persistBBL({ transData: data }); callback?.({ transData: data }); }, onError, }); setTimeout(() => synchronizeBBL(request), ms); } export interface CreateCloudSettingOptions { projectName: string; } /** 创建于云配置交互的对象 */ function createCloudSetting(options: CreateCloudSettingOptions) { const { projectName } = options; function createFetcher(key: 'used' | 'warn') { return (value: T) => fetch(`https://cloud-now.sheincorp.cn/api/options`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ appKey: 'e72abef04a9d3cf488ce0156', selfUpdate: true, uid: 'lcd', category: `i18n-${key}-key__${projectName}`, key: `i18n-${key}-key__${projectName}`, value: value, }), }); } const missingFetch = createFetcher('warn'); const usedFetch = createFetcher('used'); const sendMissing = debounce((value: Set) => { missingFetch(Array.from(value)); value.clear(); }, 5000); const sendUsed = debounce((value: Map) => { usedFetch(Array.from(value.values()).map((x) => ({ ...x, qty: String(x.qty) }))); value.clear(); }, 5000); return { sendMissing, sendUsed, }; } function transformLocale(locale) { return locale === 'EN' ? 'US' : locale; } export interface TranslateOptions { uniqId?: string; keys: any[]; locale?: string; } export function translate({ uniqId = 'default', keys, locale: $locale }: TranslateOptions) { const peristKey = `${DEFAULT_STORAGE_KEY}/${uniqId.toUpperCase()}`; const data = translateStorage[peristKey] ?? {}; const { transData, locale, fallback } = data; const [template, ..._params] = keys?.map((x) => { if (React.isValidElement(x)) { return x; } return String(x); }); /** 模板异常,直接返回空字符串 */ if (!template) { return ''; } /** 词典不存在,直接返回模板 */ if (!transData) { return format(template, _params); } /** 词条元数据不存在,直接返回模板 */ const textMeta = transData[template as never]; if (!textMeta) { /** 缺失词条 */ if (enableMissingReport) { missingSet.add(template); cloudSetting?.sendMissing?.(missingSet); } /** 缺失词条 */ return format(template, _params); } const localeText = textMeta[$locale ?? locale] || textMeta[fallback]; /** 如果对应的语种不存在,并且降级语种也不存在,那么直接返回模板 */ if (!localeText) { return format(template, _params); } /** 词条使用量统计 */ if (enableUsedReport) { const nid = textMeta.nid; if (usedMap.has(nid)) { usedMap.get(nid).qty += 1; } else { usedMap.set(nid, { id: nid, qty: 1 }); } cloudSetting?.sendUsed?.(usedMap); } /** 词条使用量统计 */ return format(localeText, _params); } export function configurationBBL(options: ConfigurationBBLOptions) { const { npid, url, seconds, locale: loc, fallback, projectName, initialTransData, onError } = options; if (!npid) { throw new Error('i18n: 缺少npid!'); } enableMissingReport = options?.enableMissingReport ?? true; enableUsedReport = options?.enableUsedReport ?? false; const locale = transformLocale(loc); const peristKey = `${DEFAULT_STORAGE_KEY}/${(options.uniqId ?? 'default').toUpperCase()}`; translateStorage[peristKey] = { fallback, locale, projectName, transData: initialTransData, }; cloudSetting = createCloudSetting({ projectName: projectName, }); /** 持久化bbl */ async function persistBBL(domainData: PlainObject) { const data = { locale, fallback, projectName, ...domainData, }; translateStorage[peristKey] = data; localforage.setItem(peristKey, data); } localforage.getItem(peristKey).then((x) => { translateStorage[peristKey] = x; }); const synchronize = async function (callback?: (domainData: PlainObject) => void) { synchronizeBBL({ ms: (seconds ?? DEFAULT_LOOP_MS) * 1000, persistBBL, npid, url, callback, onError, }); }; const getDictionary = () => translateStorage[peristKey]?.['transData']; const t = (...keys: any[]) => translate({ uniqId: options.uniqId, keys }); return { t, getT: (locale: string) => { translateStorage[peristKey] && (translateStorage[peristKey].locale = locale); return (...keys: any[]) => translate({ uniqId: options.uniqId, keys, locale }); }, synchronize, getDictionary, }; } export const getStorage = (uniqId: string = 'default') => { const peristKey = `${DEFAULT_STORAGE_KEY}/${uniqId.toUpperCase()}`; return translateStorage[peristKey]; };