@xiaonoodles/meetfun-i18n 1.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1173 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var vueI18n = require('vue-i18n');
6
+ var lodash = require('lodash');
7
+
8
+ /**
9
+ * 缓存管理类
10
+ */
11
+ class CacheManager {
12
+ constructor(localeCacheKey, langDictCacheKey, cacheMaxAge, baseMessages) {
13
+ this.localeCacheKey = localeCacheKey;
14
+ this.langDictCacheKey = langDictCacheKey;
15
+ this.cacheMaxAge = cacheMaxAge;
16
+ this.baseMessages = baseMessages;
17
+ }
18
+ /**
19
+ * 保存语言包数据到本地缓存
20
+ * @param langDictData 语言包数据
21
+ * @param version 版本号(可选)
22
+ */
23
+ saveLangDictToCache(langDictData, version = "1.0.0") {
24
+ try {
25
+ const cacheData = {
26
+ timestamp: Date.now(),
27
+ version,
28
+ data: langDictData,
29
+ };
30
+ uni.setStorageSync(this.langDictCacheKey, JSON.stringify(cacheData));
31
+ }
32
+ catch (error) {
33
+ console.error("国际化 保存语言包到缓存失败:", error);
34
+ }
35
+ }
36
+ /**
37
+ * 保存语言设置到缓存
38
+ * @param locale 语言代码
39
+ */
40
+ saveLocaleToCache(locale) {
41
+ try {
42
+ uni.setStorageSync(this.localeCacheKey, locale);
43
+ }
44
+ catch (error) {
45
+ console.error("国际化 保存语言到缓存失败:", error);
46
+ }
47
+ }
48
+ /**
49
+ * 获取缓存的语言设置
50
+ */
51
+ getCachedLocale() {
52
+ try {
53
+ const cachedLocale = uni.getStorageSync(this.localeCacheKey);
54
+ return cachedLocale || null;
55
+ }
56
+ catch (error) {
57
+ console.error("国际化 获取缓存语言失败:", error);
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * 从本地缓存获取语言包数据
63
+ * @returns 缓存的语言包数据或null
64
+ */
65
+ getLangDictFromCache() {
66
+ try {
67
+ const cachedDataStr = uni.getStorageSync(this.langDictCacheKey);
68
+ if (!cachedDataStr) {
69
+ return null;
70
+ }
71
+ const cachedData = JSON.parse(cachedDataStr);
72
+ const now = Date.now();
73
+ // 检查缓存是否过期
74
+ if (now - cachedData.timestamp > this.cacheMaxAge) {
75
+ uni.removeStorageSync(this.langDictCacheKey);
76
+ return null;
77
+ }
78
+ return cachedData.data;
79
+ }
80
+ catch (error) {
81
+ console.error("国际化 从缓存获取语言包失败:", error);
82
+ // 清除损坏的缓存数据
83
+ try {
84
+ uni.removeStorageSync(this.langDictCacheKey);
85
+ }
86
+ catch (e) {
87
+ console.error("国际化 清除损坏的缓存失败:", e);
88
+ }
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * 清除语言包缓存
94
+ */
95
+ clearLangDictCache() {
96
+ try {
97
+ uni.removeStorageSync(this.langDictCacheKey);
98
+ }
99
+ catch (error) {
100
+ console.error("国际化 清除语言包缓存失败:", error);
101
+ }
102
+ }
103
+ /**
104
+ * 获取初始化时的合并语言包(包含本地缓存)
105
+ * @returns 合并后的语言包数据
106
+ */
107
+ getInitialMergedMessages() {
108
+ try {
109
+ console.log("国际化 开始获取初始化合并语言包");
110
+ // 从缓存获取语言包数据
111
+ const cachedLangDict = this.getLangDictFromCache();
112
+ // 如果没有缓存数据,直接返回基础语言包
113
+ if (!cachedLangDict) {
114
+ console.log("国际化 没有缓存数据,使用基础语言包");
115
+ return this.baseMessages;
116
+ }
117
+ console.log("国际化 找到缓存的语言包数据,开始合并");
118
+ // 转换缓存数据格式
119
+ const { zh_cn: zhHans, zh_hk: zhHant, ...other } = cachedLangDict;
120
+ const correctDictionaryData = {
121
+ "zh-Hans": zhHans,
122
+ "zh-Hant": zhHant,
123
+ ...other,
124
+ };
125
+ // 合并基础语言包和缓存的语言包
126
+ const mergedMessages = lodash.merge({}, this.baseMessages, correctDictionaryData);
127
+ console.log("国际化 初始化合并完成,最终语言包:", Object.keys(mergedMessages));
128
+ return mergedMessages;
129
+ }
130
+ catch (error) {
131
+ console.error("国际化 获取初始化合并语言包失败:", error);
132
+ // 发生错误时返回基础语言包
133
+ return this.baseMessages;
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 语言配置枚举
140
+ */
141
+ exports.LANGUAGE_OPTIONS_CONFIGURATION = void 0;
142
+ (function (LANGUAGE_OPTIONS_CONFIGURATION) {
143
+ LANGUAGE_OPTIONS_CONFIGURATION["FOLLOW_SYSTEM"] = "followSystem";
144
+ LANGUAGE_OPTIONS_CONFIGURATION["EN"] = "en";
145
+ LANGUAGE_OPTIONS_CONFIGURATION["ZH_HANS"] = "zh-Hans";
146
+ LANGUAGE_OPTIONS_CONFIGURATION["ZH_HANT"] = "zh-Hant";
147
+ })(exports.LANGUAGE_OPTIONS_CONFIGURATION || (exports.LANGUAGE_OPTIONS_CONFIGURATION = {}));
148
+
149
+ /**
150
+ * 翻译管理类
151
+ */
152
+ class TranslateManager {
153
+ constructor(i18n, cacheManager, apiConfig, defaultLocale, defaultLangDictDomainCode, reportDelay, onLocaleChanged, debug = false, maxReportedKeysCache = 10000) {
154
+ this.missingKeyCache = {};
155
+ this.usedKeyCache = {};
156
+ this.reportedKeyCache = {}; // 已上报过的键缓存
157
+ this.reportedKeysOrder = new Map(); // LRU缓存顺序跟踪,key为 "domainCode:keyCode",value为时间戳
158
+ this.reportTimer = null;
159
+ this.i18n = i18n;
160
+ this.cacheManager = cacheManager;
161
+ this.apiConfig = apiConfig;
162
+ this.defaultLocale = defaultLocale;
163
+ this.defaultLangDictDomainCode = defaultLangDictDomainCode;
164
+ this.reportDelay = reportDelay;
165
+ this.onLocaleChanged = onLocaleChanged;
166
+ this.debug = debug;
167
+ this.maxReportedKeysCache = maxReportedKeysCache;
168
+ }
169
+ /**
170
+ * 调试日志输出方法
171
+ * 只有在 debug 模式下才会输出日志
172
+ */
173
+ log(...args) {
174
+ if (this.debug) {
175
+ console.log('[国际化 Debug]', ...args);
176
+ }
177
+ }
178
+ /**
179
+ * 获取已上报缓存的总键数量
180
+ */
181
+ getTotalReportedKeysCount() {
182
+ let count = 0;
183
+ for (const keySet of Object.values(this.reportedKeyCache)) {
184
+ count += keySet.size;
185
+ }
186
+ return count;
187
+ }
188
+ /**
189
+ * LRU 缓存清理机制 - 当缓存达到上限时,清理最旧的 20% 条目
190
+ */
191
+ evictOldestReportedKeys() {
192
+ if (this.maxReportedKeysCache === 0) {
193
+ // 0 表示不限制
194
+ return;
195
+ }
196
+ const totalCount = this.getTotalReportedKeysCount();
197
+ if (totalCount <= this.maxReportedKeysCache) {
198
+ return; // 未达到上限,无需清理
199
+ }
200
+ // 计算需要删除的数量(清理 20%)
201
+ const evictCount = Math.floor(this.maxReportedKeysCache * 0.2);
202
+ this.log(`缓存达到上限 (${totalCount}/${this.maxReportedKeysCache}),开始清理最旧的 ${evictCount} 条记录`);
203
+ // 按时间戳排序,找出最旧的条目
204
+ const sortedEntries = Array.from(this.reportedKeysOrder.entries())
205
+ .sort((a, b) => a[1] - b[1]) // 按时间戳升序排序
206
+ .slice(0, evictCount); // 取最旧的 evictCount 条
207
+ // 删除这些条目
208
+ let deletedCount = 0;
209
+ for (const [fullKey] of sortedEntries) {
210
+ const [domainCode, keyCode] = fullKey.split(':');
211
+ if (this.reportedKeyCache[domainCode]) {
212
+ this.reportedKeyCache[domainCode].delete(keyCode);
213
+ deletedCount++;
214
+ // 如果该域的 Set 为空,删除整个域
215
+ if (this.reportedKeyCache[domainCode].size === 0) {
216
+ delete this.reportedKeyCache[domainCode];
217
+ }
218
+ }
219
+ // 从顺序跟踪中删除
220
+ this.reportedKeysOrder.delete(fullKey);
221
+ }
222
+ this.log(`LRU 清理完成,已删除 ${deletedCount} 条记录,当前缓存数量: ${this.getTotalReportedKeysCount()}`);
223
+ }
224
+ /**
225
+ * 统一的变量替换方法 - 支持嵌套对象路径和复杂变量替换
226
+ * @param template 模板字符串,支持 {key} 或 {key.subkey} 格式的变量
227
+ * @param params 参数对象,支持嵌套对象
228
+ * @param options 可选配置
229
+ * @returns 替换后的字符串
230
+ */
231
+ interpolateVariables(template, params, options) {
232
+ // 默认配置
233
+ const config = {
234
+ fallbackToOriginal: true,
235
+ logWarnings: true,
236
+ escapeHtml: false,
237
+ ...options,
238
+ };
239
+ // 输入验证
240
+ if (!template || typeof template !== 'string') {
241
+ return template || '';
242
+ }
243
+ if (!params || typeof params !== 'object') {
244
+ return template;
245
+ }
246
+ try {
247
+ // 正则匹配 {key} 或 {key.subkey} 格式的变量
248
+ const result = template.replace(/\{([^}]+)\}/g, (match, keyPath) => {
249
+ try {
250
+ // 按点号分割路径,支持嵌套访问
251
+ const keys = keyPath.trim().split('.');
252
+ let value = params;
253
+ // 逐层取值
254
+ for (const key of keys) {
255
+ if (value && typeof value === 'object' && key in value) {
256
+ value = value[key];
257
+ }
258
+ else {
259
+ // 如果路径不存在
260
+ if (config.logWarnings) {
261
+ console.warn(`国际化 变量替换失败: 路径 "${keyPath}" 不存在于参数中`, params);
262
+ }
263
+ return config.fallbackToOriginal ? match : '';
264
+ }
265
+ }
266
+ // 处理不同类型的值
267
+ let finalValue;
268
+ if (value === null || value === undefined) {
269
+ finalValue = '';
270
+ }
271
+ else if (typeof value === 'object') {
272
+ // 对象类型转为JSON字符串
273
+ finalValue = JSON.stringify(value);
274
+ }
275
+ else {
276
+ // 其他类型转为字符串
277
+ finalValue = String(value);
278
+ }
279
+ // HTML转义(如果需要)
280
+ if (config.escapeHtml) {
281
+ finalValue = finalValue
282
+ .replace(/&/g, '&amp;')
283
+ .replace(/</g, '&lt;')
284
+ .replace(/>/g, '&gt;')
285
+ .replace(/"/g, '&quot;')
286
+ .replace(/'/g, '&#x27;');
287
+ }
288
+ return finalValue;
289
+ }
290
+ catch (error) {
291
+ if (config.logWarnings) {
292
+ console.error(`国际化 变量替换错误: 路径 "${keyPath}"`, error);
293
+ }
294
+ return config.fallbackToOriginal ? match : '';
295
+ }
296
+ });
297
+ return result;
298
+ }
299
+ catch (error) {
300
+ console.error('国际化 变量替换方法执行失败:', error);
301
+ return template;
302
+ }
303
+ }
304
+ /**
305
+ * 获取国际化热数据
306
+ */
307
+ async fetchLangDictHotData(systemCode) {
308
+ this.log('开始获取热数据, systemCode:', systemCode);
309
+ try {
310
+ const params = {
311
+ systemCode,
312
+ };
313
+ const res = await this.apiConfig.queryLangDictHotData(params);
314
+ this.log('热数据获取成功:', res.data);
315
+ // 使用工具类处理接口返回的国际化数据
316
+ await this.updateI18nMessages(res.data);
317
+ }
318
+ catch (error) {
319
+ console.error('[国际化] 获取国际化数据失败:', error);
320
+ this.log('尝试从缓存加载语言包');
321
+ // 从缓存获取语言包
322
+ const cachedLangDict = this.cacheManager.getLangDictFromCache();
323
+ if (cachedLangDict) {
324
+ this.log('找到缓存的语言包,使用缓存数据');
325
+ try {
326
+ // 使用缓存的语言包,但不再次保存到缓存
327
+ await this.updateI18nMessages(cachedLangDict, false);
328
+ this.log('缓存语言包加载成功');
329
+ }
330
+ catch (cacheError) {
331
+ console.error('[国际化] 加载缓存语言包失败:', cacheError);
332
+ }
333
+ }
334
+ else {
335
+ this.log('没有找到可用的缓存语言包');
336
+ }
337
+ }
338
+ }
339
+ /**
340
+ * 批量冷数据&缺失数据上报
341
+ */
342
+ async batchReportMissingKeys() {
343
+ if (Object.keys(this.missingKeyCache).length === 0 && Object.keys(this.usedKeyCache).length === 0) {
344
+ this.log('没有需要上报的缺失键或已用键');
345
+ return;
346
+ }
347
+ // 获取所有涉及的域代码
348
+ const allDomainCodes = new Set([
349
+ ...Object.keys(this.missingKeyCache),
350
+ ...Object.keys(this.usedKeyCache),
351
+ ]);
352
+ // 构建所有域的数据列表
353
+ const langDictColdDataCmdList = [];
354
+ for (const domainCode of allDomainCodes) {
355
+ // 过滤掉已上报过的键
356
+ const missingKeysSet = this.missingKeyCache[domainCode] || new Set();
357
+ const reportedKeysSet = this.reportedKeyCache[domainCode] || new Set();
358
+ // 只上报未上报过的缺失键
359
+ const missingKeys = Array.from(missingKeysSet).filter(key => !reportedKeysSet.has(key));
360
+ const usedKeys = Array.from(this.usedKeyCache[domainCode] || []);
361
+ if (missingKeys.length === 0 && usedKeys.length === 0)
362
+ continue;
363
+ langDictColdDataCmdList.push({
364
+ missingDictKeyList: missingKeys,
365
+ usedDictKeyList: usedKeys,
366
+ domainCode,
367
+ });
368
+ }
369
+ // 统一一次上报所有域的数据
370
+ if (langDictColdDataCmdList.length > 0) {
371
+ const params = {
372
+ langDictColdDataCmdList,
373
+ };
374
+ const totalMissingKeys = langDictColdDataCmdList.reduce((sum, item) => sum + item.missingDictKeyList.length, 0);
375
+ const totalUsedKeys = langDictColdDataCmdList.reduce((sum, item) => sum + item.usedDictKeyList.length, 0);
376
+ this.log(`开始批量上报翻译数据: 域数量=${langDictColdDataCmdList.length}, 总缺失键=${totalMissingKeys}, 总已用键=${totalUsedKeys}`, params);
377
+ try {
378
+ const res = await this.apiConfig.queryLangDictColdData(params);
379
+ this.log('冷数据上报成功,返回数据:', res.data);
380
+ // 上报成功后,将这些键添加到已上报缓存中
381
+ for (const item of langDictColdDataCmdList) {
382
+ const { domainCode, missingDictKeyList } = item;
383
+ // 初始化已上报缓存
384
+ if (!this.reportedKeyCache[domainCode]) {
385
+ this.reportedKeyCache[domainCode] = new Set();
386
+ }
387
+ // 将成功上报的缺失键添加到已上报缓存
388
+ const timestamp = Date.now();
389
+ missingDictKeyList.forEach(key => {
390
+ this.reportedKeyCache[domainCode].add(key);
391
+ // 记录到 LRU 顺序跟踪中
392
+ const fullKey = `${domainCode}:${key}`;
393
+ this.reportedKeysOrder.set(fullKey, timestamp);
394
+ });
395
+ this.log(`域 ${domainCode} 的 ${missingDictKeyList.length} 个缺失键已标记为已上报`);
396
+ }
397
+ // 检查并执行 LRU 清理
398
+ this.evictOldestReportedKeys();
399
+ // 处理返回的冷数据,合并到当前国际化语言包中
400
+ if (res.data) {
401
+ await this.updateI18nMessages(res.data);
402
+ }
403
+ }
404
+ catch (error) {
405
+ console.error(`国际化 统一上报翻译数据失败: 域数量=${langDictColdDataCmdList.length}, 总缺失键数量=${totalMissingKeys}, 总已用键数量=${totalUsedKeys}`, error);
406
+ // 上报失败时不添加到已上报缓存,下次继续尝试上报
407
+ }
408
+ }
409
+ // 清空缓存
410
+ Object.keys(this.missingKeyCache).forEach((domainCode) => {
411
+ this.missingKeyCache[domainCode].clear();
412
+ });
413
+ Object.keys(this.usedKeyCache).forEach((domainCode) => {
414
+ this.usedKeyCache[domainCode].clear();
415
+ });
416
+ }
417
+ /**
418
+ * 自定义翻译查找函数,直接从语言包中查找翻译并完成变量替换
419
+ * @param key 翻译键
420
+ * @param domainCode 域代码
421
+ * @param locale 目标语言
422
+ * @param params 变量替换参数
423
+ * @returns 翻译结果或null
424
+ */
425
+ findTranslation(key, domainCode, locale, params) {
426
+ try {
427
+ // 获取当前语言的语言包
428
+ const messages = this.getI18nMessages();
429
+ const localeMessages = messages[locale];
430
+ if (!localeMessages) {
431
+ console.warn(`国际化 语言包不存在: ${locale}`);
432
+ return null;
433
+ }
434
+ // 查找域下的翻译
435
+ const domainMessages = localeMessages[domainCode];
436
+ if (!domainMessages) {
437
+ console.warn(`国际化 域代码不存在: ${domainCode} in ${locale}`);
438
+ return null;
439
+ }
440
+ // 获取翻译模板
441
+ const template = domainMessages[key];
442
+ if (typeof template !== 'string') {
443
+ console.warn(`国际化 翻译键不存在: ${key} in ${domainCode}.${locale}`);
444
+ return null;
445
+ }
446
+ // 使用统一的变量替换方法
447
+ const result = this.interpolateVariables(template, params, {
448
+ fallbackToOriginal: true,
449
+ logWarnings: true,
450
+ escapeHtml: false,
451
+ });
452
+ return result;
453
+ }
454
+ catch (error) {
455
+ console.error(`国际化 查找翻译出错: key=${key}, domain=${domainCode}, locale=${locale}`, error);
456
+ return null;
457
+ }
458
+ }
459
+ /**
460
+ * 自定义翻译函数,支持缺失翻译的处理和手动变量替换
461
+ * @param key 翻译键
462
+ * @param params 翻译参数
463
+ * @returns 翻译结果
464
+ */
465
+ safeTranslate(key, params, domainCode) {
466
+ this.log('开始翻译:', { key, params, domainCode });
467
+ try {
468
+ let result = null;
469
+ // 获取当前语言
470
+ const currentLocale = typeof this.i18n.global.locale === 'string'
471
+ ? this.i18n.global.locale
472
+ : this.i18n.global.locale.value;
473
+ this.log('当前语言:', currentLocale);
474
+ // 使用传入的域代码,如果没有传入则使用默认域代码
475
+ const finalDomainCode = domainCode || this.defaultLangDictDomainCode;
476
+ this.log('使用域代码:', finalDomainCode);
477
+ // 首先尝试在当前语言中查找(包含变量替换)
478
+ result = this.findTranslation(key, finalDomainCode, currentLocale, params);
479
+ // 如果找到了翻译
480
+ if (result !== null) {
481
+ this.log('翻译成功:', { key, result });
482
+ // 记录成功翻译的键到已使用键缓存
483
+ if (!this.usedKeyCache[finalDomainCode]) {
484
+ this.usedKeyCache[finalDomainCode] = new Set();
485
+ this.log('初始化已使用键缓存:', finalDomainCode);
486
+ }
487
+ this.usedKeyCache[finalDomainCode].add(key);
488
+ this.log('添加到已使用键缓存:', { domainCode: finalDomainCode, key, 当前缓存数量: this.usedKeyCache[finalDomainCode].size });
489
+ return result;
490
+ }
491
+ else {
492
+ this.log('翻译未找到,将作为缺失键处理:', key);
493
+ // 调用缺失翻译处理
494
+ const domainKey = `${finalDomainCode}.${key}`;
495
+ this.handleMissing(currentLocale, domainKey);
496
+ // 返回原始键名,但仍需要进行变量替换
497
+ result = this.interpolateVariables(key, params, {
498
+ fallbackToOriginal: true,
499
+ logWarnings: false, // 对原始键不记录警告
500
+ escapeHtml: false,
501
+ });
502
+ this.log('返回原始键名(已替换变量):', result);
503
+ return result;
504
+ }
505
+ }
506
+ catch (error) {
507
+ console.error(`国际化 翻译错误: 键=${key}, 错误=${error}`);
508
+ this.log('翻译发生错误,返回原始键名:', key);
509
+ return key; // 返回键名作为fallback
510
+ }
511
+ }
512
+ /**
513
+ * 处理缺失翻译
514
+ */
515
+ handleMissing(locale, domainKey) {
516
+ this.log('检测到缺失翻译键:', { locale, domainKey });
517
+ // 只分割第一个点号,避免key中包含...等多个点的情况
518
+ const dotIndex = domainKey.indexOf('.');
519
+ const domainCode = dotIndex !== -1 ? domainKey.substring(0, dotIndex) : this.defaultLangDictDomainCode;
520
+ const keyCode = dotIndex !== -1 ? domainKey.substring(dotIndex + 1) : domainKey;
521
+ this.log('解析缺失翻译键:', { domainCode, keyCode });
522
+ // 初始化已上报缓存
523
+ if (!this.reportedKeyCache[domainCode]) {
524
+ this.reportedKeyCache[domainCode] = new Set();
525
+ }
526
+ // 检查是否已经上报过,如果已上报则直接返回,不再添加到缓存
527
+ if (this.reportedKeyCache[domainCode].has(keyCode)) {
528
+ this.log('该键已上报过,跳过:', { domainCode, keyCode });
529
+ return this.defaultLocale;
530
+ }
531
+ // 初始化域代码对应的缓存集合
532
+ if (!this.missingKeyCache[domainCode]) {
533
+ this.missingKeyCache[domainCode] = new Set();
534
+ this.log('初始化缺失键缓存:', domainCode);
535
+ }
536
+ // 检查是否已经缓存过此键,避免重复添加
537
+ if (!this.missingKeyCache[domainCode].has(keyCode)) {
538
+ this.missingKeyCache[domainCode].add(keyCode);
539
+ this.log('添加缺失键到缓存:', { domainCode, keyCode, 当前缓存数量: this.missingKeyCache[domainCode].size });
540
+ }
541
+ else {
542
+ this.log('缺失键已存在于缓存中,跳过:', { domainCode, keyCode });
543
+ }
544
+ // 重置定时器,使用防抖机制
545
+ if (this.reportTimer) {
546
+ clearTimeout(this.reportTimer);
547
+ this.log('重置上报定时器');
548
+ }
549
+ this.reportTimer = setTimeout(() => {
550
+ this.log(`防抖延迟 ${this.reportDelay}ms 后触发批量上报`);
551
+ this.batchReportMissingKeys();
552
+ this.reportTimer = null;
553
+ }, this.reportDelay);
554
+ return this.defaultLocale;
555
+ }
556
+ /**
557
+ * 立即上报所有缓存的缺失翻译键
558
+ * 用于页面卸载或其他需要立即上报的场景
559
+ */
560
+ async flushMissingKeys() {
561
+ if (this.reportTimer) {
562
+ clearTimeout(this.reportTimer);
563
+ this.reportTimer = null;
564
+ }
565
+ return this.batchReportMissingKeys();
566
+ }
567
+ /**
568
+ * 获取当前缓存的翻译键统计信息
569
+ */
570
+ getTranslationKeysStats() {
571
+ const missingStats = {};
572
+ const usedStats = {};
573
+ const reportedStats = {};
574
+ for (const [domainCode, keySet] of Object.entries(this.missingKeyCache)) {
575
+ missingStats[domainCode] = keySet.size;
576
+ }
577
+ for (const [domainCode, keySet] of Object.entries(this.usedKeyCache)) {
578
+ usedStats[domainCode] = keySet.size;
579
+ }
580
+ for (const [domainCode, keySet] of Object.entries(this.reportedKeyCache)) {
581
+ reportedStats[domainCode] = keySet.size;
582
+ }
583
+ return {
584
+ missingKeys: missingStats,
585
+ usedKeys: usedStats,
586
+ reportedKeys: reportedStats,
587
+ };
588
+ }
589
+ /**
590
+ * 清空已上报键缓存(可选,用于需要重新上报的场景)
591
+ */
592
+ clearReportedKeysCache() {
593
+ this.reportedKeyCache = {};
594
+ this.reportedKeysOrder.clear();
595
+ this.log('已上报键缓存已清空');
596
+ }
597
+ /**
598
+ * 获取缓存内存使用情况(用于监控和调试)
599
+ */
600
+ getCacheMemoryInfo() {
601
+ const reportedKeysCount = this.getTotalReportedKeysCount();
602
+ const usagePercentage = this.maxReportedKeysCache > 0
603
+ ? Math.round((reportedKeysCount / this.maxReportedKeysCache) * 100)
604
+ : 0;
605
+ let missingKeysCount = 0;
606
+ for (const keySet of Object.values(this.missingKeyCache)) {
607
+ missingKeysCount += keySet.size;
608
+ }
609
+ let usedKeysCount = 0;
610
+ for (const keySet of Object.values(this.usedKeyCache)) {
611
+ usedKeysCount += keySet.size;
612
+ }
613
+ return {
614
+ reportedKeysCount,
615
+ maxReportedKeysCache: this.maxReportedKeysCache,
616
+ usagePercentage,
617
+ missingKeysCount,
618
+ usedKeysCount,
619
+ };
620
+ }
621
+ isVue2I18n() {
622
+ return typeof this.i18n.global === 'undefined';
623
+ }
624
+ /**
625
+ * 更新国际化语言包
626
+ * @param langDictData 接口返回的语言包数据
627
+ * @param saveToCache 是否保存到本地缓存,默认true
628
+ */
629
+ async updateI18nMessages(langDictData, saveToCache = true) {
630
+ try {
631
+ this.log('开始更新国际化语言包, saveToCache:', saveToCache);
632
+ this.log('接收到的语言包数据:', langDictData);
633
+ const { zh_cn: zhHans, zh_hk: zhHant, ...other } = langDictData;
634
+ const correctDictionaryData = {
635
+ 'zh-Hans': zhHans,
636
+ 'zh-Hant': zhHant,
637
+ ...other,
638
+ };
639
+ // 获取 i18n.global 中现有的语言包
640
+ const currentMessages = this.getI18nMessages();
641
+ // 使用 Lodash merge 深度合并
642
+ const allMessage = lodash.merge({}, currentMessages, correctDictionaryData);
643
+ this.log('合并后的语言包:', allMessage);
644
+ let isVue2 = this.isVue2I18n();
645
+ // 更新 i18n 实例的语言包
646
+ Object.keys(allMessage).forEach((langCode) => {
647
+ const item = allMessage[langCode];
648
+ if (isVue2) {
649
+ this.i18n.mergeLocaleMessage(langCode, item);
650
+ }
651
+ else {
652
+ this.i18n.global.setLocaleMessage(langCode, item);
653
+ }
654
+ });
655
+ // 保存语言包到本地缓存
656
+ if (saveToCache) {
657
+ const version = `v${Date.now()}`; // 使用时间戳作为版本号
658
+ this.log(`保存语言包到缓存, version: ${version}`, allMessage);
659
+ this.cacheManager.saveLangDictToCache(allMessage, version);
660
+ }
661
+ // 强制触发响应式更新 - 通过重新设置 locale 来触发所有依赖更新
662
+ const currentLocale = typeof this.i18n.global.locale === 'string'
663
+ ? this.i18n.global.locale
664
+ : this.i18n.global.locale.value;
665
+ if (typeof this.i18n.global.locale === 'string') {
666
+ this.i18n.global.locale = currentLocale;
667
+ setTimeout(() => {
668
+ this.i18n.global.locale = currentLocale;
669
+ }, 0);
670
+ }
671
+ else if ('value' in this.i18n.global.locale) {
672
+ this.i18n.global.locale.value = currentLocale;
673
+ setTimeout(() => {
674
+ this.i18n.global.locale.value = currentLocale;
675
+ }, 0);
676
+ }
677
+ // 发送全局事件通知所有组件语言包已更新
678
+ if (typeof uni !== 'undefined') {
679
+ uni.$emit('i18n:updated', {
680
+ locale: currentLocale,
681
+ messages: allMessage[currentLocale],
682
+ });
683
+ }
684
+ }
685
+ catch (error) {
686
+ console.error('[国际化] 更新国际化语言包失败:', error);
687
+ throw error;
688
+ }
689
+ }
690
+ /**
691
+ * 切换语言
692
+ * @param setLanguageIdentifier 语言标识符
693
+ */
694
+ switchLocale(setLanguageIdentifier) {
695
+ let targetLocale = 'zh-Hans';
696
+ if (setLanguageIdentifier === exports.LANGUAGE_OPTIONS_CONFIGURATION.FOLLOW_SYSTEM) {
697
+ // 跟随系统语言
698
+ targetLocale = uni.getLocale();
699
+ this.log(`跟随系统语言: ${targetLocale}`);
700
+ }
701
+ else {
702
+ targetLocale = setLanguageIdentifier;
703
+ this.log(`切换到指定语言: ${targetLocale}`);
704
+ }
705
+ try {
706
+ // 检查语言包是否存在
707
+ if (!this.hasLocale(targetLocale)) {
708
+ console.warn(`[国际化] 语言包不存在: ${targetLocale}`);
709
+ return;
710
+ }
711
+ // 设置新的语言 - 根据 i18n 配置使用正确的方式
712
+ if (typeof this.i18n.global.locale === 'string') {
713
+ // Legacy 模式
714
+ this.i18n.global.locale = targetLocale;
715
+ this.log('使用 Legacy 模式设置语言');
716
+ }
717
+ else {
718
+ // Composition API 模式
719
+ this.i18n.global.locale.value = targetLocale;
720
+ this.log('使用 Composition API 模式设置语言');
721
+ }
722
+ // 保存新语言到缓存
723
+ this.cacheManager.saveLocaleToCache(setLanguageIdentifier);
724
+ this.log('语言设置已保存到缓存');
725
+ // 调用钩子函数通知语言已切换
726
+ if (this.onLocaleChanged) {
727
+ const messages = this.getI18nMessages();
728
+ this.log('调用 onLocaleChanged 钩子');
729
+ this.onLocaleChanged({
730
+ targetLocale,
731
+ messages: messages[targetLocale],
732
+ });
733
+ }
734
+ this.log(`语言切换完成: ${targetLocale}`);
735
+ }
736
+ catch (error) {
737
+ console.error('[国际化] 语言切换失败:', error);
738
+ throw error;
739
+ }
740
+ }
741
+ /**
742
+ * 获取当前语言 (Vue3 版本)
743
+ */
744
+ getCurrentLocale() {
745
+ if (typeof this.i18n?.global?.locale === 'string') {
746
+ return this.i18n.global.locale;
747
+ }
748
+ else {
749
+ if (typeof this.i18n.global !== 'undefined') {
750
+ return this.i18n?.global.locale?.value;
751
+ }
752
+ else {
753
+ return this.getCurrentLocaleVue2();
754
+ }
755
+ }
756
+ }
757
+ /**
758
+ * 获取当前语言 (Vue2 版本 - vue-i18n@8)
759
+ * Vue2 的 VueI18n 实例直接通过 i18n.locale 访问,而不是 i18n.global.locale
760
+ */
761
+ getCurrentLocaleVue2() {
762
+ // Vue2 i18n@8 的方式:直接访问 this.i18n.locale
763
+ const i18nAny = this.i18n;
764
+ if (i18nAny.locale) {
765
+ return typeof i18nAny.locale === 'string'
766
+ ? i18nAny.locale
767
+ : i18nAny.locale;
768
+ }
769
+ return this.defaultLocale;
770
+ }
771
+ /**
772
+ * 检查语言包是否存在
773
+ * @param locale 语言代码
774
+ */
775
+ hasLocale(locale) {
776
+ const messages = this.getI18nMessages();
777
+ return locale in messages;
778
+ }
779
+ /**
780
+ * 获取当前 i18n 语言标识
781
+ */
782
+ getI18nLang() {
783
+ const currentLocale = typeof this.i18n.global.locale === 'string'
784
+ ? this.i18n.global.locale
785
+ : this.i18n.global.locale.value;
786
+ return currentLocale;
787
+ }
788
+ /**
789
+ * 获取请求头使用的语言标识
790
+ */
791
+ getHeaderLang() {
792
+ const langMap = {
793
+ 'zh-Hans': 'zh_cn',
794
+ 'zh-Hant': 'zh_hk',
795
+ en: 'en',
796
+ };
797
+ const lang = this.getI18nLang();
798
+ const headerLang = langMap[lang] || 'zh_cn';
799
+ return headerLang;
800
+ }
801
+ /**
802
+ * 获取 i18n messages 的兼容性方法
803
+ * 兼容 vue-i18n@8 和 vue-i18n@9+ 版本
804
+ */
805
+ getI18nMessages() {
806
+ try {
807
+ // vue-i18n@8 版本,messages 是普通对象
808
+ if (this.isVue2I18n()) {
809
+ return this.i18n.messages;
810
+ }
811
+ // vue-i18n@9+ 版本,messages 是 Ref 对象
812
+ if (this.i18n.global.messages && typeof this.i18n.global.messages === 'object' && 'value' in this.i18n.global.messages) {
813
+ return this.i18n.global.messages.value;
814
+ }
815
+ // 兜底情况
816
+ this.log('警告: 无法获取 i18n messages,返回空对象');
817
+ return {};
818
+ }
819
+ catch (error) {
820
+ console.error('[国际化] 获取 i18n messages 失败:', error);
821
+ return {};
822
+ }
823
+ }
824
+ }
825
+
826
+ /**
827
+ * 默认配置
828
+ */
829
+ const DEFAULT_CONFIG = {
830
+ /** 默认语言 */
831
+ defaultLocale: 'zh-Hans',
832
+ /** 默认领域代码 */
833
+ defaultLangDictDomainCode: 'TALENTS',
834
+ /** 语言代码映射 */
835
+ localeMap: {
836
+ 'zh-CN': 'zh-Hans',
837
+ 'zh-Hans': 'zh-Hans',
838
+ 'zh-Hant': 'zh-Hant',
839
+ en: 'en',
840
+ },
841
+ /** 缓存键名 */
842
+ localeCacheKey: 'APP_LOCALE_SETTING',
843
+ /** 语言包缓存键名 */
844
+ langDictCacheKey: 'APP_LANG_DICT_CACHE',
845
+ /** 缓存最大有效期(毫秒),默认365天 */
846
+ cacheMaxAge: 365 * 24 * 60 * 60 * 1000,
847
+ /** 上报延迟时间(毫秒),默认5000ms */
848
+ reportDelay: 5000,
849
+ /** 是否启用调试模式,默认false */
850
+ debug: false,
851
+ /** 已上报键缓存最大数量,默认10000,0表示不限制 */
852
+ maxReportedKeysCache: 10000,
853
+ };
854
+ /**
855
+ * 语言代码映射函数
856
+ */
857
+ function getMappedLocale(locale, localeMap) {
858
+ const map = localeMap || DEFAULT_CONFIG.localeMap;
859
+ return map[locale] || DEFAULT_CONFIG.defaultLocale;
860
+ }
861
+
862
+ /**
863
+ * MeetFun I18n 实例
864
+ */
865
+ class MeetFunI18n {
866
+ constructor(baseMessages, apiConfig, userConfig = {}) {
867
+ /**
868
+ * 翻译函数
869
+ */
870
+ this.$t = (key, params) => {
871
+ return this.translateManager.safeTranslate(key, params);
872
+ };
873
+ /**
874
+ * 获取语言包热数据
875
+ */
876
+ this.fetchLangDictHotData = async () => {
877
+ if (!this.config.systemCode) {
878
+ console.warn('[国际化] systemCode 未配置,无法获取热数据');
879
+ return;
880
+ }
881
+ return this.translateManager.fetchLangDictHotData(this.config.systemCode);
882
+ };
883
+ /**
884
+ * 更新国际化语言包
885
+ */
886
+ this.updateI18nMessages = async (langDictData, saveToCache = true) => {
887
+ return this.translateManager.updateI18nMessages(langDictData, saveToCache);
888
+ };
889
+ /**
890
+ * 切换语言
891
+ */
892
+ this.switchLocale = (locale) => {
893
+ return this.translateManager.switchLocale(locale);
894
+ };
895
+ /**
896
+ * 获取当前语言
897
+ */
898
+ this.getCurrentLocale = () => {
899
+ return this.translateManager.getCurrentLocale();
900
+ };
901
+ /**
902
+ * 获取 i18n 语言标识
903
+ */
904
+ this.getI18nLang = () => {
905
+ return this.translateManager.getI18nLang();
906
+ };
907
+ /**
908
+ * 获取请求头语言标识
909
+ */
910
+ this.getHeaderLang = () => {
911
+ return this.translateManager.getHeaderLang();
912
+ };
913
+ /**
914
+ * 立即上报缺失键
915
+ */
916
+ this.flushMissingKeys = () => {
917
+ return this.translateManager.flushMissingKeys();
918
+ };
919
+ /**
920
+ * 获取翻译键统计信息
921
+ */
922
+ this.getTranslationKeysStats = () => {
923
+ return this.translateManager.getTranslationKeysStats();
924
+ };
925
+ /**
926
+ * 检查语言包是否存在
927
+ */
928
+ this.hasLocale = (locale) => {
929
+ return this.translateManager.hasLocale(locale);
930
+ };
931
+ /**
932
+ * 变量替换
933
+ */
934
+ this.interpolateVariables = (template, params, options) => {
935
+ return this.translateManager.interpolateVariables(template, params, options);
936
+ };
937
+ /**
938
+ * 获取缓存的语言设置
939
+ */
940
+ this.getCachedLocale = () => {
941
+ return this.cacheManager.getCachedLocale();
942
+ };
943
+ /**
944
+ * 保存语言设置到缓存
945
+ */
946
+ this.saveLocaleToCache = (locale) => {
947
+ return this.cacheManager.saveLocaleToCache(locale);
948
+ };
949
+ /**
950
+ * 清除语言包缓存
951
+ */
952
+ this.clearLangDictCache = () => {
953
+ return this.cacheManager.clearLangDictCache();
954
+ };
955
+ /**
956
+ * 清空已上报键缓存
957
+ */
958
+ this.clearReportedKeysCache = () => {
959
+ return this.translateManager.clearReportedKeysCache();
960
+ };
961
+ /**
962
+ * 获取缓存内存使用情况(用于监控和调试)
963
+ */
964
+ this.getCacheMemoryInfo = () => {
965
+ return this.translateManager.getCacheMemoryInfo();
966
+ };
967
+ /**
968
+ * 创建国际化代理对象
969
+ * 用于创建一个对象,当访问属性时自动通过翻译函数进行国际化转换
970
+ *
971
+ * @param data 原始数据对象
972
+ * @param config 配置选项
973
+ * @returns 代理对象
974
+ *
975
+ * @example
976
+ * ```typescript
977
+ * const roleNames = i18nInstance.createI18nProxy({
978
+ * PRODUCING_AREA: 'role.PRODUCING_AREA',
979
+ * PRESENTATION_AREA: 'role.PRESENTATION_AREA'
980
+ * })
981
+ *
982
+ * // 访问时会自动调用翻译函数
983
+ * console.log(roleNames.PRODUCING_AREA)
984
+ * console.log(roleNames['PRODUCING_AREA'])
985
+ * ```
986
+ */
987
+ this.createI18nProxy = (data, config = {}) => {
988
+ const proxyConfig = {
989
+ fallbackToKey: true,
990
+ debug: false,
991
+ ...config,
992
+ };
993
+ // 创建 Proxy 来拦截属性访问
994
+ return new Proxy(data, {
995
+ get: (target, prop) => {
996
+ if (typeof prop === 'symbol') {
997
+ return target[prop];
998
+ }
999
+ const originalValue = target[prop];
1000
+ if (originalValue === undefined) {
1001
+ if (proxyConfig.debug) {
1002
+ console.warn(`I18nProxy: 键 "${String(prop)}" 不存在于源数据中`);
1003
+ }
1004
+ return String(prop);
1005
+ }
1006
+ // 使用原始值作为翻译键
1007
+ const translationKey = originalValue;
1008
+ if (proxyConfig.debug) {
1009
+ console.log(`I18nProxy: 翻译键 "${String(prop)}" -> "${translationKey}"`);
1010
+ }
1011
+ try {
1012
+ // 调用翻译函数
1013
+ const translatedValue = this.$t(translationKey);
1014
+ if (proxyConfig.debug) {
1015
+ console.log(`I18nProxy: 翻译结果 "${translationKey}" -> "${translatedValue}"`);
1016
+ }
1017
+ // 如果翻译结果和翻译键相同,说明没有找到翻译
1018
+ if (translatedValue === translationKey && proxyConfig.fallbackToKey) {
1019
+ return originalValue; // 返回原始值
1020
+ }
1021
+ return translatedValue;
1022
+ }
1023
+ catch (error) {
1024
+ if (proxyConfig.debug) {
1025
+ console.error(`I18nProxy: 翻译错误 "${translationKey}":`, error);
1026
+ }
1027
+ return proxyConfig.fallbackToKey ? originalValue : String(prop);
1028
+ }
1029
+ },
1030
+ // 支持 Object.keys() 等操作
1031
+ ownKeys: (target) => {
1032
+ return Reflect.ownKeys(target);
1033
+ },
1034
+ // 支持 hasOwnProperty 检查
1035
+ has: (target, prop) => {
1036
+ return Reflect.has(target, prop);
1037
+ },
1038
+ // 支持 getOwnPropertyDescriptor
1039
+ getOwnPropertyDescriptor: (target, prop) => {
1040
+ return Reflect.getOwnPropertyDescriptor(target, prop);
1041
+ },
1042
+ });
1043
+ };
1044
+ /**
1045
+ * 创建枚举类型的国际化代理
1046
+ * 专门用于处理枚举值到翻译键的映射
1047
+ *
1048
+ * @param enumObject 枚举对象
1049
+ * @param config 额外配置
1050
+ * @returns 代理对象
1051
+ *
1052
+ * @example
1053
+ * ```typescript
1054
+ * const statusEnum = i18nInstance.createEnumI18nProxy({
1055
+ * ACTIVE: 'status.active',
1056
+ * INACTIVE: 'status.inactive'
1057
+ * })
1058
+ * ```
1059
+ */
1060
+ this.createEnumI18nProxy = (enumObject, config) => {
1061
+ return this.createI18nProxy(enumObject, config);
1062
+ };
1063
+ // 合并配置
1064
+ this.config = {
1065
+ ...DEFAULT_CONFIG,
1066
+ ...userConfig,
1067
+ systemCode: userConfig.systemCode || '',
1068
+ };
1069
+ // 初始化缓存管理器
1070
+ this.cacheManager = new CacheManager(this.config.localeCacheKey, this.config.langDictCacheKey, this.config.cacheMaxAge, baseMessages);
1071
+ // 获取初始语言设置
1072
+ const currentLocale = this.getInitialLocale();
1073
+ // 获取合并了缓存的初始语言包
1074
+ const initialMessages = this.cacheManager.getInitialMergedMessages();
1075
+ // 创建 i18n 实例
1076
+ this.i18n = vueI18n.createI18n({
1077
+ locale: currentLocale,
1078
+ messages: initialMessages,
1079
+ allowComposition: true,
1080
+ globalInjection: false,
1081
+ fallbackLocale: this.config.defaultLocale,
1082
+ missingWarn: true,
1083
+ fallbackWarn: true,
1084
+ legacy: false,
1085
+ });
1086
+ // 初始化翻译管理器
1087
+ this.translateManager = new TranslateManager(this.i18n, this.cacheManager, apiConfig, this.config.defaultLocale, this.config.defaultLangDictDomainCode, this.config.reportDelay, this.config.onLocaleChanged, this.config.debug, this.config.maxReportedKeysCache);
1088
+ // 设置 missing handler
1089
+ if ('missing' in this.i18n.global) {
1090
+ this.i18n.global.missing = (locale, key) => {
1091
+ return this.translateManager.handleMissing(locale, key);
1092
+ };
1093
+ }
1094
+ }
1095
+ /**
1096
+ * 获取初始语言设置
1097
+ */
1098
+ getInitialLocale() {
1099
+ const systemLocale = uni.getLocale();
1100
+ const setLanguageIdentifier = this.cacheManager.getCachedLocale();
1101
+ // 优先使用缓存的语言,如果没有缓存则使用系统语言
1102
+ // 如果缓存的是 followSystem,则使用系统语言
1103
+ let currentLocale;
1104
+ if (setLanguageIdentifier === exports.LANGUAGE_OPTIONS_CONFIGURATION.FOLLOW_SYSTEM ||
1105
+ !setLanguageIdentifier) {
1106
+ currentLocale = getMappedLocale(systemLocale, this.config.localeMap) || this.config.defaultLocale;
1107
+ }
1108
+ else {
1109
+ currentLocale =
1110
+ getMappedLocale(setLanguageIdentifier, this.config.localeMap) || this.config.defaultLocale;
1111
+ }
1112
+ return currentLocale;
1113
+ }
1114
+ /**
1115
+ * 获取 i18n 实例
1116
+ */
1117
+ getI18n() {
1118
+ return this.i18n;
1119
+ }
1120
+ }
1121
+ /**
1122
+ * 创建 MeetFun I18n 实例的工厂函数
1123
+ *
1124
+ * @param baseMessages 基础语言包
1125
+ * @param apiConfig API 配置
1126
+ * @param userConfig 用户配置
1127
+ * @returns MeetFunI18n 实例
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * import { createMeetFunI18n } from '@meetfun/i18n'
1132
+ * import zhHans from './locales/zh-Hans.json'
1133
+ * import zhHant from './locales/zh-Hant.json'
1134
+ * import en from './locales/en.json'
1135
+ * import { queryLangDictHotData, queryLangDictColdData } from './api/translate'
1136
+ *
1137
+ * const baseMessages = {
1138
+ * 'zh-Hans': zhHans,
1139
+ * 'zh-Hant': zhHant,
1140
+ * en: en,
1141
+ * }
1142
+ *
1143
+ * const apiConfig = {
1144
+ * queryLangDictHotData,
1145
+ * queryLangDictColdData,
1146
+ * }
1147
+ *
1148
+ * const i18nInstance = createMeetFunI18n(baseMessages, apiConfig, {
1149
+ * defaultLocale: 'zh-Hans',
1150
+ * defaultLangDictDomainCode: 'TALENTS',
1151
+ * systemCode: 'TALENTS_STUDENT',
1152
+ * debug: true, // 启用调试模式,会输出详细日志
1153
+ * onLocaleChanged: (payload) => {
1154
+ * console.log('语言已切换:', payload.targetLocale)
1155
+ * // 可以在这里执行其他需要的操作
1156
+ * }
1157
+ * })
1158
+ *
1159
+ * // 在 main.ts 中安装
1160
+ * app.use(i18nInstance.getI18n())
1161
+ *
1162
+ * // 使用翻译函数
1163
+ * const text = i18nInstance.$t('welcomeMessage')
1164
+ * ```
1165
+ */
1166
+ function createMeetFunI18n(baseMessages, apiConfig, userConfig) {
1167
+ return new MeetFunI18n(baseMessages, apiConfig, userConfig);
1168
+ }
1169
+
1170
+ exports.MeetFunI18n = MeetFunI18n;
1171
+ exports.createMeetFunI18n = createMeetFunI18n;
1172
+ exports.default = createMeetFunI18n;
1173
+ //# sourceMappingURL=index.js.map