@modern-js/plugin-i18n 2.69.7 → 3.0.0-alpha.1

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.
Files changed (151) hide show
  1. package/README.md +6 -0
  2. package/dist/cjs/cli/index.cjs +154 -0
  3. package/dist/cjs/runtime/I18nLink.cjs +68 -0
  4. package/dist/cjs/runtime/context.cjs +138 -0
  5. package/dist/cjs/runtime/hooks.cjs +189 -0
  6. package/dist/cjs/runtime/i18n/backend/config.cjs +39 -0
  7. package/dist/cjs/runtime/i18n/backend/defaults.cjs +56 -0
  8. package/dist/cjs/runtime/i18n/backend/defaults.node.cjs +56 -0
  9. package/dist/cjs/runtime/i18n/backend/index.cjs +108 -0
  10. package/dist/cjs/runtime/i18n/backend/middleware.cjs +54 -0
  11. package/dist/cjs/runtime/i18n/backend/middleware.common.cjs +105 -0
  12. package/dist/cjs/runtime/i18n/backend/middleware.node.cjs +58 -0
  13. package/dist/cjs/runtime/i18n/backend/sdk-backend.cjs +171 -0
  14. package/dist/cjs/runtime/i18n/detection/config.cjs +63 -0
  15. package/dist/cjs/runtime/i18n/detection/index.cjs +309 -0
  16. package/dist/cjs/runtime/i18n/detection/middleware.cjs +185 -0
  17. package/dist/cjs/runtime/i18n/detection/middleware.node.cjs +74 -0
  18. package/dist/cjs/runtime/i18n/index.cjs +43 -0
  19. package/dist/cjs/runtime/i18n/instance.cjs +132 -0
  20. package/dist/cjs/runtime/i18n/utils.cjs +185 -0
  21. package/dist/cjs/runtime/index.cjs +172 -0
  22. package/dist/cjs/runtime/types.cjs +18 -0
  23. package/dist/cjs/runtime/utils.cjs +134 -0
  24. package/dist/cjs/server/index.cjs +178 -0
  25. package/dist/cjs/shared/deepMerge.cjs +54 -0
  26. package/dist/cjs/shared/detection.cjs +105 -0
  27. package/dist/cjs/shared/type.cjs +18 -0
  28. package/dist/cjs/shared/utils.cjs +78 -0
  29. package/dist/esm/cli/index.js +106 -0
  30. package/dist/esm/runtime/I18nLink.js +31 -0
  31. package/dist/esm/runtime/context.js +101 -0
  32. package/dist/esm/runtime/hooks.js +146 -0
  33. package/dist/esm/runtime/i18n/backend/config.js +5 -0
  34. package/dist/esm/runtime/i18n/backend/defaults.js +19 -0
  35. package/dist/esm/runtime/i18n/backend/defaults.node.js +19 -0
  36. package/dist/esm/runtime/i18n/backend/index.js +74 -0
  37. package/dist/esm/runtime/i18n/backend/middleware.common.js +61 -0
  38. package/dist/esm/runtime/i18n/backend/middleware.js +7 -0
  39. package/dist/esm/runtime/i18n/backend/middleware.node.js +8 -0
  40. package/dist/esm/runtime/i18n/backend/sdk-backend.js +137 -0
  41. package/dist/esm/runtime/i18n/detection/config.js +26 -0
  42. package/dist/esm/runtime/i18n/detection/index.js +260 -0
  43. package/dist/esm/runtime/i18n/detection/middleware.js +132 -0
  44. package/dist/esm/runtime/i18n/detection/middleware.node.js +31 -0
  45. package/dist/esm/runtime/i18n/index.js +3 -0
  46. package/dist/esm/runtime/i18n/instance.js +77 -0
  47. package/dist/esm/runtime/i18n/utils.js +136 -0
  48. package/dist/esm/runtime/index.js +129 -0
  49. package/dist/esm/runtime/types.js +0 -0
  50. package/dist/esm/runtime/utils.js +82 -0
  51. package/dist/esm/server/index.js +168 -0
  52. package/dist/esm/shared/deepMerge.js +20 -0
  53. package/dist/esm/shared/detection.js +71 -0
  54. package/dist/esm/shared/type.js +0 -0
  55. package/dist/esm/shared/utils.js +35 -0
  56. package/dist/esm-node/cli/index.js +106 -0
  57. package/dist/esm-node/runtime/I18nLink.js +31 -0
  58. package/dist/esm-node/runtime/context.js +101 -0
  59. package/dist/esm-node/runtime/hooks.js +146 -0
  60. package/dist/esm-node/runtime/i18n/backend/config.js +5 -0
  61. package/dist/esm-node/runtime/i18n/backend/defaults.js +19 -0
  62. package/dist/esm-node/runtime/i18n/backend/defaults.node.js +19 -0
  63. package/dist/esm-node/runtime/i18n/backend/index.js +74 -0
  64. package/dist/esm-node/runtime/i18n/backend/middleware.common.js +61 -0
  65. package/dist/esm-node/runtime/i18n/backend/middleware.js +7 -0
  66. package/dist/esm-node/runtime/i18n/backend/middleware.node.js +8 -0
  67. package/dist/esm-node/runtime/i18n/backend/sdk-backend.js +137 -0
  68. package/dist/esm-node/runtime/i18n/detection/config.js +26 -0
  69. package/dist/esm-node/runtime/i18n/detection/index.js +260 -0
  70. package/dist/esm-node/runtime/i18n/detection/middleware.js +132 -0
  71. package/dist/esm-node/runtime/i18n/detection/middleware.node.js +31 -0
  72. package/dist/esm-node/runtime/i18n/index.js +3 -0
  73. package/dist/esm-node/runtime/i18n/instance.js +77 -0
  74. package/dist/esm-node/runtime/i18n/utils.js +136 -0
  75. package/dist/esm-node/runtime/index.js +129 -0
  76. package/dist/esm-node/runtime/types.js +0 -0
  77. package/dist/esm-node/runtime/utils.js +82 -0
  78. package/dist/esm-node/server/index.js +168 -0
  79. package/dist/esm-node/shared/deepMerge.js +20 -0
  80. package/dist/esm-node/shared/detection.js +71 -0
  81. package/dist/esm-node/shared/type.js +0 -0
  82. package/dist/esm-node/shared/utils.js +35 -0
  83. package/dist/types/cli/index.d.ts +21 -0
  84. package/dist/types/runtime/I18nLink.d.ts +8 -0
  85. package/dist/types/runtime/context.d.ts +38 -0
  86. package/dist/types/runtime/hooks.d.ts +28 -0
  87. package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
  88. package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
  89. package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
  90. package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
  91. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
  92. package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
  93. package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
  94. package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +52 -0
  95. package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
  96. package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
  97. package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
  98. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
  99. package/dist/types/runtime/i18n/index.d.ts +3 -0
  100. package/dist/types/runtime/i18n/instance.d.ts +93 -0
  101. package/dist/types/runtime/i18n/utils.d.ts +29 -0
  102. package/dist/types/runtime/index.d.ts +20 -0
  103. package/dist/types/runtime/types.d.ts +15 -0
  104. package/dist/types/runtime/utils.d.ts +33 -0
  105. package/dist/types/server/index.d.ts +8 -0
  106. package/dist/types/shared/deepMerge.d.ts +1 -0
  107. package/dist/types/shared/detection.d.ts +11 -0
  108. package/dist/types/shared/type.d.ts +156 -0
  109. package/dist/types/shared/utils.d.ts +5 -0
  110. package/package.json +100 -34
  111. package/rslib.config.mts +4 -0
  112. package/src/cli/index.ts +245 -0
  113. package/src/runtime/I18nLink.tsx +76 -0
  114. package/src/runtime/context.tsx +256 -0
  115. package/src/runtime/hooks.ts +274 -0
  116. package/src/runtime/i18n/backend/config.ts +10 -0
  117. package/src/runtime/i18n/backend/defaults.node.ts +31 -0
  118. package/src/runtime/i18n/backend/defaults.ts +37 -0
  119. package/src/runtime/i18n/backend/index.ts +181 -0
  120. package/src/runtime/i18n/backend/middleware.common.ts +116 -0
  121. package/src/runtime/i18n/backend/middleware.node.ts +32 -0
  122. package/src/runtime/i18n/backend/middleware.ts +28 -0
  123. package/src/runtime/i18n/backend/sdk-backend.ts +292 -0
  124. package/src/runtime/i18n/detection/config.ts +32 -0
  125. package/src/runtime/i18n/detection/index.ts +641 -0
  126. package/src/runtime/i18n/detection/middleware.node.ts +84 -0
  127. package/src/runtime/i18n/detection/middleware.ts +251 -0
  128. package/src/runtime/i18n/index.ts +8 -0
  129. package/src/runtime/i18n/instance.ts +227 -0
  130. package/src/runtime/i18n/utils.ts +333 -0
  131. package/src/runtime/index.tsx +281 -0
  132. package/src/runtime/types.ts +17 -0
  133. package/src/runtime/utils.ts +151 -0
  134. package/src/server/index.ts +336 -0
  135. package/src/shared/deepMerge.ts +38 -0
  136. package/src/shared/detection.ts +131 -0
  137. package/src/shared/type.ts +170 -0
  138. package/src/shared/utils.ts +82 -0
  139. package/tsconfig.json +12 -0
  140. package/dist/cjs/index.js +0 -73
  141. package/dist/cjs/languageDetector.js +0 -51
  142. package/dist/cjs/utils/index.js +0 -39
  143. package/dist/esm/index.js +0 -61
  144. package/dist/esm/languageDetector.js +0 -33
  145. package/dist/esm/utils/index.js +0 -16
  146. package/dist/esm-node/index.js +0 -49
  147. package/dist/esm-node/languageDetector.js +0 -26
  148. package/dist/esm-node/utils/index.js +0 -15
  149. package/dist/types/index.d.ts +0 -34
  150. package/dist/types/languageDetector.d.ts +0 -6
  151. package/dist/types/utils/index.d.ts +0 -5
@@ -0,0 +1,181 @@
1
+ import type {
2
+ BaseBackendOptions,
3
+ ChainedBackendConfig,
4
+ } from '../../../shared/type';
5
+ import type { BackendOptions, I18nInitOptions } from '../instance';
6
+ import { mergeBackendOptions as baseMergeBackendOptions } from './config';
7
+ import {
8
+ DEFAULT_I18NEXT_BACKEND_OPTIONS,
9
+ convertBackendOptions,
10
+ } from './defaults';
11
+
12
+ function hasSdkFunction(
13
+ backend?: BaseBackendOptions,
14
+ userInitOptions?: I18nInitOptions,
15
+ ): boolean {
16
+ return (
17
+ typeof userInitOptions?.backend?.sdk === 'function' ||
18
+ (!!backend?.enabled && !!backend?.sdk && typeof backend.sdk === 'function')
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Checks if loadPath is configured.
24
+ * If backend.enabled is true and user didn't explicitly configure loadPath,
25
+ * we will use default loadPath, so consider it as having loadPath.
26
+ */
27
+ function hasLoadPath(
28
+ backend?: BaseBackendOptions,
29
+ userInitOptions?: I18nInitOptions,
30
+ ): { hasPath: boolean; isExplicit: boolean } {
31
+ const userLoadPath = userInitOptions?.backend?.loadPath ?? backend?.loadPath;
32
+ const isExplicit: boolean = !!userLoadPath && userLoadPath !== '';
33
+ const hasPath =
34
+ isExplicit || (!!backend?.enabled && userLoadPath === undefined);
35
+
36
+ return { hasPath, isExplicit };
37
+ }
38
+
39
+ function ensureDefaultLoadPath(
40
+ merged: BackendOptions,
41
+ backend?: BaseBackendOptions,
42
+ isExplicitLoadPath = false,
43
+ ): void {
44
+ if (backend?.enabled && !isExplicitLoadPath && !merged.loadPath) {
45
+ merged.loadPath = DEFAULT_I18NEXT_BACKEND_OPTIONS.loadPath;
46
+ merged.addPath = DEFAULT_I18NEXT_BACKEND_OPTIONS.addPath;
47
+ }
48
+ }
49
+
50
+ function getFinalLoadPath(
51
+ mergedOptions?: BackendOptions,
52
+ backend?: BaseBackendOptions,
53
+ userInitOptions?: I18nInitOptions,
54
+ ): string | undefined {
55
+ return (
56
+ mergedOptions?.loadPath ||
57
+ userInitOptions?.backend?.loadPath ||
58
+ (backend?.enabled ? DEFAULT_I18NEXT_BACKEND_OPTIONS.loadPath : undefined)
59
+ );
60
+ }
61
+
62
+ function getFinalSdk(
63
+ mergedOptions?: BackendOptions,
64
+ backend?: BaseBackendOptions,
65
+ userInitOptions?: I18nInitOptions,
66
+ ): any {
67
+ return (
68
+ mergedOptions?.sdk ||
69
+ userInitOptions?.backend?.sdk ||
70
+ (backend?.sdk && typeof backend.sdk === 'function'
71
+ ? backend.sdk
72
+ : undefined)
73
+ );
74
+ }
75
+
76
+ function buildChainedBackendConfig(
77
+ backend?: BaseBackendOptions,
78
+ userInitOptions?: I18nInitOptions,
79
+ ): ChainedBackendConfig & BaseBackendOptions {
80
+ const merged = baseMergeBackendOptions(
81
+ DEFAULT_I18NEXT_BACKEND_OPTIONS,
82
+ backend as BackendOptions,
83
+ userInitOptions?.backend,
84
+ );
85
+
86
+ const { isExplicit } = hasLoadPath(backend, userInitOptions);
87
+ ensureDefaultLoadPath(merged, backend, isExplicit);
88
+
89
+ const mergedOptions = convertBackendOptions(merged);
90
+ const finalLoadPath = getFinalLoadPath(
91
+ mergedOptions,
92
+ backend,
93
+ userInitOptions,
94
+ );
95
+ const finalSdk = getFinalSdk(mergedOptions, backend, userInitOptions);
96
+
97
+ const chainedBackendOptions = [
98
+ {
99
+ loadPath: finalLoadPath,
100
+ addPath:
101
+ mergedOptions?.addPath || DEFAULT_I18NEXT_BACKEND_OPTIONS.addPath,
102
+ },
103
+ {
104
+ sdk: finalSdk,
105
+ },
106
+ ];
107
+
108
+ return {
109
+ ...mergedOptions,
110
+ loadPath: finalLoadPath,
111
+ sdk: finalSdk,
112
+ cacheHitMode:
113
+ mergedOptions?.cacheHitMode ||
114
+ (backend as BackendOptions)?.cacheHitMode ||
115
+ userInitOptions?.backend?.cacheHitMode ||
116
+ 'refreshAndUpdateStore',
117
+ _useChainedBackend: true,
118
+ _chainedBackendConfig: {
119
+ backendOptions: chainedBackendOptions,
120
+ },
121
+ };
122
+ }
123
+
124
+ function buildSdkOnlyBackendConfig(
125
+ backend?: BaseBackendOptions,
126
+ userInitOptions?: I18nInitOptions,
127
+ ): BackendOptions {
128
+ const merged = baseMergeBackendOptions(
129
+ {} as BackendOptions,
130
+ backend as BackendOptions,
131
+ userInitOptions?.backend,
132
+ );
133
+ return convertBackendOptions(merged) || ({} as BackendOptions);
134
+ }
135
+
136
+ function buildHttpFsBackendConfig(
137
+ backend?: BaseBackendOptions,
138
+ userInitOptions?: I18nInitOptions,
139
+ ): BackendOptions | undefined {
140
+ // If backend.enabled is false and no userInitOptions.backend, return undefined
141
+ // to avoid registering backend plugin unnecessarily
142
+ if (!backend?.enabled && !userInitOptions?.backend) {
143
+ return undefined;
144
+ }
145
+
146
+ const mergedBackend = backend?.enabled
147
+ ? baseMergeBackendOptions(
148
+ DEFAULT_I18NEXT_BACKEND_OPTIONS,
149
+ backend as BackendOptions,
150
+ userInitOptions?.backend,
151
+ )
152
+ : userInitOptions?.backend;
153
+
154
+ if (mergedBackend) {
155
+ const { isExplicit } = hasLoadPath(backend, userInitOptions);
156
+ ensureDefaultLoadPath(mergedBackend, backend, isExplicit);
157
+ }
158
+
159
+ return (
160
+ convertBackendOptions(mergedBackend as BackendOptions) ||
161
+ ({} as BackendOptions)
162
+ );
163
+ }
164
+
165
+ export const mergeBackendOptions = (
166
+ backend?: BaseBackendOptions,
167
+ userInitOptions?: I18nInitOptions,
168
+ ): BackendOptions | undefined => {
169
+ const sdkFunction = hasSdkFunction(backend, userInitOptions);
170
+ const { hasPath } = hasLoadPath(backend, userInitOptions);
171
+
172
+ if (hasPath && sdkFunction) {
173
+ return buildChainedBackendConfig(backend, userInitOptions);
174
+ }
175
+
176
+ if (sdkFunction) {
177
+ return buildSdkOnlyBackendConfig(backend, userInitOptions);
178
+ }
179
+
180
+ return buildHttpFsBackendConfig(backend, userInitOptions);
181
+ };
@@ -0,0 +1,116 @@
1
+ import ChainedBackend from 'i18next-chained-backend';
2
+ import type {
3
+ BaseBackendOptions,
4
+ ChainedBackendConfig,
5
+ } from '../../../shared/type';
6
+ import type { I18nInstance } from '../instance';
7
+ import { getActualI18nextInstance } from '../instance';
8
+ import { SdkBackend } from './sdk-backend';
9
+
10
+ type BackendConfigWithChained = BaseBackendOptions &
11
+ Partial<ChainedBackendConfig>;
12
+
13
+ function checkBackendConfig(backend?: BackendConfigWithChained) {
14
+ const hasSdk = backend?.sdk && typeof backend.sdk === 'function';
15
+ const hasLoadPath = !!backend?.loadPath;
16
+ const useChained = backend?._useChainedBackend;
17
+
18
+ return { hasSdk, hasLoadPath, useChained };
19
+ }
20
+
21
+ function buildChainedBackendConfig(
22
+ backend: BackendConfigWithChained,
23
+ BackendWithSave: new (...args: any[]) => any,
24
+ ) {
25
+ const cacheHitMode = backend.cacheHitMode || 'refreshAndUpdateStore';
26
+
27
+ if (backend._chainedBackendConfig) {
28
+ return {
29
+ backends: [BackendWithSave, SdkBackend],
30
+ backendOptions: backend._chainedBackendConfig.backendOptions,
31
+ cacheHitMode,
32
+ };
33
+ }
34
+
35
+ // Legacy: build chained backend config from backend options
36
+ return {
37
+ backends: [BackendWithSave, SdkBackend],
38
+ backendOptions: [
39
+ {
40
+ loadPath: backend.loadPath,
41
+ addPath: backend.addPath,
42
+ },
43
+ {
44
+ sdk: backend.sdk,
45
+ },
46
+ ],
47
+ cacheHitMode,
48
+ };
49
+ }
50
+
51
+ function setupChainedBackend(
52
+ i18nInstance: I18nInstance,
53
+ backend: BackendConfigWithChained,
54
+ BackendWithSave: new (...args: any[]) => any,
55
+ ) {
56
+ i18nInstance.use(ChainedBackend);
57
+ const actualInstance = getActualI18nextInstance(i18nInstance);
58
+ if (actualInstance?.options) {
59
+ actualInstance.options.backend = buildChainedBackendConfig(
60
+ backend,
61
+ BackendWithSave,
62
+ );
63
+ }
64
+ if (i18nInstance.options) {
65
+ i18nInstance.options.backend = buildChainedBackendConfig(
66
+ backend,
67
+ BackendWithSave,
68
+ );
69
+ }
70
+ }
71
+
72
+ function cleanBackendConfig(backend: BackendConfigWithChained) {
73
+ const { _useChainedBackend, _chainedBackendConfig, ...cleanBackend } =
74
+ backend;
75
+ return cleanBackend;
76
+ }
77
+
78
+ /**
79
+ * Common logic for using i18next backend
80
+ * This function handles the backend selection and chained backend configuration
81
+ *
82
+ * @param i18nInstance - The i18n instance to configure
83
+ * @param BackendWithSave - The wrapped backend class with save method (required for chained backend refresh logic)
84
+ * @param BackendBase - The base backend class (for non-chained use)
85
+ * @param backend - Optional backend configuration
86
+ */
87
+ export function useI18nextBackendCommon(
88
+ i18nInstance: I18nInstance,
89
+ BackendWithSave: new (...args: any[]) => any,
90
+ BackendBase: new (...args: any[]) => any,
91
+ backend?: BackendConfigWithChained,
92
+ ) {
93
+ if (!backend) {
94
+ return i18nInstance.use(BackendBase);
95
+ }
96
+
97
+ const { hasSdk, hasLoadPath, useChained } = checkBackendConfig(backend);
98
+
99
+ if (useChained || (hasLoadPath && hasSdk)) {
100
+ setupChainedBackend(i18nInstance, backend, BackendWithSave);
101
+ return;
102
+ }
103
+
104
+ if (hasSdk) {
105
+ return i18nInstance.use(SdkBackend);
106
+ }
107
+
108
+ const actualInstance = getActualI18nextInstance(i18nInstance);
109
+ if (actualInstance?.options) {
110
+ actualInstance.options.backend = cleanBackendConfig(backend);
111
+ }
112
+ if (i18nInstance.options) {
113
+ i18nInstance.options.backend = cleanBackendConfig(backend);
114
+ }
115
+ return i18nInstance.use(BackendBase);
116
+ }
@@ -0,0 +1,32 @@
1
+ import Backend from 'i18next-fs-backend';
2
+ import type { ExtendedBackendOptions } from '../../../shared/type';
3
+ import type { I18nInstance } from '../instance';
4
+ import { useI18nextBackendCommon } from './middleware.common';
5
+
6
+ /**
7
+ * Wrapper for FS backend to add a no-op save method
8
+ * This is required for i18next-chained-backend to trigger refresh logic
9
+ * when cacheHitMode is 'refresh' or 'refreshAndUpdateStore'
10
+ */
11
+ export class FsBackendWithSave extends Backend {
12
+ save(_language: string, _namespace: string, _data: unknown): void {
13
+ // No-op: FS backend doesn't need to save in this context, but we need this method
14
+ // to trigger i18next-chained-backend's refresh logic
15
+ }
16
+ }
17
+
18
+ // Export as HttpBackendWithSave for consistency with browser version
19
+ // This allows utils.ts to import the same name in both environments
20
+ export const HttpBackendWithSave = FsBackendWithSave;
21
+
22
+ export const useI18nextBackend = (
23
+ i18nInstance: I18nInstance,
24
+ backend?: ExtendedBackendOptions,
25
+ ) => {
26
+ return useI18nextBackendCommon(
27
+ i18nInstance,
28
+ FsBackendWithSave,
29
+ Backend,
30
+ backend,
31
+ );
32
+ };
@@ -0,0 +1,28 @@
1
+ import Backend from 'i18next-http-backend';
2
+ import type { ExtendedBackendOptions } from '../../../shared/type';
3
+ import type { I18nInstance } from '../instance';
4
+ import { useI18nextBackendCommon } from './middleware.common';
5
+
6
+ /**
7
+ * Wrapper for HTTP backend to add a no-op save method
8
+ * This is required for i18next-chained-backend to trigger refresh logic
9
+ * when cacheHitMode is 'refresh' or 'refreshAndUpdateStore'
10
+ */
11
+ export class HttpBackendWithSave extends Backend {
12
+ save(_language: string, _namespace: string, _data: unknown): void {
13
+ // No-op: HTTP backend doesn't need to save, but we need this method
14
+ // to trigger i18next-chained-backend's refresh logic
15
+ }
16
+ }
17
+
18
+ export const useI18nextBackend = (
19
+ i18nInstance: I18nInstance,
20
+ backend?: ExtendedBackendOptions,
21
+ ) => {
22
+ return useI18nextBackendCommon(
23
+ i18nInstance,
24
+ HttpBackendWithSave,
25
+ Backend,
26
+ backend,
27
+ );
28
+ };
@@ -0,0 +1,292 @@
1
+ import type { I18nSdkLoadOptions, I18nSdkLoader } from '../../../shared/type';
2
+ import type { Resources } from '../instance';
3
+
4
+ interface BackendOptions {
5
+ sdk?: I18nSdkLoader;
6
+ [key: string]: unknown;
7
+ }
8
+
9
+ interface I18nextServices {
10
+ resourceStore?: {
11
+ data?: {
12
+ [language: string]: {
13
+ [namespace: string]: Record<string, string>;
14
+ };
15
+ };
16
+ };
17
+ store?: {
18
+ data?: {
19
+ [language: string]: {
20
+ [namespace: string]: Record<string, string>;
21
+ };
22
+ };
23
+ };
24
+ [key: string]: any;
25
+ }
26
+
27
+ export class SdkBackend {
28
+ static type = 'backend';
29
+ type = 'backend' as const;
30
+ sdk?: I18nSdkLoader;
31
+ private allResourcesCache: Resources | null = null;
32
+ private loadingPromises = new Map<string, Promise<unknown>>();
33
+ private services?: I18nextServices;
34
+
35
+ constructor(_services: unknown, _options: Record<string, unknown>) {
36
+ void _services;
37
+ void _options;
38
+ }
39
+
40
+ init(
41
+ services: I18nextServices,
42
+ backendOptions: BackendOptions,
43
+ _i18nextOptions: unknown,
44
+ ): void {
45
+ this.services = services;
46
+ void _i18nextOptions;
47
+ this.sdk = backendOptions?.sdk;
48
+ if (!this.sdk) {
49
+ throw new Error(
50
+ 'SdkBackend requires an SDK function to be provided in backend options',
51
+ );
52
+ }
53
+ }
54
+
55
+ read(
56
+ language: string,
57
+ namespace: string,
58
+ callback: (error: Error | null, data: unknown) => void,
59
+ ) {
60
+ if (!this.sdk) {
61
+ console.error('[i18n] SdkBackend.read - SDK function not initialized');
62
+ callback(new Error('SDK function not initialized'), null);
63
+ return;
64
+ }
65
+
66
+ const cached = this.allResourcesCache
67
+ ? this.extractFromCache(language, namespace)
68
+ : null;
69
+ if (cached !== null) {
70
+ // Merge cached data with existing store data to preserve HTTP backend data
71
+ const mergedData = this.mergeWithExistingResources(
72
+ language,
73
+ namespace,
74
+ cached,
75
+ );
76
+ callback(null, mergedData);
77
+ return;
78
+ }
79
+
80
+ const cacheKey = this.getCacheKey(language, namespace);
81
+ const existingPromise = this.loadingPromises.get(cacheKey);
82
+ if (existingPromise) {
83
+ this.handlePromise(existingPromise, language, namespace, callback, false);
84
+ return;
85
+ }
86
+
87
+ this.loadResource(language, namespace, callback);
88
+ }
89
+
90
+ create(
91
+ _languages: string[],
92
+ _namespace: string,
93
+ _key: string,
94
+ _fallbackValue: string,
95
+ ): void {
96
+ // Not implemented - translations are managed by the external SDK service
97
+ }
98
+
99
+ isLoading(language: string, namespace: string): boolean {
100
+ return this.loadingPromises.has(this.getCacheKey(language, namespace));
101
+ }
102
+
103
+ getLoadingResources(): Array<{ language: string; namespace: string }> {
104
+ const loading: Array<{ language: string; namespace: string }> = [];
105
+ for (const key of this.loadingPromises.keys()) {
106
+ const [language, namespace] = key.split(':');
107
+ if (language && namespace) {
108
+ loading.push({ language, namespace });
109
+ }
110
+ }
111
+ return loading;
112
+ }
113
+
114
+ hasLoadingResources(): boolean {
115
+ return this.loadingPromises.size > 0;
116
+ }
117
+
118
+ private getCacheKey(language: string, namespace: string): string {
119
+ return `${language}:${namespace}`;
120
+ }
121
+
122
+ private loadResource(
123
+ language: string,
124
+ namespace: string,
125
+ callback: (error: Error | null, data: unknown) => void,
126
+ ): void {
127
+ try {
128
+ const result = this.callSdk(language, namespace);
129
+ const loadPromise =
130
+ result instanceof Promise ? result : Promise.resolve(result);
131
+ const cacheKey = this.getCacheKey(language, namespace);
132
+
133
+ this.loadingPromises.set(cacheKey, loadPromise);
134
+
135
+ this.handlePromise(loadPromise, language, namespace, callback, true);
136
+ } catch (error) {
137
+ callback(this.normalizeError(error), null);
138
+ }
139
+ }
140
+
141
+ private handlePromise(
142
+ promise: Promise<unknown>,
143
+ language: string,
144
+ namespace: string,
145
+ callback: (error: Error | null, data: unknown) => void,
146
+ shouldUpdateCache: boolean,
147
+ ): void {
148
+ const cacheKey = this.getCacheKey(language, namespace);
149
+
150
+ promise
151
+ .then(data => {
152
+ const formattedData = this.formatResources(data, language, namespace);
153
+ // Merge with existing resources in store to preserve data from other backends (e.g., HTTP backend)
154
+ // This is important when using refreshAndUpdateStore mode in chained backend
155
+ const mergedData = this.mergeWithExistingResources(
156
+ language,
157
+ namespace,
158
+ formattedData,
159
+ );
160
+ if (shouldUpdateCache) {
161
+ this.updateCache(language, namespace, mergedData);
162
+ this.loadingPromises.delete(cacheKey);
163
+ }
164
+ callback(null, mergedData);
165
+ this.triggerI18nextUpdate(language, namespace);
166
+ })
167
+ .catch(error => {
168
+ if (shouldUpdateCache) {
169
+ this.loadingPromises.delete(cacheKey);
170
+ }
171
+ callback(this.normalizeError(error), null);
172
+ });
173
+ }
174
+
175
+ private normalizeError(error: unknown): Error {
176
+ return error instanceof Error ? error : new Error(String(error));
177
+ }
178
+
179
+ private callSdk(
180
+ language: string,
181
+ namespace: string,
182
+ ): Promise<Resources> | Resources {
183
+ if (!this.sdk) {
184
+ throw new Error('SDK function not initialized');
185
+ }
186
+
187
+ const options: I18nSdkLoadOptions = { lng: language, ns: namespace };
188
+ return this.sdk(options);
189
+ }
190
+
191
+ private extractFromCache(
192
+ language: string,
193
+ namespace: string,
194
+ ): Record<string, string> | null {
195
+ if (!this.allResourcesCache) {
196
+ return null;
197
+ }
198
+
199
+ const langData = this.allResourcesCache[language];
200
+ if (!this.isObject(langData)) {
201
+ return null;
202
+ }
203
+
204
+ const nsData = langData[namespace];
205
+ if (!this.isObject(nsData)) {
206
+ return null;
207
+ }
208
+
209
+ return nsData as Record<string, string>;
210
+ }
211
+
212
+ private updateCache(
213
+ language: string,
214
+ namespace: string,
215
+ data: unknown,
216
+ ): void {
217
+ if (!this.allResourcesCache) {
218
+ this.allResourcesCache = {};
219
+ }
220
+
221
+ if (!this.allResourcesCache[language]) {
222
+ this.allResourcesCache[language] = {};
223
+ }
224
+
225
+ if (this.isObject(data)) {
226
+ this.allResourcesCache[language][namespace] = data as Record<
227
+ string,
228
+ string
229
+ >;
230
+ }
231
+ }
232
+
233
+ private formatResources(
234
+ data: unknown,
235
+ language: string,
236
+ namespace: string,
237
+ ): Record<string, string> {
238
+ if (!this.isObject(data)) {
239
+ return {};
240
+ }
241
+
242
+ const dataObj = data as Record<string, unknown>;
243
+ const langData = dataObj[language];
244
+
245
+ if (this.isObject(langData)) {
246
+ const nsData = (langData as Record<string, unknown>)[namespace];
247
+ if (this.isObject(nsData)) {
248
+ return nsData as Record<string, string>;
249
+ }
250
+ }
251
+
252
+ const hasLanguageKeys = Object.keys(dataObj).some(key =>
253
+ this.isObject(dataObj[key]),
254
+ );
255
+ if (!hasLanguageKeys) {
256
+ return dataObj as Record<string, string>;
257
+ }
258
+
259
+ return {};
260
+ }
261
+
262
+ private isObject(value: unknown): value is Record<string, unknown> {
263
+ return value !== null && typeof value === 'object';
264
+ }
265
+
266
+ private mergeWithExistingResources(
267
+ language: string,
268
+ namespace: string,
269
+ sdkData: Record<string, string>,
270
+ ): Record<string, string> {
271
+ // Get existing resources from store (may contain data from HTTP backend)
272
+ const store = this.services?.resourceStore || this.services?.store;
273
+ const existingData =
274
+ store?.data?.[language]?.[namespace] || ({} as Record<string, string>);
275
+
276
+ // Merge: preserve existing data (from HTTP backend), add/update with SDK data
277
+ // This ensures that when using refreshAndUpdateStore, HTTP backend data is not lost
278
+ return {
279
+ ...existingData,
280
+ ...sdkData,
281
+ };
282
+ }
283
+
284
+ private triggerI18nextUpdate(language: string, namespace: string): void {
285
+ if (typeof window !== 'undefined') {
286
+ const event = new CustomEvent('i18n-sdk-resources-loaded', {
287
+ detail: { language, namespace },
288
+ });
289
+ window.dispatchEvent(event);
290
+ }
291
+ }
292
+ }
@@ -0,0 +1,32 @@
1
+ import { deepMerge } from '../../../shared/deepMerge.js';
2
+ import type { LanguageDetectorOptions } from '../instance';
3
+
4
+ export const DEFAULT_I18NEXT_DETECTION_OPTIONS = {
5
+ caches: ['cookie', 'localStorage'],
6
+ order: [
7
+ 'querystring',
8
+ 'cookie',
9
+ 'localStorage',
10
+ 'header',
11
+ 'navigator',
12
+ 'htmlTag',
13
+ 'path',
14
+ 'subdomain',
15
+ ],
16
+ cookieMinutes: 60 * 24 * 365,
17
+ lookupQuerystring: 'lng',
18
+ lookupCookie: 'i18next',
19
+ lookupLocalStorage: 'i18nextLng',
20
+ lookupHeader: 'accept-language',
21
+ };
22
+
23
+ export function mergeDetectionOptions(
24
+ cliOptions?: LanguageDetectorOptions,
25
+ userOptions?: LanguageDetectorOptions,
26
+ defaultOptions: LanguageDetectorOptions = DEFAULT_I18NEXT_DETECTION_OPTIONS,
27
+ ): LanguageDetectorOptions {
28
+ return deepMerge(
29
+ deepMerge(defaultOptions, cliOptions ?? {}),
30
+ userOptions ?? {},
31
+ );
32
+ }