@wsxjs/wsx-i18next 0.0.22 → 0.0.24
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/decorator.d.ts.map +1 -1
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.cjs +14 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/decorator.ts +20 -2
- package/src/i18n.ts +3 -9
package/dist/decorator.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,SAAS,GAAE,MAAiB,IAErC,CAAC,SAAS;IAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;CAAE,EAAE,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"decorator.d.ts","sourceRoot":"","sources":["../src/decorator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,SAAS,GAAE,MAAiB,IAErC,CAAC,SAAS;IAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,CAAA;CAAE,EAAE,aAAa,CAAC,KAoH7C,CAAC,CAE/B"}
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,UAAe,GAAG,OAAO,OAAO,
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,UAAe,GAAG,OAAO,OAAO,CA4BhE;AAGD,eAAO,MAAM,IAAI,EAAE,OAAO,OAAiB,CAAC"}
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,6 @@ const i18next = require("i18next");
|
|
|
4
4
|
const LanguageDetector = require("i18next-browser-languagedetector");
|
|
5
5
|
const Backend = require("i18next-http-backend");
|
|
6
6
|
function initI18n(config = {}) {
|
|
7
|
-
const savedLanguage = typeof window !== "undefined" ? localStorage.getItem("wsx-language") : null;
|
|
8
7
|
const finalConfig = {
|
|
9
8
|
fallbackLng: "en",
|
|
10
9
|
debug: false,
|
|
@@ -16,10 +15,9 @@ function initI18n(config = {}) {
|
|
|
16
15
|
},
|
|
17
16
|
ns: ["common", "home", "docs", "examples"],
|
|
18
17
|
defaultNS: "common",
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
// 配置 LanguageDetector 的检测顺序(仅在没有显式 lng 时生效)
|
|
18
|
+
// 配置 LanguageDetector 的检测顺序
|
|
19
|
+
// LanguageDetector 会自动从 localStorage 读取(使用 lookupLocalStorage 指定的键)
|
|
20
|
+
// 然后回退到浏览器语言检测
|
|
23
21
|
detection: {
|
|
24
22
|
order: ["localStorage", "navigator"],
|
|
25
23
|
caches: ["localStorage"],
|
|
@@ -52,8 +50,14 @@ function i18nDecorator(namespace = "common") {
|
|
|
52
50
|
var _a;
|
|
53
51
|
(_a = super.onConnected) == null ? void 0 : _a.call(this);
|
|
54
52
|
const handler = () => {
|
|
53
|
+
if (this._i18nRerenderTimer !== void 0) {
|
|
54
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
55
|
+
}
|
|
55
56
|
if (this.rerender) {
|
|
56
|
-
this.
|
|
57
|
+
this._i18nRerenderTimer = requestAnimationFrame(() => {
|
|
58
|
+
this.rerender();
|
|
59
|
+
this._i18nRerenderTimer = void 0;
|
|
60
|
+
});
|
|
57
61
|
}
|
|
58
62
|
};
|
|
59
63
|
this._languageChangedHandler = handler;
|
|
@@ -71,6 +75,10 @@ function i18nDecorator(namespace = "common") {
|
|
|
71
75
|
// 生命周期:组件断开时取消订阅
|
|
72
76
|
onDisconnected() {
|
|
73
77
|
var _a;
|
|
78
|
+
if (this._i18nRerenderTimer !== void 0) {
|
|
79
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
80
|
+
this._i18nRerenderTimer = void 0;
|
|
81
|
+
}
|
|
74
82
|
if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === "function") {
|
|
75
83
|
try {
|
|
76
84
|
this._i18nUnsubscribe();
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/i18n.ts","../src/decorator.ts","../src/hooks.ts","../src/mixin.ts"],"sourcesContent":["/**\n * i18next 配置和初始化\n */\n\nimport i18next from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport Backend from \"i18next-http-backend\";\nimport type { I18nConfig } from \"./types\";\n\n/**\n * 初始化 i18next\n * @param config 配置选项\n * @returns i18n 实例\n */\nexport function initI18n(config: I18nConfig = {}): typeof i18next {\n // 关键修复:手动从 localStorage 读取语言,优先级高于浏览器检测\n // 这确保了即使 LanguageDetector 失败,也能正确加载保存的语言\n const savedLanguage =\n typeof window !== \"undefined\" ? localStorage.getItem(\"wsx-language\") : null;\n\n // 合并配置\n const finalConfig = {\n fallbackLng: \"en\",\n debug: false,\n interpolation: {\n escapeValue: false,\n },\n backend: {\n loadPath: \"/locales/{{lng}}/{{ns}}.json\",\n },\n ns: [\"common\", \"home\", \"docs\", \"examples\"],\n defaultNS: \"common\",\n // 关键修复:如果有 savedLanguage,强制使用它(优先级最高)\n // 如果没有,让 LanguageDetector 决定(会先检查 localStorage,再检查 navigator)\n ...(savedLanguage ? { lng: savedLanguage } : {}),\n // 配置 LanguageDetector 的检测顺序(仅在没有显式 lng 时生效)\n detection: {\n order: [\"localStorage\", \"navigator\"],\n caches: [\"localStorage\"],\n lookupLocalStorage: \"wsx-language\",\n },\n ...config,\n };\n\n // 初始化 i18next\n i18next.use(Backend).use(LanguageDetector).init(finalConfig);\n\n return i18next;\n}\n\n// 导出 i18n 实例(直接导出常量)\nexport const i18n: typeof i18next = i18next;\n","/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n */\n\nimport { i18n } from \"./i18n\";\n\n/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n *\n * 使用方式:\n * ```tsx\n * @i18n('common')\n * export class MyComponent extends WebComponent {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 类装饰器\n */\nexport function i18nDecorator(namespace: string = \"common\") {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return function <T extends { new (...args: any[]): any }>(constructor: T) {\n class I18nEnhanced extends constructor {\n // 使用 public 而不是 private,因为导出的匿名类类型限制\n public _i18nNamespace!: string;\n public _i18nUnsubscribe?: () => void;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n constructor(...args: any[]) {\n super(...args);\n // 初始化命名空间\n this._i18nNamespace = namespace;\n }\n\n // 注入 t 方法\n public t(key: string, options?: object): string {\n return i18n.t(key, { ns: this._i18nNamespace, ...options });\n }\n\n // 注入 i18n 实例\n public get i18n() {\n return i18n;\n }\n\n // 生命周期:组件连接时订阅语言变化\n public onConnected(): void {\n // 先调用父类的 onConnected(如果存在)\n super.onConnected?.();\n\n // 创建回调函数并保存引用,以便后续取消订阅\n const handler = (() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n }) as () => void;\n\n // 保存回调引用以便取消订阅\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any)._languageChangedHandler = handler;\n\n // 订阅语言变化事件\n const unsubscribe = i18n.on(\"languageChanged\", handler);\n\n // 如果返回的是函数,直接使用;否则使用 off 方法\n if (typeof unsubscribe === \"function\") {\n this._i18nUnsubscribe = unsubscribe;\n } else {\n // 如果 i18n.on 返回的不是函数,创建一个取消订阅函数\n // 使用 off 方法(如果可用)或空函数\n this._i18nUnsubscribe = () => {\n if (typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n };\n }\n }\n\n // 生命周期:组件断开时取消订阅\n public onDisconnected(): void {\n // 取消 i18n 订阅\n if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === \"function\") {\n try {\n this._i18nUnsubscribe();\n } catch {\n // 如果 unsubscribe 调用失败,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n this._i18nUnsubscribe = undefined;\n } else {\n // 如果 unsubscribe 不是函数,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n // 清理回调引用\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any)._languageChangedHandler) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete (this as any)._languageChangedHandler;\n }\n\n // 调用父类的 onDisconnected(如果存在)\n super.onDisconnected?.();\n }\n }\n // 复制静态属性和方法\n Object.setPrototypeOf(I18nEnhanced, constructor);\n Object.defineProperty(I18nEnhanced, \"name\", {\n value: constructor.name,\n writable: false,\n });\n return I18nEnhanced as T;\n };\n}\n","/**\n * useTranslation 函数(API 与 react-i18next 兼容)\n */\n\nimport { i18n } from \"./i18n\";\nimport type { UseTranslationResponse } from \"./types\";\n\n/**\n * useTranslation - API 与 react-i18next 兼容的翻译函数\n *\n * **重要说明**:\n * - 这不是 React hook,而是 WSXJS 的普通函数\n * - API 设计参考 react-i18next,但实现方式完全不同\n * - 在 WSXJS 中,需要配合 @state 或 @i18n 装饰器实现响应式\n * - 不会自动响应语言变化,需要手动订阅 languageChanged 事件\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 翻译对象\n */\nexport function useTranslation(namespace: string = \"common\"): UseTranslationResponse {\n // 创建一个包装函数,保持 API 兼容性\n const t = (key: string, options?: object): string => {\n // 每次调用 t() 时,i18n.t() 会使用当前的 i18n.language\n // 所以只要组件重渲染,就会得到新的翻译\n return i18n.t(key, { ns: namespace, ...options });\n };\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: t as any,\n i18n,\n ready: i18n.isInitialized,\n };\n}\n","/**\n * Mixin API - 为基类添加 i18n 支持\n */\n\nimport { i18n } from \"./i18n\";\nimport type { WebComponent, LightComponent } from \"@wsxjs/wsx-core\";\n\n/**\n * 为任何继承自 WebComponent 或 LightComponent 的类添加 i18n 支持\n *\n * 使用方式:\n * ```tsx\n * export class MyComponent extends withI18n(WebComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n *\n * export class MyLightComponent extends withI18n(LightComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param Base 基类(WebComponent 或 LightComponent)\n * @param defaultNamespace 默认命名空间\n * @returns 增强后的类\n */\nexport function withI18n<T extends typeof WebComponent | typeof LightComponent>(\n Base: T,\n defaultNamespace: string = \"common\"\n): T {\n return class extends Base {\n protected t(key: string, namespace?: string, options?: object): string {\n return i18n.t(key, { ns: namespace || defaultNamespace, ...options });\n }\n\n protected get i18n() {\n return i18n;\n }\n\n protected onConnected(): void {\n // 订阅语言变化事件,自动触发重渲染\n i18n.on(\"languageChanged\", () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n });\n }\n } as T;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/i18n.ts","../src/decorator.ts","../src/hooks.ts","../src/mixin.ts"],"sourcesContent":["/**\n * i18next 配置和初始化\n */\n\nimport i18next from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport Backend from \"i18next-http-backend\";\nimport type { I18nConfig } from \"./types\";\n\n/**\n * 初始化 i18next\n * @param config 配置选项\n * @returns i18n 实例\n */\nexport function initI18n(config: I18nConfig = {}): typeof i18next {\n // 合并配置\n const finalConfig = {\n fallbackLng: \"en\",\n debug: false,\n interpolation: {\n escapeValue: false,\n },\n backend: {\n loadPath: \"/locales/{{lng}}/{{ns}}.json\",\n },\n ns: [\"common\", \"home\", \"docs\", \"examples\"],\n defaultNS: \"common\",\n // 配置 LanguageDetector 的检测顺序\n // LanguageDetector 会自动从 localStorage 读取(使用 lookupLocalStorage 指定的键)\n // 然后回退到浏览器语言检测\n detection: {\n order: [\"localStorage\", \"navigator\"],\n caches: [\"localStorage\"],\n lookupLocalStorage: \"wsx-language\",\n },\n ...config,\n };\n\n // 初始化 i18next\n i18next.use(Backend).use(LanguageDetector).init(finalConfig);\n\n return i18next;\n}\n\n// 导出 i18n 实例(直接导出常量)\nexport const i18n: typeof i18next = i18next;\n","/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n */\n\nimport { i18n } from \"./i18n\";\n\n/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n *\n * 使用方式:\n * ```tsx\n * @i18n('common')\n * export class MyComponent extends WebComponent {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 类装饰器\n */\nexport function i18nDecorator(namespace: string = \"common\") {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return function <T extends { new (...args: any[]): any }>(constructor: T) {\n class I18nEnhanced extends constructor {\n // 使用 public 而不是 private,因为导出的匿名类类型限制\n public _i18nNamespace!: string;\n public _i18nUnsubscribe?: () => void;\n // 防抖定时器,避免多次 rerender 调用\n private _i18nRerenderTimer?: number;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n constructor(...args: any[]) {\n super(...args);\n // 初始化命名空间\n this._i18nNamespace = namespace;\n }\n\n // 注入 t 方法\n public t(key: string, options?: object): string {\n return i18n.t(key, { ns: this._i18nNamespace, ...options });\n }\n\n // 注入 i18n 实例\n public get i18n() {\n return i18n;\n }\n\n // 生命周期:组件连接时订阅语言变化\n public onConnected(): void {\n // 先调用父类的 onConnected(如果存在)\n super.onConnected?.();\n\n // 创建回调函数并保存引用,以便后续取消订阅\n const handler = (() => {\n // 关键修复 (RFC-0042):防抖 rerender 调用,避免多次更新导致文本节点更新丢失\n // i18next 的 languageChanged 事件可能在 changeLanguage 完成之前被多次触发\n // 使用 requestAnimationFrame 确保只在下一个渲染帧触发一次 rerender\n if (this._i18nRerenderTimer !== undefined) {\n cancelAnimationFrame(this._i18nRerenderTimer);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n this._i18nRerenderTimer = requestAnimationFrame(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n this._i18nRerenderTimer = undefined;\n });\n }\n }) as () => void;\n\n // 保存回调引用以便取消订阅\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any)._languageChangedHandler = handler;\n\n // 订阅语言变化事件\n const unsubscribe = i18n.on(\"languageChanged\", handler);\n\n // 如果返回的是函数,直接使用;否则使用 off 方法\n if (typeof unsubscribe === \"function\") {\n this._i18nUnsubscribe = unsubscribe;\n } else {\n // 如果 i18n.on 返回的不是函数,创建一个取消订阅函数\n // 使用 off 方法(如果可用)或空函数\n this._i18nUnsubscribe = () => {\n if (typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n };\n }\n }\n\n // 生命周期:组件断开时取消订阅\n public onDisconnected(): void {\n // 取消防抖定时器\n if (this._i18nRerenderTimer !== undefined) {\n cancelAnimationFrame(this._i18nRerenderTimer);\n this._i18nRerenderTimer = undefined;\n }\n\n // 取消 i18n 订阅\n if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === \"function\") {\n try {\n this._i18nUnsubscribe();\n } catch {\n // 如果 unsubscribe 调用失败,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n this._i18nUnsubscribe = undefined;\n } else {\n // 如果 unsubscribe 不是函数,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n // 清理回调引用\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any)._languageChangedHandler) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete (this as any)._languageChangedHandler;\n }\n\n // 调用父类的 onDisconnected(如果存在)\n super.onDisconnected?.();\n }\n }\n // 复制静态属性和方法\n Object.setPrototypeOf(I18nEnhanced, constructor);\n Object.defineProperty(I18nEnhanced, \"name\", {\n value: constructor.name,\n writable: false,\n });\n return I18nEnhanced as T;\n };\n}\n","/**\n * useTranslation 函数(API 与 react-i18next 兼容)\n */\n\nimport { i18n } from \"./i18n\";\nimport type { UseTranslationResponse } from \"./types\";\n\n/**\n * useTranslation - API 与 react-i18next 兼容的翻译函数\n *\n * **重要说明**:\n * - 这不是 React hook,而是 WSXJS 的普通函数\n * - API 设计参考 react-i18next,但实现方式完全不同\n * - 在 WSXJS 中,需要配合 @state 或 @i18n 装饰器实现响应式\n * - 不会自动响应语言变化,需要手动订阅 languageChanged 事件\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 翻译对象\n */\nexport function useTranslation(namespace: string = \"common\"): UseTranslationResponse {\n // 创建一个包装函数,保持 API 兼容性\n const t = (key: string, options?: object): string => {\n // 每次调用 t() 时,i18n.t() 会使用当前的 i18n.language\n // 所以只要组件重渲染,就会得到新的翻译\n return i18n.t(key, { ns: namespace, ...options });\n };\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: t as any,\n i18n,\n ready: i18n.isInitialized,\n };\n}\n","/**\n * Mixin API - 为基类添加 i18n 支持\n */\n\nimport { i18n } from \"./i18n\";\nimport type { WebComponent, LightComponent } from \"@wsxjs/wsx-core\";\n\n/**\n * 为任何继承自 WebComponent 或 LightComponent 的类添加 i18n 支持\n *\n * 使用方式:\n * ```tsx\n * export class MyComponent extends withI18n(WebComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n *\n * export class MyLightComponent extends withI18n(LightComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param Base 基类(WebComponent 或 LightComponent)\n * @param defaultNamespace 默认命名空间\n * @returns 增强后的类\n */\nexport function withI18n<T extends typeof WebComponent | typeof LightComponent>(\n Base: T,\n defaultNamespace: string = \"common\"\n): T {\n return class extends Base {\n protected t(key: string, namespace?: string, options?: object): string {\n return i18n.t(key, { ns: namespace || defaultNamespace, ...options });\n }\n\n protected get i18n() {\n return i18n;\n }\n\n protected onConnected(): void {\n // 订阅语言变化事件,自动触发重渲染\n i18n.on(\"languageChanged\", () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n });\n }\n } as T;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import i18next from "i18next";
|
|
|
2
2
|
import LanguageDetector from "i18next-browser-languagedetector";
|
|
3
3
|
import Backend from "i18next-http-backend";
|
|
4
4
|
function initI18n(config = {}) {
|
|
5
|
-
const savedLanguage = typeof window !== "undefined" ? localStorage.getItem("wsx-language") : null;
|
|
6
5
|
const finalConfig = {
|
|
7
6
|
fallbackLng: "en",
|
|
8
7
|
debug: false,
|
|
@@ -14,10 +13,9 @@ function initI18n(config = {}) {
|
|
|
14
13
|
},
|
|
15
14
|
ns: ["common", "home", "docs", "examples"],
|
|
16
15
|
defaultNS: "common",
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
// 配置 LanguageDetector 的检测顺序(仅在没有显式 lng 时生效)
|
|
16
|
+
// 配置 LanguageDetector 的检测顺序
|
|
17
|
+
// LanguageDetector 会自动从 localStorage 读取(使用 lookupLocalStorage 指定的键)
|
|
18
|
+
// 然后回退到浏览器语言检测
|
|
21
19
|
detection: {
|
|
22
20
|
order: ["localStorage", "navigator"],
|
|
23
21
|
caches: ["localStorage"],
|
|
@@ -50,8 +48,14 @@ function i18nDecorator(namespace = "common") {
|
|
|
50
48
|
var _a;
|
|
51
49
|
(_a = super.onConnected) == null ? void 0 : _a.call(this);
|
|
52
50
|
const handler = () => {
|
|
51
|
+
if (this._i18nRerenderTimer !== void 0) {
|
|
52
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
53
|
+
}
|
|
53
54
|
if (this.rerender) {
|
|
54
|
-
this.
|
|
55
|
+
this._i18nRerenderTimer = requestAnimationFrame(() => {
|
|
56
|
+
this.rerender();
|
|
57
|
+
this._i18nRerenderTimer = void 0;
|
|
58
|
+
});
|
|
55
59
|
}
|
|
56
60
|
};
|
|
57
61
|
this._languageChangedHandler = handler;
|
|
@@ -69,6 +73,10 @@ function i18nDecorator(namespace = "common") {
|
|
|
69
73
|
// 生命周期:组件断开时取消订阅
|
|
70
74
|
onDisconnected() {
|
|
71
75
|
var _a;
|
|
76
|
+
if (this._i18nRerenderTimer !== void 0) {
|
|
77
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
78
|
+
this._i18nRerenderTimer = void 0;
|
|
79
|
+
}
|
|
72
80
|
if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === "function") {
|
|
73
81
|
try {
|
|
74
82
|
this._i18nUnsubscribe();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/i18n.ts","../src/decorator.ts","../src/hooks.ts","../src/mixin.ts"],"sourcesContent":["/**\n * i18next 配置和初始化\n */\n\nimport i18next from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport Backend from \"i18next-http-backend\";\nimport type { I18nConfig } from \"./types\";\n\n/**\n * 初始化 i18next\n * @param config 配置选项\n * @returns i18n 实例\n */\nexport function initI18n(config: I18nConfig = {}): typeof i18next {\n // 关键修复:手动从 localStorage 读取语言,优先级高于浏览器检测\n // 这确保了即使 LanguageDetector 失败,也能正确加载保存的语言\n const savedLanguage =\n typeof window !== \"undefined\" ? localStorage.getItem(\"wsx-language\") : null;\n\n // 合并配置\n const finalConfig = {\n fallbackLng: \"en\",\n debug: false,\n interpolation: {\n escapeValue: false,\n },\n backend: {\n loadPath: \"/locales/{{lng}}/{{ns}}.json\",\n },\n ns: [\"common\", \"home\", \"docs\", \"examples\"],\n defaultNS: \"common\",\n // 关键修复:如果有 savedLanguage,强制使用它(优先级最高)\n // 如果没有,让 LanguageDetector 决定(会先检查 localStorage,再检查 navigator)\n ...(savedLanguage ? { lng: savedLanguage } : {}),\n // 配置 LanguageDetector 的检测顺序(仅在没有显式 lng 时生效)\n detection: {\n order: [\"localStorage\", \"navigator\"],\n caches: [\"localStorage\"],\n lookupLocalStorage: \"wsx-language\",\n },\n ...config,\n };\n\n // 初始化 i18next\n i18next.use(Backend).use(LanguageDetector).init(finalConfig);\n\n return i18next;\n}\n\n// 导出 i18n 实例(直接导出常量)\nexport const i18n: typeof i18next = i18next;\n","/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n */\n\nimport { i18n } from \"./i18n\";\n\n/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n *\n * 使用方式:\n * ```tsx\n * @i18n('common')\n * export class MyComponent extends WebComponent {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 类装饰器\n */\nexport function i18nDecorator(namespace: string = \"common\") {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return function <T extends { new (...args: any[]): any }>(constructor: T) {\n class I18nEnhanced extends constructor {\n // 使用 public 而不是 private,因为导出的匿名类类型限制\n public _i18nNamespace!: string;\n public _i18nUnsubscribe?: () => void;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n constructor(...args: any[]) {\n super(...args);\n // 初始化命名空间\n this._i18nNamespace = namespace;\n }\n\n // 注入 t 方法\n public t(key: string, options?: object): string {\n return i18n.t(key, { ns: this._i18nNamespace, ...options });\n }\n\n // 注入 i18n 实例\n public get i18n() {\n return i18n;\n }\n\n // 生命周期:组件连接时订阅语言变化\n public onConnected(): void {\n // 先调用父类的 onConnected(如果存在)\n super.onConnected?.();\n\n // 创建回调函数并保存引用,以便后续取消订阅\n const handler = (() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n }) as () => void;\n\n // 保存回调引用以便取消订阅\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any)._languageChangedHandler = handler;\n\n // 订阅语言变化事件\n const unsubscribe = i18n.on(\"languageChanged\", handler);\n\n // 如果返回的是函数,直接使用;否则使用 off 方法\n if (typeof unsubscribe === \"function\") {\n this._i18nUnsubscribe = unsubscribe;\n } else {\n // 如果 i18n.on 返回的不是函数,创建一个取消订阅函数\n // 使用 off 方法(如果可用)或空函数\n this._i18nUnsubscribe = () => {\n if (typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n };\n }\n }\n\n // 生命周期:组件断开时取消订阅\n public onDisconnected(): void {\n // 取消 i18n 订阅\n if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === \"function\") {\n try {\n this._i18nUnsubscribe();\n } catch {\n // 如果 unsubscribe 调用失败,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n this._i18nUnsubscribe = undefined;\n } else {\n // 如果 unsubscribe 不是函数,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n // 清理回调引用\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any)._languageChangedHandler) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete (this as any)._languageChangedHandler;\n }\n\n // 调用父类的 onDisconnected(如果存在)\n super.onDisconnected?.();\n }\n }\n // 复制静态属性和方法\n Object.setPrototypeOf(I18nEnhanced, constructor);\n Object.defineProperty(I18nEnhanced, \"name\", {\n value: constructor.name,\n writable: false,\n });\n return I18nEnhanced as T;\n };\n}\n","/**\n * useTranslation 函数(API 与 react-i18next 兼容)\n */\n\nimport { i18n } from \"./i18n\";\nimport type { UseTranslationResponse } from \"./types\";\n\n/**\n * useTranslation - API 与 react-i18next 兼容的翻译函数\n *\n * **重要说明**:\n * - 这不是 React hook,而是 WSXJS 的普通函数\n * - API 设计参考 react-i18next,但实现方式完全不同\n * - 在 WSXJS 中,需要配合 @state 或 @i18n 装饰器实现响应式\n * - 不会自动响应语言变化,需要手动订阅 languageChanged 事件\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 翻译对象\n */\nexport function useTranslation(namespace: string = \"common\"): UseTranslationResponse {\n // 创建一个包装函数,保持 API 兼容性\n const t = (key: string, options?: object): string => {\n // 每次调用 t() 时,i18n.t() 会使用当前的 i18n.language\n // 所以只要组件重渲染,就会得到新的翻译\n return i18n.t(key, { ns: namespace, ...options });\n };\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: t as any,\n i18n,\n ready: i18n.isInitialized,\n };\n}\n","/**\n * Mixin API - 为基类添加 i18n 支持\n */\n\nimport { i18n } from \"./i18n\";\nimport type { WebComponent, LightComponent } from \"@wsxjs/wsx-core\";\n\n/**\n * 为任何继承自 WebComponent 或 LightComponent 的类添加 i18n 支持\n *\n * 使用方式:\n * ```tsx\n * export class MyComponent extends withI18n(WebComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n *\n * export class MyLightComponent extends withI18n(LightComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param Base 基类(WebComponent 或 LightComponent)\n * @param defaultNamespace 默认命名空间\n * @returns 增强后的类\n */\nexport function withI18n<T extends typeof WebComponent | typeof LightComponent>(\n Base: T,\n defaultNamespace: string = \"common\"\n): T {\n return class extends Base {\n protected t(key: string, namespace?: string, options?: object): string {\n return i18n.t(key, { ns: namespace || defaultNamespace, ...options });\n }\n\n protected get i18n() {\n return i18n;\n }\n\n protected onConnected(): void {\n // 订阅语言变化事件,自动触发重渲染\n i18n.on(\"languageChanged\", () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n });\n }\n } as T;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/i18n.ts","../src/decorator.ts","../src/hooks.ts","../src/mixin.ts"],"sourcesContent":["/**\n * i18next 配置和初始化\n */\n\nimport i18next from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport Backend from \"i18next-http-backend\";\nimport type { I18nConfig } from \"./types\";\n\n/**\n * 初始化 i18next\n * @param config 配置选项\n * @returns i18n 实例\n */\nexport function initI18n(config: I18nConfig = {}): typeof i18next {\n // 合并配置\n const finalConfig = {\n fallbackLng: \"en\",\n debug: false,\n interpolation: {\n escapeValue: false,\n },\n backend: {\n loadPath: \"/locales/{{lng}}/{{ns}}.json\",\n },\n ns: [\"common\", \"home\", \"docs\", \"examples\"],\n defaultNS: \"common\",\n // 配置 LanguageDetector 的检测顺序\n // LanguageDetector 会自动从 localStorage 读取(使用 lookupLocalStorage 指定的键)\n // 然后回退到浏览器语言检测\n detection: {\n order: [\"localStorage\", \"navigator\"],\n caches: [\"localStorage\"],\n lookupLocalStorage: \"wsx-language\",\n },\n ...config,\n };\n\n // 初始化 i18next\n i18next.use(Backend).use(LanguageDetector).init(finalConfig);\n\n return i18next;\n}\n\n// 导出 i18n 实例(直接导出常量)\nexport const i18n: typeof i18next = i18next;\n","/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n */\n\nimport { i18n } from \"./i18n\";\n\n/**\n * @i18n 装饰器 - 自动为组件注入翻译功能\n *\n * 使用方式:\n * ```tsx\n * @i18n('common')\n * export class MyComponent extends WebComponent {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 类装饰器\n */\nexport function i18nDecorator(namespace: string = \"common\") {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return function <T extends { new (...args: any[]): any }>(constructor: T) {\n class I18nEnhanced extends constructor {\n // 使用 public 而不是 private,因为导出的匿名类类型限制\n public _i18nNamespace!: string;\n public _i18nUnsubscribe?: () => void;\n // 防抖定时器,避免多次 rerender 调用\n private _i18nRerenderTimer?: number;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n constructor(...args: any[]) {\n super(...args);\n // 初始化命名空间\n this._i18nNamespace = namespace;\n }\n\n // 注入 t 方法\n public t(key: string, options?: object): string {\n return i18n.t(key, { ns: this._i18nNamespace, ...options });\n }\n\n // 注入 i18n 实例\n public get i18n() {\n return i18n;\n }\n\n // 生命周期:组件连接时订阅语言变化\n public onConnected(): void {\n // 先调用父类的 onConnected(如果存在)\n super.onConnected?.();\n\n // 创建回调函数并保存引用,以便后续取消订阅\n const handler = (() => {\n // 关键修复 (RFC-0042):防抖 rerender 调用,避免多次更新导致文本节点更新丢失\n // i18next 的 languageChanged 事件可能在 changeLanguage 完成之前被多次触发\n // 使用 requestAnimationFrame 确保只在下一个渲染帧触发一次 rerender\n if (this._i18nRerenderTimer !== undefined) {\n cancelAnimationFrame(this._i18nRerenderTimer);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n this._i18nRerenderTimer = requestAnimationFrame(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n this._i18nRerenderTimer = undefined;\n });\n }\n }) as () => void;\n\n // 保存回调引用以便取消订阅\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any)._languageChangedHandler = handler;\n\n // 订阅语言变化事件\n const unsubscribe = i18n.on(\"languageChanged\", handler);\n\n // 如果返回的是函数,直接使用;否则使用 off 方法\n if (typeof unsubscribe === \"function\") {\n this._i18nUnsubscribe = unsubscribe;\n } else {\n // 如果 i18n.on 返回的不是函数,创建一个取消订阅函数\n // 使用 off 方法(如果可用)或空函数\n this._i18nUnsubscribe = () => {\n if (typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n };\n }\n }\n\n // 生命周期:组件断开时取消订阅\n public onDisconnected(): void {\n // 取消防抖定时器\n if (this._i18nRerenderTimer !== undefined) {\n cancelAnimationFrame(this._i18nRerenderTimer);\n this._i18nRerenderTimer = undefined;\n }\n\n // 取消 i18n 订阅\n if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === \"function\") {\n try {\n this._i18nUnsubscribe();\n } catch {\n // 如果 unsubscribe 调用失败,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n this._i18nUnsubscribe = undefined;\n } else {\n // 如果 unsubscribe 不是函数,尝试使用 off 方法(如果可用)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const handler = (this as any)._languageChangedHandler;\n if (handler && typeof i18n.off === \"function\") {\n i18n.off(\"languageChanged\", handler);\n }\n }\n // 清理回调引用\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any)._languageChangedHandler) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n delete (this as any)._languageChangedHandler;\n }\n\n // 调用父类的 onDisconnected(如果存在)\n super.onDisconnected?.();\n }\n }\n // 复制静态属性和方法\n Object.setPrototypeOf(I18nEnhanced, constructor);\n Object.defineProperty(I18nEnhanced, \"name\", {\n value: constructor.name,\n writable: false,\n });\n return I18nEnhanced as T;\n };\n}\n","/**\n * useTranslation 函数(API 与 react-i18next 兼容)\n */\n\nimport { i18n } from \"./i18n\";\nimport type { UseTranslationResponse } from \"./types\";\n\n/**\n * useTranslation - API 与 react-i18next 兼容的翻译函数\n *\n * **重要说明**:\n * - 这不是 React hook,而是 WSXJS 的普通函数\n * - API 设计参考 react-i18next,但实现方式完全不同\n * - 在 WSXJS 中,需要配合 @state 或 @i18n 装饰器实现响应式\n * - 不会自动响应语言变化,需要手动订阅 languageChanged 事件\n *\n * @param namespace 命名空间,默认为 'common'\n * @returns 翻译对象\n */\nexport function useTranslation(namespace: string = \"common\"): UseTranslationResponse {\n // 创建一个包装函数,保持 API 兼容性\n const t = (key: string, options?: object): string => {\n // 每次调用 t() 时,i18n.t() 会使用当前的 i18n.language\n // 所以只要组件重渲染,就会得到新的翻译\n return i18n.t(key, { ns: namespace, ...options });\n };\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: t as any,\n i18n,\n ready: i18n.isInitialized,\n };\n}\n","/**\n * Mixin API - 为基类添加 i18n 支持\n */\n\nimport { i18n } from \"./i18n\";\nimport type { WebComponent, LightComponent } from \"@wsxjs/wsx-core\";\n\n/**\n * 为任何继承自 WebComponent 或 LightComponent 的类添加 i18n 支持\n *\n * 使用方式:\n * ```tsx\n * export class MyComponent extends withI18n(WebComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n *\n * export class MyLightComponent extends withI18n(LightComponent, 'common') {\n * render() {\n * return <div>{this.t('welcome')}</div>;\n * }\n * }\n * ```\n *\n * @param Base 基类(WebComponent 或 LightComponent)\n * @param defaultNamespace 默认命名空间\n * @returns 增强后的类\n */\nexport function withI18n<T extends typeof WebComponent | typeof LightComponent>(\n Base: T,\n defaultNamespace: string = \"common\"\n): T {\n return class extends Base {\n protected t(key: string, namespace?: string, options?: object): string {\n return i18n.t(key, { ns: namespace || defaultNamespace, ...options });\n }\n\n protected get i18n() {\n return i18n;\n }\n\n protected onConnected(): void {\n // 订阅语言变化事件,自动触发重渲染\n i18n.on(\"languageChanged\", () => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((this as any).rerender) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this as any).rerender();\n }\n });\n }\n } as T;\n}\n"],"names":[],"mappings":";;;AAcO,SAAS,SAAS,SAAqB,IAAoB;AAE9D,QAAM,cAAc;AAAA,IAChB,aAAa;AAAA,IACb,OAAO;AAAA,IACP,eAAe;AAAA,MACX,aAAa;AAAA,IAAA;AAAA,IAEjB,SAAS;AAAA,MACL,UAAU;AAAA,IAAA;AAAA,IAEd,IAAI,CAAC,UAAU,QAAQ,QAAQ,UAAU;AAAA,IACzC,WAAW;AAAA;AAAA;AAAA;AAAA,IAIX,WAAW;AAAA,MACP,OAAO,CAAC,gBAAgB,WAAW;AAAA,MACnC,QAAQ,CAAC,cAAc;AAAA,MACvB,oBAAoB;AAAA,IAAA;AAAA,IAExB,GAAG;AAAA,EAAA;AAIP,UAAQ,IAAI,OAAO,EAAE,IAAI,gBAAgB,EAAE,KAAK,WAAW;AAE3D,SAAO;AACX;AAGO,MAAM,OAAuB;ACvB7B,SAAS,cAAc,YAAoB,UAAU;AAExD,SAAO,SAAmD,aAAgB;AAAA,IACtE,MAAM,qBAAqB,YAAY;AAAA;AAAA,MAQnC,eAAe,MAAa;AACxB,cAAM,GAAG,IAAI;AAEb,aAAK,iBAAiB;AAAA,MAC1B;AAAA;AAAA,MAGO,EAAE,KAAa,SAA0B;AAC5C,eAAO,KAAK,EAAE,KAAK,EAAE,IAAI,KAAK,gBAAgB,GAAG,SAAS;AAAA,MAC9D;AAAA;AAAA,MAGA,IAAW,OAAO;AACd,eAAO;AAAA,MACX;AAAA;AAAA,MAGO,cAAoB;;AAEvB,oBAAM,gBAAN;AAGA,cAAM,UAAW,MAAM;AAInB,cAAI,KAAK,uBAAuB,QAAW;AACvC,iCAAqB,KAAK,kBAAkB;AAAA,UAChD;AAGA,cAAK,KAAa,UAAU;AACxB,iBAAK,qBAAqB,sBAAsB,MAAM;AAEjD,mBAAa,SAAA;AACd,mBAAK,qBAAqB;AAAA,YAC9B,CAAC;AAAA,UACL;AAAA,QACJ;AAIC,aAAa,0BAA0B;AAGxC,cAAM,cAAc,KAAK,GAAG,mBAAmB,OAAO;AAGtD,YAAI,OAAO,gBAAgB,YAAY;AACnC,eAAK,mBAAmB;AAAA,QAC5B,OAAO;AAGH,eAAK,mBAAmB,MAAM;AAC1B,gBAAI,OAAO,KAAK,QAAQ,YAAY;AAChC,mBAAK,IAAI,mBAAmB,OAAO;AAAA,YACvC;AAAA,UACJ;AAAA,QACJ;AAAA,MACJ;AAAA;AAAA,MAGO,iBAAuB;;AAE1B,YAAI,KAAK,uBAAuB,QAAW;AACvC,+BAAqB,KAAK,kBAAkB;AAC5C,eAAK,qBAAqB;AAAA,QAC9B;AAGA,YAAI,KAAK,oBAAoB,OAAO,KAAK,qBAAqB,YAAY;AACtE,cAAI;AACA,iBAAK,iBAAA;AAAA,UACT,QAAQ;AAGJ,kBAAM,UAAW,KAAa;AAC9B,gBAAI,WAAW,OAAO,KAAK,QAAQ,YAAY;AAC3C,mBAAK,IAAI,mBAAmB,OAAO;AAAA,YACvC;AAAA,UACJ;AACA,eAAK,mBAAmB;AAAA,QAC5B,OAAO;AAGH,gBAAM,UAAW,KAAa;AAC9B,cAAI,WAAW,OAAO,KAAK,QAAQ,YAAY;AAC3C,iBAAK,IAAI,mBAAmB,OAAO;AAAA,UACvC;AAAA,QACJ;AAGA,YAAK,KAAa,yBAAyB;AAEvC,iBAAQ,KAAa;AAAA,QACzB;AAGA,oBAAM,mBAAN;AAAA,MACJ;AAAA,IAAA;AAGJ,WAAO,eAAe,cAAc,WAAW;AAC/C,WAAO,eAAe,cAAc,QAAQ;AAAA,MACxC,OAAO,YAAY;AAAA,MACnB,UAAU;AAAA,IAAA,CACb;AACD,WAAO;AAAA,EACX;AACJ;AC3HO,SAAS,eAAe,YAAoB,UAAkC;AAEjF,QAAM,IAAI,CAAC,KAAa,YAA6B;AAGjD,WAAO,KAAK,EAAE,KAAK,EAAE,IAAI,WAAW,GAAG,SAAS;AAAA,EACpD;AACA,SAAO;AAAA;AAAA,IAEH;AAAA,IACA;AAAA,IACA,OAAO,KAAK;AAAA,EAAA;AAEpB;ACHO,SAAS,SACZ,MACA,mBAA2B,UAC1B;AACD,SAAO,cAAc,KAAK;AAAA,IACZ,EAAE,KAAa,WAAoB,SAA0B;AACnE,aAAO,KAAK,EAAE,KAAK,EAAE,IAAI,aAAa,kBAAkB,GAAG,SAAS;AAAA,IACxE;AAAA,IAEA,IAAc,OAAO;AACjB,aAAO;AAAA,IACX;AAAA,IAEU,cAAoB;AAE1B,WAAK,GAAG,mBAAmB,MAAM;AAE7B,YAAK,KAAa,UAAU;AAEvB,eAAa,SAAA;AAAA,QAClB;AAAA,MACJ,CAAC;AAAA,IACL;AAAA,EAAA;AAER;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/wsx-i18next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "i18next integration for WSXJS components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"i18next": "^23.0.0",
|
|
39
39
|
"i18next-browser-languagedetector": "^7.0.0",
|
|
40
40
|
"i18next-http-backend": "^2.0.0",
|
|
41
|
-
"@wsxjs/wsx-core": "0.0.
|
|
41
|
+
"@wsxjs/wsx-core": "0.0.24"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/jest": "^29.0.0",
|
package/src/decorator.ts
CHANGED
|
@@ -27,6 +27,8 @@ export function i18nDecorator(namespace: string = "common") {
|
|
|
27
27
|
// 使用 public 而不是 private,因为导出的匿名类类型限制
|
|
28
28
|
public _i18nNamespace!: string;
|
|
29
29
|
public _i18nUnsubscribe?: () => void;
|
|
30
|
+
// 防抖定时器,避免多次 rerender 调用
|
|
31
|
+
private _i18nRerenderTimer?: number;
|
|
30
32
|
|
|
31
33
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
34
|
constructor(...args: any[]) {
|
|
@@ -52,10 +54,20 @@ export function i18nDecorator(namespace: string = "common") {
|
|
|
52
54
|
|
|
53
55
|
// 创建回调函数并保存引用,以便后续取消订阅
|
|
54
56
|
const handler = (() => {
|
|
57
|
+
// 关键修复 (RFC-0042):防抖 rerender 调用,避免多次更新导致文本节点更新丢失
|
|
58
|
+
// i18next 的 languageChanged 事件可能在 changeLanguage 完成之前被多次触发
|
|
59
|
+
// 使用 requestAnimationFrame 确保只在下一个渲染帧触发一次 rerender
|
|
60
|
+
if (this._i18nRerenderTimer !== undefined) {
|
|
61
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
65
|
if ((this as any).rerender) {
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
this._i18nRerenderTimer = requestAnimationFrame(() => {
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
(this as any).rerender();
|
|
69
|
+
this._i18nRerenderTimer = undefined;
|
|
70
|
+
});
|
|
59
71
|
}
|
|
60
72
|
}) as () => void;
|
|
61
73
|
|
|
@@ -82,6 +94,12 @@ export function i18nDecorator(namespace: string = "common") {
|
|
|
82
94
|
|
|
83
95
|
// 生命周期:组件断开时取消订阅
|
|
84
96
|
public onDisconnected(): void {
|
|
97
|
+
// 取消防抖定时器
|
|
98
|
+
if (this._i18nRerenderTimer !== undefined) {
|
|
99
|
+
cancelAnimationFrame(this._i18nRerenderTimer);
|
|
100
|
+
this._i18nRerenderTimer = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
// 取消 i18n 订阅
|
|
86
104
|
if (this._i18nUnsubscribe && typeof this._i18nUnsubscribe === "function") {
|
|
87
105
|
try {
|
package/src/i18n.ts
CHANGED
|
@@ -13,11 +13,6 @@ import type { I18nConfig } from "./types";
|
|
|
13
13
|
* @returns i18n 实例
|
|
14
14
|
*/
|
|
15
15
|
export function initI18n(config: I18nConfig = {}): typeof i18next {
|
|
16
|
-
// 关键修复:手动从 localStorage 读取语言,优先级高于浏览器检测
|
|
17
|
-
// 这确保了即使 LanguageDetector 失败,也能正确加载保存的语言
|
|
18
|
-
const savedLanguage =
|
|
19
|
-
typeof window !== "undefined" ? localStorage.getItem("wsx-language") : null;
|
|
20
|
-
|
|
21
16
|
// 合并配置
|
|
22
17
|
const finalConfig = {
|
|
23
18
|
fallbackLng: "en",
|
|
@@ -30,10 +25,9 @@ export function initI18n(config: I18nConfig = {}): typeof i18next {
|
|
|
30
25
|
},
|
|
31
26
|
ns: ["common", "home", "docs", "examples"],
|
|
32
27
|
defaultNS: "common",
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
// 配置 LanguageDetector 的检测顺序(仅在没有显式 lng 时生效)
|
|
28
|
+
// 配置 LanguageDetector 的检测顺序
|
|
29
|
+
// LanguageDetector 会自动从 localStorage 读取(使用 lookupLocalStorage 指定的键)
|
|
30
|
+
// 然后回退到浏览器语言检测
|
|
37
31
|
detection: {
|
|
38
32
|
order: ["localStorage", "navigator"],
|
|
39
33
|
caches: ["localStorage"],
|