@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.
- 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 +172 -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 +129 -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 +129 -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 +20 -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 +281 -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,256 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo } from 'react';
|
|
3
|
+
import type { FC, ReactNode } from 'react';
|
|
4
|
+
import type { I18nInstance } from './i18n';
|
|
5
|
+
import type { SdkBackend } from './i18n/backend/sdk-backend';
|
|
6
|
+
import { cacheUserLanguage } from './i18n/detection';
|
|
7
|
+
import {
|
|
8
|
+
buildLocalizedUrl,
|
|
9
|
+
getEntryPath,
|
|
10
|
+
shouldIgnoreRedirect,
|
|
11
|
+
useRouterHooks,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
export interface ModernI18nContextValue {
|
|
15
|
+
language: string;
|
|
16
|
+
i18nInstance: I18nInstance;
|
|
17
|
+
// Plugin configuration for useModernI18n hook
|
|
18
|
+
entryName?: string;
|
|
19
|
+
languages?: string[];
|
|
20
|
+
localePathRedirect?: boolean;
|
|
21
|
+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
22
|
+
// Callback to update language in context
|
|
23
|
+
updateLanguage?: (newLang: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ModernI18nContext = createContext<ModernI18nContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
export interface ModernI18nProviderProps {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
value: ModernI18nContextValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ModernI18nProvider: FC<ModernI18nProviderProps> = ({
|
|
34
|
+
children,
|
|
35
|
+
value,
|
|
36
|
+
}) => {
|
|
37
|
+
return (
|
|
38
|
+
<ModernI18nContext.Provider value={value}>
|
|
39
|
+
{children}
|
|
40
|
+
</ModernI18nContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface UseModernI18nReturn {
|
|
45
|
+
language: string;
|
|
46
|
+
changeLanguage: (newLang: string) => Promise<void>;
|
|
47
|
+
i18nInstance: I18nInstance;
|
|
48
|
+
supportedLanguages: string[];
|
|
49
|
+
isLanguageSupported: (lang: string) => boolean;
|
|
50
|
+
// Indicates if translation resources for current language are ready to use
|
|
51
|
+
isResourcesReady: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook for accessing i18n functionality in Modern.js applications.
|
|
56
|
+
*
|
|
57
|
+
* This hook provides:
|
|
58
|
+
* - Current language from URL params or i18n context
|
|
59
|
+
* - changeLanguage function that updates both i18n instance and URL
|
|
60
|
+
* - Direct access to the i18n instance
|
|
61
|
+
* - List of supported languages
|
|
62
|
+
* - Helper function to check if a language is supported
|
|
63
|
+
*
|
|
64
|
+
* @param options - Optional configuration to override context settings
|
|
65
|
+
* @returns Object containing i18n functionality and utilities
|
|
66
|
+
*/
|
|
67
|
+
export const useModernI18n = (): UseModernI18nReturn => {
|
|
68
|
+
const context = useContext(ModernI18nContext);
|
|
69
|
+
if (!context) {
|
|
70
|
+
throw new Error('useModernI18n must be used within a ModernI18nProvider');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
language: contextLanguage,
|
|
75
|
+
i18nInstance,
|
|
76
|
+
languages,
|
|
77
|
+
localePathRedirect,
|
|
78
|
+
ignoreRedirectRoutes,
|
|
79
|
+
updateLanguage,
|
|
80
|
+
} = context;
|
|
81
|
+
|
|
82
|
+
// Get router hooks safely
|
|
83
|
+
const { navigate, location, hasRouter } = useRouterHooks();
|
|
84
|
+
|
|
85
|
+
// Get current language from context (which reflects the actual current language)
|
|
86
|
+
// URL params might be stale after language changes, so we prioritize the context language
|
|
87
|
+
const currentLanguage = contextLanguage;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Changes the current language and updates the URL accordingly.
|
|
91
|
+
*
|
|
92
|
+
* This function:
|
|
93
|
+
* 1. Updates the i18n instance language
|
|
94
|
+
* 2. Updates the URL by replacing the language prefix in the current path
|
|
95
|
+
* 3. Triggers a navigation to the new URL
|
|
96
|
+
*
|
|
97
|
+
* @param newLang - The new language code to switch to
|
|
98
|
+
*/
|
|
99
|
+
const changeLanguage = useCallback(
|
|
100
|
+
async (newLang: string) => {
|
|
101
|
+
try {
|
|
102
|
+
// Validate language
|
|
103
|
+
if (!newLang || typeof newLang !== 'string') {
|
|
104
|
+
throw new Error('Language must be a non-empty string');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await i18nInstance?.setLang?.(newLang);
|
|
108
|
+
await i18nInstance?.changeLanguage?.(newLang);
|
|
109
|
+
|
|
110
|
+
if (isBrowser()) {
|
|
111
|
+
const detectionOptions = i18nInstance.options?.detection;
|
|
112
|
+
cacheUserLanguage(i18nInstance, newLang, detectionOptions);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
localePathRedirect &&
|
|
117
|
+
isBrowser() &&
|
|
118
|
+
hasRouter &&
|
|
119
|
+
navigate &&
|
|
120
|
+
location
|
|
121
|
+
) {
|
|
122
|
+
const currentPath = location.pathname;
|
|
123
|
+
const entryPath = getEntryPath();
|
|
124
|
+
const relativePath = currentPath.replace(entryPath, '');
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
!shouldIgnoreRedirect(
|
|
128
|
+
relativePath,
|
|
129
|
+
languages || [],
|
|
130
|
+
ignoreRedirectRoutes,
|
|
131
|
+
)
|
|
132
|
+
) {
|
|
133
|
+
const newPath = buildLocalizedUrl(
|
|
134
|
+
relativePath,
|
|
135
|
+
newLang,
|
|
136
|
+
languages || [],
|
|
137
|
+
);
|
|
138
|
+
const newUrl =
|
|
139
|
+
entryPath + newPath + location.search + location.hash;
|
|
140
|
+
|
|
141
|
+
await navigate(newUrl, { replace: true });
|
|
142
|
+
}
|
|
143
|
+
} else if (localePathRedirect && isBrowser() && !hasRouter) {
|
|
144
|
+
const currentPath = window.location.pathname;
|
|
145
|
+
const entryPath = getEntryPath();
|
|
146
|
+
const relativePath = currentPath.replace(entryPath, '');
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
!shouldIgnoreRedirect(
|
|
150
|
+
relativePath,
|
|
151
|
+
languages || [],
|
|
152
|
+
ignoreRedirectRoutes,
|
|
153
|
+
)
|
|
154
|
+
) {
|
|
155
|
+
const newPath = buildLocalizedUrl(
|
|
156
|
+
relativePath,
|
|
157
|
+
newLang,
|
|
158
|
+
languages || [],
|
|
159
|
+
);
|
|
160
|
+
const newUrl =
|
|
161
|
+
entryPath +
|
|
162
|
+
newPath +
|
|
163
|
+
window.location.search +
|
|
164
|
+
window.location.hash;
|
|
165
|
+
|
|
166
|
+
window.history.pushState(null, '', newUrl);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update language state after URL update
|
|
171
|
+
if (updateLanguage) {
|
|
172
|
+
updateLanguage(newLang);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('Failed to change language:', error);
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[
|
|
180
|
+
i18nInstance,
|
|
181
|
+
updateLanguage,
|
|
182
|
+
localePathRedirect,
|
|
183
|
+
ignoreRedirectRoutes,
|
|
184
|
+
languages,
|
|
185
|
+
hasRouter,
|
|
186
|
+
navigate,
|
|
187
|
+
location,
|
|
188
|
+
],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Helper function to check if a language is supported
|
|
192
|
+
const isLanguageSupported = useCallback(
|
|
193
|
+
(lang: string) => {
|
|
194
|
+
return languages?.includes(lang) || false;
|
|
195
|
+
},
|
|
196
|
+
[languages],
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Check if current language resources are ready
|
|
200
|
+
// This checks if all required namespaces for the current language are loaded
|
|
201
|
+
const isResourcesReady = useMemo(() => {
|
|
202
|
+
if (!i18nInstance?.isInitialized) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Get backend instance
|
|
207
|
+
const backend = i18nInstance?.services?.backend as SdkBackend | undefined;
|
|
208
|
+
|
|
209
|
+
// If using SDK backend, check loading state
|
|
210
|
+
if (backend && typeof backend.isLoading === 'function') {
|
|
211
|
+
// Check if any resource for current language is loading
|
|
212
|
+
const loadingResources = backend.getLoadingResources();
|
|
213
|
+
const isCurrentLanguageLoading = loadingResources.some(
|
|
214
|
+
({ language }) => language === currentLanguage,
|
|
215
|
+
);
|
|
216
|
+
if (isCurrentLanguageLoading) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if resources exist in store
|
|
222
|
+
const store = (i18nInstance as any).store;
|
|
223
|
+
if (!store?.data) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const langData = store.data[currentLanguage];
|
|
228
|
+
if (!langData || typeof langData !== 'object') {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get required namespaces
|
|
233
|
+
const options = i18nInstance.options;
|
|
234
|
+
const namespaces = options?.ns || options?.defaultNS || ['translation'];
|
|
235
|
+
const requiredNamespaces = Array.isArray(namespaces)
|
|
236
|
+
? namespaces
|
|
237
|
+
: [namespaces];
|
|
238
|
+
|
|
239
|
+
// Check if all required namespaces are loaded
|
|
240
|
+
return requiredNamespaces.every(ns => {
|
|
241
|
+
const nsData = langData[ns];
|
|
242
|
+
return (
|
|
243
|
+
nsData && typeof nsData === 'object' && Object.keys(nsData).length > 0
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
}, [currentLanguage, i18nInstance]);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
language: currentLanguage,
|
|
250
|
+
changeLanguage,
|
|
251
|
+
i18nInstance,
|
|
252
|
+
supportedLanguages: languages || [],
|
|
253
|
+
isLanguageSupported,
|
|
254
|
+
isResourcesReady,
|
|
255
|
+
};
|
|
256
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { TRuntimeContext } from '@modern-js/runtime';
|
|
3
|
+
import type React from 'react';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import type { I18nInstance } from './i18n';
|
|
6
|
+
import { cacheUserLanguage } from './i18n/detection';
|
|
7
|
+
import {
|
|
8
|
+
buildLocalizedUrl,
|
|
9
|
+
detectLanguageFromPath,
|
|
10
|
+
getEntryPath,
|
|
11
|
+
getPathname,
|
|
12
|
+
shouldIgnoreRedirect,
|
|
13
|
+
useRouterHooks,
|
|
14
|
+
} from './utils';
|
|
15
|
+
|
|
16
|
+
interface RuntimeContextWithI18n extends TRuntimeContext {
|
|
17
|
+
i18nInstance?: I18nInstance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createMinimalI18nInstance(language: string): I18nInstance {
|
|
21
|
+
const minimalInstance: I18nInstance = {
|
|
22
|
+
language,
|
|
23
|
+
isInitialized: false,
|
|
24
|
+
init: () => Promise.resolve(undefined),
|
|
25
|
+
use: () => {},
|
|
26
|
+
createInstance: () => minimalInstance,
|
|
27
|
+
services: {},
|
|
28
|
+
};
|
|
29
|
+
return minimalInstance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createContextValue(
|
|
33
|
+
lang: string,
|
|
34
|
+
i18nInstance: I18nInstance | undefined,
|
|
35
|
+
entryName: string | undefined,
|
|
36
|
+
languages: string[],
|
|
37
|
+
localePathRedirect: boolean,
|
|
38
|
+
ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
|
|
39
|
+
setLang: (lang: string) => void,
|
|
40
|
+
) {
|
|
41
|
+
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
42
|
+
return {
|
|
43
|
+
language: lang,
|
|
44
|
+
i18nInstance: instance,
|
|
45
|
+
entryName,
|
|
46
|
+
languages,
|
|
47
|
+
localePathRedirect,
|
|
48
|
+
ignoreRedirectRoutes,
|
|
49
|
+
updateLanguage: setLang,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useSdkResourcesLoader(
|
|
54
|
+
i18nInstance: I18nInstance | undefined,
|
|
55
|
+
setForceUpdate: React.Dispatch<React.SetStateAction<number>>,
|
|
56
|
+
) {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!i18nInstance || !isBrowser()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleSdkResourcesLoaded = (event: Event) => {
|
|
63
|
+
const customEvent = event as CustomEvent<{
|
|
64
|
+
language: string;
|
|
65
|
+
namespace: string;
|
|
66
|
+
}>;
|
|
67
|
+
const { language, namespace } = customEvent.detail;
|
|
68
|
+
|
|
69
|
+
const triggerUpdate = (retryCount = 0) => {
|
|
70
|
+
const store = (i18nInstance as any).store;
|
|
71
|
+
const hasResource = store?.data?.[language]?.[namespace];
|
|
72
|
+
|
|
73
|
+
if (hasResource || retryCount >= 10) {
|
|
74
|
+
if (store?.data?.[language]?.[namespace]) {
|
|
75
|
+
if (typeof store.emit === 'function') {
|
|
76
|
+
store.emit('added', language, namespace);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof i18nInstance.emit === 'function') {
|
|
81
|
+
i18nInstance.emit('loaded', { language, namespace });
|
|
82
|
+
i18nInstance.emit('loaded', language, namespace);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof i18nInstance.reloadResources === 'function') {
|
|
86
|
+
i18nInstance
|
|
87
|
+
.reloadResources(language, namespace)
|
|
88
|
+
.then(() => {
|
|
89
|
+
if (typeof i18nInstance.emit === 'function') {
|
|
90
|
+
i18nInstance.emit('loaded', { language, namespace });
|
|
91
|
+
}
|
|
92
|
+
setForceUpdate(prev => prev + 1);
|
|
93
|
+
})
|
|
94
|
+
.catch(() => {
|
|
95
|
+
// Ignore errors from reloadResources
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof i18nInstance.emit === 'function') {
|
|
100
|
+
i18nInstance.emit('languageChanged', language);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setForceUpdate(prev => prev + 1);
|
|
104
|
+
} else {
|
|
105
|
+
setTimeout(() => triggerUpdate(retryCount + 1), 10);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
triggerUpdate();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
window.addEventListener(
|
|
113
|
+
'i18n-sdk-resources-loaded',
|
|
114
|
+
handleSdkResourcesLoaded,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
window.removeEventListener(
|
|
119
|
+
'i18n-sdk-resources-loaded',
|
|
120
|
+
handleSdkResourcesLoaded,
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
}, [i18nInstance, setForceUpdate]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hook to handle client-side redirect for locale path redirect in static deployments
|
|
128
|
+
* This ensures that when users access paths without language prefix, they are redirected
|
|
129
|
+
* to the localized version of the path
|
|
130
|
+
*
|
|
131
|
+
* Note: This hook only runs in CSR (Client-Side Rendering) scenarios.
|
|
132
|
+
* In SSR/SSG scenarios, server-side middleware handles redirects, so this hook is skipped.
|
|
133
|
+
* We use process.env.MODERN_TARGET to ensure this code is only included in browser bundles.
|
|
134
|
+
*/
|
|
135
|
+
export function useClientSideRedirect(
|
|
136
|
+
i18nInstance: I18nInstance | undefined,
|
|
137
|
+
localePathRedirect: boolean,
|
|
138
|
+
languages: string[],
|
|
139
|
+
fallbackLanguage: string,
|
|
140
|
+
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
141
|
+
) {
|
|
142
|
+
const hasRedirectedRef = useRef(false);
|
|
143
|
+
// Get router hooks safely
|
|
144
|
+
const { navigate, location, hasRouter } = useRouterHooks();
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (process.env.MODERN_TARGET !== 'browser') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!localePathRedirect || !i18nInstance) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const ssrData = (window as any)._SSR_DATA;
|
|
156
|
+
if (ssrData) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore errors when checking SSR data
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (hasRedirectedRef.current) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!i18nInstance.isInitialized) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Use router location if available, otherwise fallback to window.location
|
|
172
|
+
const currentPathname =
|
|
173
|
+
hasRouter && location ? location.pathname : window.location.pathname;
|
|
174
|
+
const currentSearch =
|
|
175
|
+
hasRouter && location ? location.search : window.location.search;
|
|
176
|
+
const currentHash =
|
|
177
|
+
hasRouter && location ? location.hash : window.location.hash;
|
|
178
|
+
|
|
179
|
+
const entryPath = getEntryPath();
|
|
180
|
+
const relativePath = currentPathname.replace(entryPath, '');
|
|
181
|
+
|
|
182
|
+
if (shouldIgnoreRedirect(relativePath, languages, ignoreRedirectRoutes)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pathDetection = detectLanguageFromPath(
|
|
187
|
+
currentPathname,
|
|
188
|
+
languages,
|
|
189
|
+
localePathRedirect,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (pathDetection.detected) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const targetLanguage =
|
|
197
|
+
i18nInstance.language || fallbackLanguage || languages[0] || 'en';
|
|
198
|
+
|
|
199
|
+
const newPath = buildLocalizedUrl(relativePath, targetLanguage, languages);
|
|
200
|
+
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
201
|
+
|
|
202
|
+
if (newUrl !== currentPathname + currentSearch + currentHash) {
|
|
203
|
+
hasRedirectedRef.current = true;
|
|
204
|
+
|
|
205
|
+
// Use navigate if router is available (similar to changeLanguage implementation)
|
|
206
|
+
if (hasRouter && navigate && location) {
|
|
207
|
+
navigate(newUrl, { replace: true });
|
|
208
|
+
} else {
|
|
209
|
+
// Fallback to window.location.replace for non-router scenarios
|
|
210
|
+
// This ensures the new URL is properly recognized and translations are reloaded
|
|
211
|
+
window.location.replace(newUrl);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, [
|
|
215
|
+
navigate,
|
|
216
|
+
location,
|
|
217
|
+
hasRouter,
|
|
218
|
+
localePathRedirect,
|
|
219
|
+
i18nInstance,
|
|
220
|
+
languages,
|
|
221
|
+
fallbackLanguage,
|
|
222
|
+
ignoreRedirectRoutes,
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function useLanguageSync(
|
|
227
|
+
i18nInstance: I18nInstance | undefined,
|
|
228
|
+
localePathRedirect: boolean,
|
|
229
|
+
languages: string[],
|
|
230
|
+
runtimeContextRef: React.MutableRefObject<RuntimeContextWithI18n>,
|
|
231
|
+
prevLangRef: React.MutableRefObject<string>,
|
|
232
|
+
setLang: (lang: string) => void,
|
|
233
|
+
) {
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!i18nInstance) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (localePathRedirect) {
|
|
240
|
+
const currentPathname = getPathname(runtimeContextRef.current);
|
|
241
|
+
const pathDetection = detectLanguageFromPath(
|
|
242
|
+
currentPathname,
|
|
243
|
+
languages,
|
|
244
|
+
localePathRedirect,
|
|
245
|
+
);
|
|
246
|
+
if (pathDetection.detected && pathDetection.language) {
|
|
247
|
+
const currentLang = pathDetection.language;
|
|
248
|
+
if (currentLang !== prevLangRef.current) {
|
|
249
|
+
prevLangRef.current = currentLang;
|
|
250
|
+
setLang(currentLang);
|
|
251
|
+
i18nInstance.setLang?.(currentLang);
|
|
252
|
+
i18nInstance.changeLanguage?.(currentLang);
|
|
253
|
+
if (isBrowser()) {
|
|
254
|
+
const detectionOptions = i18nInstance.options?.detection;
|
|
255
|
+
cacheUserLanguage(i18nInstance, currentLang, detectionOptions);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
const instanceLang = i18nInstance.language;
|
|
261
|
+
if (instanceLang && instanceLang !== prevLangRef.current) {
|
|
262
|
+
prevLangRef.current = instanceLang;
|
|
263
|
+
setLang(instanceLang);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, [
|
|
267
|
+
i18nInstance,
|
|
268
|
+
localePathRedirect,
|
|
269
|
+
languages,
|
|
270
|
+
runtimeContextRef,
|
|
271
|
+
prevLangRef,
|
|
272
|
+
setLang,
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { deepMerge } from '../../../shared/deepMerge';
|
|
2
|
+
import type { BackendOptions } from '../instance';
|
|
3
|
+
|
|
4
|
+
export function mergeBackendOptions(
|
|
5
|
+
defaultOptions: BackendOptions,
|
|
6
|
+
cliOptions: BackendOptions = {},
|
|
7
|
+
userOptions?: BackendOptions,
|
|
8
|
+
): BackendOptions {
|
|
9
|
+
return deepMerge(deepMerge(defaultOptions, cliOptions), userOptions ?? {});
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
2
|
+
loadPath: './locales/{{lng}}/{{ns}}.json',
|
|
3
|
+
addPath: './locales/{{lng}}/{{ns}}.json',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
function convertPath(path: string | undefined): string | undefined {
|
|
7
|
+
if (!path) {
|
|
8
|
+
return path;
|
|
9
|
+
}
|
|
10
|
+
// If it's an absolute path (starts with /), convert to relative path
|
|
11
|
+
if (path.startsWith('/')) {
|
|
12
|
+
return `.${path}`;
|
|
13
|
+
}
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function convertBackendOptions<
|
|
18
|
+
T extends { loadPath?: string; addPath?: string },
|
|
19
|
+
>(options: T): T {
|
|
20
|
+
if (!options) {
|
|
21
|
+
return options;
|
|
22
|
+
}
|
|
23
|
+
const converted = { ...options };
|
|
24
|
+
if (converted.loadPath) {
|
|
25
|
+
converted.loadPath = convertPath(converted.loadPath);
|
|
26
|
+
}
|
|
27
|
+
if (converted.addPath) {
|
|
28
|
+
converted.addPath = convertPath(converted.addPath);
|
|
29
|
+
}
|
|
30
|
+
return converted;
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
2
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
|
3
|
+
addPath: '/locales/{{lng}}/{{ns}}.json',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
__assetPrefix__?: string;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function convertPath(path: string | undefined): string | undefined {
|
|
13
|
+
if (!path) {
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
// If it's an absolute path (starts with /), convert to relative path
|
|
17
|
+
if (path.startsWith('/')) {
|
|
18
|
+
return `${window.__assetPrefix__ || ''}${path}`;
|
|
19
|
+
}
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function convertBackendOptions<
|
|
24
|
+
T extends { loadPath?: string; addPath?: string },
|
|
25
|
+
>(options: T): T {
|
|
26
|
+
if (!options) {
|
|
27
|
+
return options;
|
|
28
|
+
}
|
|
29
|
+
const converted = { ...options };
|
|
30
|
+
if (converted.loadPath) {
|
|
31
|
+
converted.loadPath = convertPath(converted.loadPath);
|
|
32
|
+
}
|
|
33
|
+
if (converted.addPath) {
|
|
34
|
+
converted.addPath = convertPath(converted.addPath);
|
|
35
|
+
}
|
|
36
|
+
return converted;
|
|
37
|
+
}
|