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