@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,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
+ }