@modern-js/plugin-i18n 2.69.5 → 3.0.0-alpha.0
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/README.md +6 -0
- package/dist/cjs/cli/index.cjs +154 -0
- package/dist/cjs/runtime/I18nLink.cjs +68 -0
- package/dist/cjs/runtime/context.cjs +138 -0
- package/dist/cjs/runtime/hooks.cjs +189 -0
- package/dist/cjs/runtime/i18n/backend/config.cjs +39 -0
- package/dist/cjs/runtime/i18n/backend/defaults.cjs +56 -0
- package/dist/cjs/runtime/i18n/backend/defaults.node.cjs +56 -0
- package/dist/cjs/runtime/i18n/backend/index.cjs +108 -0
- package/dist/cjs/runtime/i18n/backend/middleware.cjs +54 -0
- package/dist/cjs/runtime/i18n/backend/middleware.common.cjs +105 -0
- package/dist/cjs/runtime/i18n/backend/middleware.node.cjs +58 -0
- package/dist/cjs/runtime/i18n/backend/sdk-backend.cjs +171 -0
- package/dist/cjs/runtime/i18n/detection/config.cjs +63 -0
- package/dist/cjs/runtime/i18n/detection/index.cjs +309 -0
- package/dist/cjs/runtime/i18n/detection/middleware.cjs +185 -0
- package/dist/cjs/runtime/i18n/detection/middleware.node.cjs +74 -0
- package/dist/cjs/runtime/i18n/index.cjs +43 -0
- package/dist/cjs/runtime/i18n/instance.cjs +132 -0
- package/dist/cjs/runtime/i18n/utils.cjs +185 -0
- package/dist/cjs/runtime/index.cjs +162 -0
- package/dist/cjs/runtime/types.cjs +18 -0
- package/dist/cjs/runtime/utils.cjs +134 -0
- package/dist/cjs/server/index.cjs +178 -0
- package/dist/cjs/shared/deepMerge.cjs +54 -0
- package/dist/cjs/shared/detection.cjs +105 -0
- package/dist/cjs/shared/type.cjs +18 -0
- package/dist/cjs/shared/utils.cjs +78 -0
- package/dist/esm/cli/index.js +106 -0
- package/dist/esm/runtime/I18nLink.js +31 -0
- package/dist/esm/runtime/context.js +101 -0
- package/dist/esm/runtime/hooks.js +146 -0
- package/dist/esm/runtime/i18n/backend/config.js +5 -0
- package/dist/esm/runtime/i18n/backend/defaults.js +19 -0
- package/dist/esm/runtime/i18n/backend/defaults.node.js +19 -0
- package/dist/esm/runtime/i18n/backend/index.js +74 -0
- package/dist/esm/runtime/i18n/backend/middleware.common.js +61 -0
- package/dist/esm/runtime/i18n/backend/middleware.js +7 -0
- package/dist/esm/runtime/i18n/backend/middleware.node.js +8 -0
- package/dist/esm/runtime/i18n/backend/sdk-backend.js +137 -0
- package/dist/esm/runtime/i18n/detection/config.js +26 -0
- package/dist/esm/runtime/i18n/detection/index.js +260 -0
- package/dist/esm/runtime/i18n/detection/middleware.js +132 -0
- package/dist/esm/runtime/i18n/detection/middleware.node.js +31 -0
- package/dist/esm/runtime/i18n/index.js +3 -0
- package/dist/esm/runtime/i18n/instance.js +77 -0
- package/dist/esm/runtime/i18n/utils.js +136 -0
- package/dist/esm/runtime/index.js +119 -0
- package/dist/esm/runtime/types.js +0 -0
- package/dist/esm/runtime/utils.js +82 -0
- package/dist/esm/server/index.js +168 -0
- package/dist/esm/shared/deepMerge.js +20 -0
- package/dist/esm/shared/detection.js +71 -0
- package/dist/esm/shared/type.js +0 -0
- package/dist/esm/shared/utils.js +35 -0
- package/dist/esm-node/cli/index.js +106 -0
- package/dist/esm-node/runtime/I18nLink.js +31 -0
- package/dist/esm-node/runtime/context.js +101 -0
- package/dist/esm-node/runtime/hooks.js +146 -0
- package/dist/esm-node/runtime/i18n/backend/config.js +5 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.js +19 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.node.js +19 -0
- package/dist/esm-node/runtime/i18n/backend/index.js +74 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.common.js +61 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.js +7 -0
- package/dist/esm-node/runtime/i18n/backend/middleware.node.js +8 -0
- package/dist/esm-node/runtime/i18n/backend/sdk-backend.js +137 -0
- package/dist/esm-node/runtime/i18n/detection/config.js +26 -0
- package/dist/esm-node/runtime/i18n/detection/index.js +260 -0
- package/dist/esm-node/runtime/i18n/detection/middleware.js +132 -0
- package/dist/esm-node/runtime/i18n/detection/middleware.node.js +31 -0
- package/dist/esm-node/runtime/i18n/index.js +3 -0
- package/dist/esm-node/runtime/i18n/instance.js +77 -0
- package/dist/esm-node/runtime/i18n/utils.js +136 -0
- package/dist/esm-node/runtime/index.js +119 -0
- package/dist/esm-node/runtime/types.js +0 -0
- package/dist/esm-node/runtime/utils.js +82 -0
- package/dist/esm-node/server/index.js +168 -0
- package/dist/esm-node/shared/deepMerge.js +20 -0
- package/dist/esm-node/shared/detection.js +71 -0
- package/dist/esm-node/shared/type.js +0 -0
- package/dist/esm-node/shared/utils.js +35 -0
- package/dist/types/cli/index.d.ts +21 -0
- package/dist/types/runtime/I18nLink.d.ts +8 -0
- package/dist/types/runtime/context.d.ts +38 -0
- package/dist/types/runtime/hooks.d.ts +28 -0
- package/dist/types/runtime/i18n/backend/config.d.ts +2 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +8 -0
- package/dist/types/runtime/i18n/backend/index.d.ts +3 -0
- package/dist/types/runtime/i18n/backend/middleware.common.d.ts +14 -0
- package/dist/types/runtime/i18n/backend/middleware.d.ts +12 -0
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +13 -0
- package/dist/types/runtime/i18n/backend/sdk-backend.d.ts +52 -0
- package/dist/types/runtime/i18n/detection/config.d.ts +11 -0
- package/dist/types/runtime/i18n/detection/index.d.ts +50 -0
- package/dist/types/runtime/i18n/detection/middleware.d.ts +24 -0
- package/dist/types/runtime/i18n/detection/middleware.node.d.ts +17 -0
- package/dist/types/runtime/i18n/index.d.ts +3 -0
- package/dist/types/runtime/i18n/instance.d.ts +93 -0
- package/dist/types/runtime/i18n/utils.d.ts +29 -0
- package/dist/types/runtime/index.d.ts +19 -0
- package/dist/types/runtime/types.d.ts +15 -0
- package/dist/types/runtime/utils.d.ts +33 -0
- package/dist/types/server/index.d.ts +8 -0
- package/dist/types/shared/deepMerge.d.ts +1 -0
- package/dist/types/shared/detection.d.ts +11 -0
- package/dist/types/shared/type.d.ts +156 -0
- package/dist/types/shared/utils.d.ts +5 -0
- package/package.json +100 -34
- package/rslib.config.mts +4 -0
- package/src/cli/index.ts +245 -0
- package/src/runtime/I18nLink.tsx +76 -0
- package/src/runtime/context.tsx +256 -0
- package/src/runtime/hooks.ts +274 -0
- package/src/runtime/i18n/backend/config.ts +10 -0
- package/src/runtime/i18n/backend/defaults.node.ts +31 -0
- package/src/runtime/i18n/backend/defaults.ts +37 -0
- package/src/runtime/i18n/backend/index.ts +181 -0
- package/src/runtime/i18n/backend/middleware.common.ts +116 -0
- package/src/runtime/i18n/backend/middleware.node.ts +32 -0
- package/src/runtime/i18n/backend/middleware.ts +28 -0
- package/src/runtime/i18n/backend/sdk-backend.ts +292 -0
- package/src/runtime/i18n/detection/config.ts +32 -0
- package/src/runtime/i18n/detection/index.ts +641 -0
- package/src/runtime/i18n/detection/middleware.node.ts +84 -0
- package/src/runtime/i18n/detection/middleware.ts +251 -0
- package/src/runtime/i18n/index.ts +8 -0
- package/src/runtime/i18n/instance.ts +227 -0
- package/src/runtime/i18n/utils.ts +333 -0
- package/src/runtime/index.tsx +275 -0
- package/src/runtime/types.ts +17 -0
- package/src/runtime/utils.ts +151 -0
- package/src/server/index.ts +336 -0
- package/src/shared/deepMerge.ts +38 -0
- package/src/shared/detection.ts +131 -0
- package/src/shared/type.ts +170 -0
- package/src/shared/utils.ts +82 -0
- package/tsconfig.json +12 -0
- package/dist/cjs/index.js +0 -73
- package/dist/cjs/languageDetector.js +0 -51
- package/dist/cjs/utils/index.js +0 -39
- package/dist/esm/index.js +0 -61
- package/dist/esm/languageDetector.js +0 -33
- package/dist/esm/utils/index.js +0 -16
- package/dist/esm-node/index.js +0 -49
- package/dist/esm-node/languageDetector.js +0 -26
- package/dist/esm-node/utils/index.js +0 -15
- package/dist/types/index.d.ts +0 -34
- package/dist/types/languageDetector.d.ts +0 -6
- 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
|
+
}
|