@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.4 → 3.5.0-ultramodern.41

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 (214) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/cli/locales.js +15 -15
  3. package/dist/cjs/runtime/Link.js +8 -89
  4. package/dist/cjs/runtime/context.js +18 -82
  5. package/dist/cjs/runtime/contextHelpers.js +137 -0
  6. package/dist/cjs/runtime/core.js +29 -132
  7. package/dist/cjs/runtime/i18n/backend/defaults.node.js +7 -7
  8. package/dist/cjs/runtime/i18n/detection/cache.js +150 -0
  9. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  10. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  11. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  12. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  13. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  14. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  15. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  16. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  17. package/dist/cjs/runtime/i18n/instance.js +7 -7
  18. package/dist/cjs/runtime/linkHelpers.js +148 -0
  19. package/dist/cjs/runtime/pluginSetup.js +102 -0
  20. package/dist/cjs/runtime/providerComposition.js +114 -0
  21. package/dist/cjs/runtime/reactI18next.js +43 -0
  22. package/dist/cjs/runtime/utils.js +1 -9
  23. package/dist/cjs/server/apiPrefix.js +72 -0
  24. package/dist/cjs/server/detectorOptions.js +97 -0
  25. package/dist/cjs/server/index.js +17 -144
  26. package/dist/cjs/server/redirectPolicy.js +96 -0
  27. package/dist/cjs/shared/detection.js +2 -104
  28. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  29. package/dist/cjs/shared/localisedUrls/index.js +91 -0
  30. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  31. package/dist/cjs/shared/localisedUrls/pathname.js +68 -0
  32. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  33. package/dist/cjs/shared/localisedUrls/redirect.js +95 -0
  34. package/dist/cjs/shared/localisedUrls/resolve.js +88 -0
  35. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  36. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  37. package/dist/cjs/shared/localisedUrls.js +28 -294
  38. package/dist/cjs/shared/utils.js +2 -8
  39. package/dist/esm/cli/index.mjs +2 -1
  40. package/dist/esm/cli/locales.mjs +13 -13
  41. package/dist/esm/runtime/Link.mjs +1 -82
  42. package/dist/esm/runtime/context.mjs +18 -82
  43. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  44. package/dist/esm/runtime/core.mjs +30 -133
  45. package/dist/esm/runtime/i18n/backend/defaults.node.mjs +5 -5
  46. package/dist/esm/runtime/i18n/detection/cache.mjs +103 -0
  47. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  48. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  49. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  50. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  51. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  52. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  53. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  54. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  55. package/dist/esm/runtime/i18n/instance.mjs +8 -5
  56. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  57. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  58. package/dist/esm/runtime/providerComposition.mjs +76 -0
  59. package/dist/esm/runtime/reactI18next.mjs +5 -0
  60. package/dist/esm/runtime/utils.mjs +2 -10
  61. package/dist/esm/server/apiPrefix.mjs +31 -0
  62. package/dist/esm/server/detectorOptions.mjs +59 -0
  63. package/dist/esm/server/index.mjs +8 -135
  64. package/dist/esm/server/redirectPolicy.mjs +46 -0
  65. package/dist/esm/shared/detection.mjs +1 -71
  66. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  67. package/dist/esm/shared/localisedUrls/index.mjs +7 -0
  68. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  69. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  70. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  71. package/dist/esm/shared/localisedUrls/redirect.mjs +48 -0
  72. package/dist/esm/shared/localisedUrls/resolve.mjs +47 -0
  73. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  74. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  75. package/dist/esm/shared/localisedUrls.mjs +1 -283
  76. package/dist/esm/shared/utils.mjs +1 -1
  77. package/dist/esm-node/cli/index.mjs +2 -1
  78. package/dist/esm-node/cli/locales.mjs +13 -13
  79. package/dist/esm-node/runtime/Link.mjs +1 -82
  80. package/dist/esm-node/runtime/context.mjs +18 -82
  81. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  82. package/dist/esm-node/runtime/core.mjs +30 -133
  83. package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +5 -5
  84. package/dist/esm-node/runtime/i18n/detection/cache.mjs +104 -0
  85. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  86. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  87. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  88. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  89. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  90. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  91. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  92. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  93. package/dist/esm-node/runtime/i18n/instance.mjs +8 -5
  94. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  95. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  96. package/dist/esm-node/runtime/providerComposition.mjs +77 -0
  97. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  98. package/dist/esm-node/runtime/utils.mjs +2 -10
  99. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  100. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  101. package/dist/esm-node/server/index.mjs +8 -135
  102. package/dist/esm-node/server/redirectPolicy.mjs +47 -0
  103. package/dist/esm-node/shared/detection.mjs +1 -71
  104. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  105. package/dist/esm-node/shared/localisedUrls/index.mjs +8 -0
  106. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  107. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  108. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  109. package/dist/esm-node/shared/localisedUrls/redirect.mjs +49 -0
  110. package/dist/esm-node/shared/localisedUrls/resolve.mjs +48 -0
  111. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  112. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  113. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  114. package/dist/esm-node/shared/utils.mjs +1 -1
  115. package/dist/types/cli/locales.d.ts +1 -1
  116. package/dist/types/runtime/Link.d.ts +3 -7
  117. package/dist/types/runtime/context.d.ts +6 -5
  118. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  119. package/dist/types/runtime/core.d.ts +3 -7
  120. package/dist/types/runtime/hooks.d.ts +5 -0
  121. package/dist/types/runtime/i18n/backend/middleware.common.d.ts +2 -1
  122. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  123. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  124. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  125. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  126. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  127. package/dist/types/runtime/i18n/detection/middleware.d.ts +4 -4
  128. package/dist/types/runtime/i18n/detection/middleware.node.d.ts +4 -4
  129. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  130. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  131. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  132. package/dist/types/runtime/i18n/detection/types.d.ts +32 -0
  133. package/dist/types/runtime/i18n/instance.d.ts +28 -26
  134. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  135. package/dist/types/runtime/i18n/utils.d.ts +14 -5
  136. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  137. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  138. package/dist/types/runtime/providerComposition.d.ts +16 -0
  139. package/dist/types/runtime/reactI18next.d.ts +7 -0
  140. package/dist/types/runtime/routerAdapter.d.ts +7 -6
  141. package/dist/types/runtime/utils.d.ts +7 -2
  142. package/dist/types/server/apiPrefix.d.ts +7 -0
  143. package/dist/types/server/detectorOptions.d.ts +21 -0
  144. package/dist/types/server/index.d.ts +1 -6
  145. package/dist/types/server/redirectPolicy.d.ts +22 -0
  146. package/dist/types/shared/detection.d.ts +1 -11
  147. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  148. package/dist/types/shared/localisedUrls/index.d.ts +9 -0
  149. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  150. package/dist/types/shared/localisedUrls/pathname.d.ts +4 -0
  151. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  152. package/dist/types/shared/localisedUrls/redirect.d.ts +11 -0
  153. package/dist/types/shared/localisedUrls/resolve.d.ts +7 -0
  154. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  155. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  156. package/dist/types/shared/localisedUrls.d.ts +2 -36
  157. package/dist/types/shared/utils.d.ts +0 -2
  158. package/package.json +16 -18
  159. package/rstest.config.mts +1 -0
  160. package/src/cli/index.ts +5 -4
  161. package/src/cli/locales.ts +3 -3
  162. package/src/runtime/Link.tsx +11 -138
  163. package/src/runtime/context.tsx +45 -199
  164. package/src/runtime/contextHelpers.ts +232 -0
  165. package/src/runtime/core.tsx +43 -274
  166. package/src/runtime/hooks.ts +13 -2
  167. package/src/runtime/i18n/backend/defaults.node.ts +2 -2
  168. package/src/runtime/i18n/backend/middleware.common.ts +5 -4
  169. package/src/runtime/i18n/detection/cache.ts +189 -0
  170. package/src/runtime/i18n/detection/detector.ts +166 -0
  171. package/src/runtime/i18n/detection/index.ts +10 -641
  172. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  173. package/src/runtime/i18n/detection/language.ts +64 -0
  174. package/src/runtime/i18n/detection/middleware.node.ts +26 -7
  175. package/src/runtime/i18n/detection/middleware.ts +15 -7
  176. package/src/runtime/i18n/detection/path.ts +41 -0
  177. package/src/runtime/i18n/detection/priority.ts +82 -0
  178. package/src/runtime/i18n/detection/ssr.ts +47 -0
  179. package/src/runtime/i18n/detection/types.ts +41 -0
  180. package/src/runtime/i18n/instance.ts +56 -31
  181. package/src/runtime/i18n/react-i18next.ts +1 -6
  182. package/src/runtime/i18n/utils.ts +18 -9
  183. package/src/runtime/linkHelpers.ts +174 -0
  184. package/src/runtime/pluginSetup.ts +189 -0
  185. package/src/runtime/providerComposition.tsx +144 -0
  186. package/src/runtime/reactI18next.ts +20 -0
  187. package/src/runtime/routerAdapter.tsx +6 -6
  188. package/src/runtime/utils.ts +13 -35
  189. package/src/server/apiPrefix.ts +67 -0
  190. package/src/server/detectorOptions.ts +105 -0
  191. package/src/server/index.ts +14 -319
  192. package/src/server/redirectPolicy.ts +127 -0
  193. package/src/shared/detection.ts +1 -1
  194. package/src/shared/localisedUrls/config.ts +18 -0
  195. package/src/shared/localisedUrls/index.ts +24 -0
  196. package/src/shared/localisedUrls/normalise.ts +67 -0
  197. package/src/shared/localisedUrls/pathname.ts +60 -0
  198. package/src/shared/localisedUrls/patterns.ts +160 -0
  199. package/src/shared/localisedUrls/redirect.ts +91 -0
  200. package/src/shared/localisedUrls/resolve.ts +135 -0
  201. package/src/shared/localisedUrls/routes.ts +226 -0
  202. package/src/shared/localisedUrls/types.ts +18 -0
  203. package/src/shared/localisedUrls.ts +26 -623
  204. package/src/shared/utils.ts +2 -2
  205. package/tests/fixtures/localised-routes.golden.json +156 -0
  206. package/tests/link.test.tsx +111 -0
  207. package/tests/linkTypes.test.ts +15 -4
  208. package/tests/localisedUrlRewriteMatrix.fork.test.ts +147 -0
  209. package/tests/localisedUrls.test.ts +129 -2
  210. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  211. package/tests/redirectPolicy.test.ts +43 -0
  212. package/tests/routerAdapter.test.tsx +85 -0
  213. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  214. package/tests/type-fixture/tsconfig.json +1 -0
@@ -1,9 +1,7 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { isBrowser } from "@modern-js/runtime";
3
2
  import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
4
- import { cacheUserLanguage } from "./i18n/detection/index.mjs";
3
+ import { cacheI18nLanguage, changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
5
4
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
6
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
7
5
  const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
8
6
  const getModernI18nContext = ()=>{
9
7
  const globalStore = globalThis;
@@ -17,14 +15,10 @@ const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nCo
17
15
  });
18
16
  const useModernI18n = ()=>{
19
17
  const context = useContext(ModernI18nContext);
20
- if (!context) throw new Error('useModernI18n must be used within a ModernI18nProvider');
18
+ if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
21
19
  const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage } = context;
22
20
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
23
- const pathLanguage = useMemo(()=>{
24
- if (!localePathRedirect || !location?.pathname) return;
25
- const detected = detectLanguageFromPath(location.pathname, languages || [], localePathRedirect);
26
- return detected.detected ? detected.language : void 0;
27
- }, [
21
+ const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
28
22
  languages,
29
23
  localePathRedirect,
30
24
  location?.pathname
@@ -35,56 +29,24 @@ const useModernI18n = ()=>{
35
29
  updateLanguage?.(pathLanguage);
36
30
  i18nInstance?.setLang?.(pathLanguage);
37
31
  i18nInstance?.changeLanguage?.(pathLanguage);
38
- if (isBrowser()) {
39
- const detectionOptions = i18nInstance.options?.detection;
40
- cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
41
- }
32
+ cacheI18nLanguage(i18nInstance, pathLanguage);
42
33
  }, [
43
34
  contextLanguage,
44
35
  i18nInstance,
45
36
  pathLanguage,
46
37
  updateLanguage
47
38
  ]);
48
- const changeLanguage = useCallback(async (newLang)=>{
49
- try {
50
- if (!newLang || 'string' != typeof newLang) throw new Error('Language must be a non-empty string');
51
- await i18nInstance?.setLang?.(newLang);
52
- await i18nInstance?.changeLanguage?.(newLang);
53
- if (isBrowser()) {
54
- const detectionOptions = i18nInstance.options?.detection;
55
- cacheUserLanguage(i18nInstance, newLang, detectionOptions);
56
- }
57
- if (localePathRedirect && isBrowser() && hasRouter && navigate && location) {
58
- const currentPath = location.pathname;
59
- const entryPath = getEntryPath();
60
- const relativePath = currentPath.replace(entryPath, '');
61
- const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
62
- if (pathLanguage.detected && pathLanguage.language === newLang) return;
63
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
64
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
65
- const newUrl = entryPath + newPath + location.search + location.hash;
66
- await navigate(newUrl, {
67
- replace: true
68
- });
69
- }
70
- } else if (localePathRedirect && isBrowser() && !hasRouter) {
71
- const currentPath = window.location.pathname;
72
- const entryPath = getEntryPath();
73
- const relativePath = currentPath.replace(entryPath, '');
74
- const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
75
- if (pathLanguage.detected && pathLanguage.language === newLang) return;
76
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
77
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
78
- const newUrl = entryPath + newPath + window.location.search + window.location.hash;
79
- window.history.pushState(null, '', newUrl);
80
- }
81
- }
82
- if (updateLanguage) updateLanguage(newLang);
83
- } catch (error) {
84
- console.error('Failed to change language:', error);
85
- throw error;
86
- }
87
- }, [
39
+ const changeLanguage = useCallback((newLang)=>changeModernI18nLanguage(newLang, {
40
+ i18nInstance,
41
+ updateLanguage,
42
+ localePathRedirect,
43
+ ignoreRedirectRoutes,
44
+ localisedUrls,
45
+ languages,
46
+ hasRouter,
47
+ navigate,
48
+ location
49
+ }), [
88
50
  i18nInstance,
89
51
  updateLanguage,
90
52
  localePathRedirect,
@@ -95,40 +57,14 @@ const useModernI18n = ()=>{
95
57
  navigate,
96
58
  location
97
59
  ]);
98
- const t = useCallback((key, ...args)=>{
99
- if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
100
- return i18nInstance.t(key, ...args);
101
- }, [
60
+ const t = useCallback((key, ...args)=>translateI18n(i18nInstance, key, ...args), [
102
61
  currentLanguage,
103
62
  i18nInstance
104
63
  ]);
105
- const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
64
+ const isLanguageSupported = useCallback((lang)=>isI18nLanguageSupported(languages, lang), [
106
65
  languages
107
66
  ]);
108
- const isResourcesReady = useMemo(()=>{
109
- if (!i18nInstance?.isInitialized) return false;
110
- const backend = i18nInstance?.services?.backend;
111
- if (backend && 'function' == typeof backend.isLoading) {
112
- const loadingResources = backend.getLoadingResources();
113
- const isCurrentLanguageLoading = loadingResources.some(({ language })=>language === currentLanguage);
114
- if (isCurrentLanguageLoading) return false;
115
- }
116
- const store = i18nInstance.store;
117
- if (!store?.data) return false;
118
- const langData = store.data[currentLanguage];
119
- if (!langData || 'object' != typeof langData) return false;
120
- const options = i18nInstance.options;
121
- const namespaces = options?.ns || options?.defaultNS || [
122
- 'translation'
123
- ];
124
- const requiredNamespaces = Array.isArray(namespaces) ? namespaces : [
125
- namespaces
126
- ];
127
- return requiredNamespaces.every((ns)=>{
128
- const nsData = langData[ns];
129
- return nsData && 'object' == typeof nsData && Object.keys(nsData).length > 0;
130
- });
131
- }, [
67
+ const isResourcesReady = useMemo(()=>isI18nResourcesReady(i18nInstance, currentLanguage), [
132
68
  currentLanguage,
133
69
  i18nInstance
134
70
  ]);
@@ -0,0 +1,84 @@
1
+ import { isBrowser } from "@modern-js/runtime";
2
+ import { cacheUserLanguage } from "./i18n/detection/index.mjs";
3
+ import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
4
+ function getPathLanguage(pathname, languages, localePathRedirect) {
5
+ if (!localePathRedirect || !pathname) return;
6
+ const detected = detectLanguageFromPath(pathname, languages || [], localePathRedirect);
7
+ return detected.detected ? detected.language : void 0;
8
+ }
9
+ function cacheI18nLanguage(i18nInstance, language) {
10
+ if (isBrowser()) {
11
+ const detectionOptions = i18nInstance.options?.detection;
12
+ cacheUserLanguage(i18nInstance, language, detectionOptions);
13
+ }
14
+ }
15
+ async function changeModernI18nLanguage(newLang, options) {
16
+ const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, localisedUrls, languages, hasRouter, navigate, location } = options;
17
+ try {
18
+ if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
19
+ await i18nInstance?.setLang?.(newLang);
20
+ await i18nInstance?.changeLanguage?.(newLang);
21
+ cacheI18nLanguage(i18nInstance, newLang);
22
+ if (localePathRedirect && isBrowser() && hasRouter && navigate && location) {
23
+ const currentPath = location.pathname;
24
+ const entryPath = getEntryPath();
25
+ const relativePath = currentPath.replace(entryPath, '');
26
+ const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
27
+ if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
28
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
29
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
30
+ const newUrl = entryPath + newPath + location.search + location.hash;
31
+ await navigate(newUrl, {
32
+ replace: true
33
+ });
34
+ }
35
+ } else if (localePathRedirect && isBrowser() && !hasRouter) {
36
+ const currentPath = window.location.pathname;
37
+ const entryPath = getEntryPath();
38
+ const relativePath = currentPath.replace(entryPath, '');
39
+ const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
40
+ if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
41
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
42
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
43
+ const newUrl = entryPath + newPath + window.location.search + window.location.hash;
44
+ window.history.pushState(null, '', newUrl);
45
+ }
46
+ }
47
+ if (updateLanguage) updateLanguage(newLang);
48
+ } catch (error) {
49
+ console.error('Failed change language:', error);
50
+ throw error;
51
+ }
52
+ }
53
+ function translateI18n(i18nInstance, key, ...args) {
54
+ if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t required');
55
+ return i18nInstance.t(key, ...args);
56
+ }
57
+ function isI18nLanguageSupported(languages, lang) {
58
+ return languages?.includes(lang) || false;
59
+ }
60
+ function isI18nResourcesReady(i18nInstance, currentLanguage) {
61
+ if (!i18nInstance?.isInitialized) return false;
62
+ const backend = i18nInstance?.services?.backend;
63
+ if (backend && 'function' == typeof backend.isLoading) {
64
+ const loadingResources = backend.getLoadingResources();
65
+ const isCurrentLanguageLoading = loadingResources.some(({ language })=>language === currentLanguage);
66
+ if (isCurrentLanguageLoading) return false;
67
+ }
68
+ const store = i18nInstance.store;
69
+ if (!store?.data) return false;
70
+ const langData = store.data[currentLanguage];
71
+ if (!langData || 'object' != typeof langData) return false;
72
+ const options = i18nInstance.options;
73
+ const namespaces = options?.ns || options?.defaultNS || [
74
+ 'translation'
75
+ ];
76
+ const requiredNamespaces = Array.isArray(namespaces) ? namespaces : [
77
+ namespaces
78
+ ];
79
+ return requiredNamespaces.every((ns)=>{
80
+ const nsData = langData[ns];
81
+ return nsData && 'object' == typeof nsData && Object.keys(nsData).length > 0;
82
+ });
83
+ }
84
+ export { cacheI18nLanguage, changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n };
@@ -1,31 +1,7 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { RuntimeContext, isBrowser } from "@modern-js/runtime";
3
- import { Helmet } from "@modern-js/runtime/head";
4
- import { merge } from "@modern-js/runtime-utils/merge";
5
- import { useContext, useEffect, useMemo, useRef, useState } from "react";
6
- import { ModernI18nProvider, useModernI18n } from "./context.mjs";
7
- import { createContextValue, useClientSideRedirect, useLanguageSync, useSdkResourcesLoader } from "./hooks.mjs";
8
- import { getI18nInstance } from "./i18n/index.mjs";
9
- import { mergeBackendOptions } from "./i18n/backend/index.mjs";
10
- import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOptions } from "./i18n/detection/index.mjs";
11
- import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
12
- import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
13
- import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
1
+ import { setupI18nBeforeRender } from "./pluginSetup.mjs";
2
+ import { createI18nRootWrapper } from "./providerComposition.mjs";
3
+ import { resolveReactI18nextIntegration } from "./reactI18next.mjs";
14
4
  import "./types.mjs";
15
- let i18nLifecycleHelpersPromise;
16
- function loadI18nLifecycleHelpers() {
17
- i18nLifecycleHelpersPromise ??= Promise.all([
18
- import("./i18n/backend/middleware.mjs"),
19
- import("./i18n/utils.mjs")
20
- ]).then(([backendMiddleware, utils])=>({
21
- useI18nextBackend: backendMiddleware.useI18nextBackend,
22
- changeI18nLanguage: utils.changeI18nLanguage,
23
- ensureLanguageMatch: utils.ensureLanguageMatch,
24
- initializeI18nInstance: utils.initializeI18nInstance,
25
- setupClonedInstance: utils.setupClonedInstance
26
- }));
27
- return i18nLifecycleHelpersPromise;
28
- }
29
5
  const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
30
6
  name: '@modern-js/plugin-i18n',
31
7
  setup: (api)=>{
@@ -34,119 +10,40 @@ const createI18nPlugin = (loadReactI18nextIntegration)=>(options)=>({
34
10
  const { enabled: backendEnabled = false } = backend || {};
35
11
  let latestI18nInstance;
36
12
  let I18nextProvider;
37
- const resolveReactI18nextIntegration = async ()=>{
38
- if (!reactI18next) return null;
39
- return loadReactI18nextIntegration?.() ?? null;
40
- };
41
13
  api.onBeforeRender(async (context)=>{
42
- const { useI18nextBackend, changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } = await loadI18nLifecycleHelpers();
43
- let i18nInstance = await getI18nInstance(userI18nInstance);
44
- const { i18n: otherConfig } = api.getRuntimeConfig();
45
- const { initOptions: otherInitOptions } = otherConfig || {};
46
- const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
47
- const reactI18nextIntegration = await resolveReactI18nextIntegration();
48
- I18nextProvider = reactI18nextIntegration?.I18nextProvider ?? null;
49
- if (reactI18nextIntegration?.initReactI18next) i18nInstance.use(reactI18nextIntegration.initReactI18next);
50
- const pathname = getPathname(context);
51
- if (i18nextDetector) useI18nextLanguageDetector(i18nInstance);
52
- const mergedDetection = mergeDetectionOptions(i18nextDetector, detection, localePathRedirect, userInitOptions);
53
- const mergedBackend = mergeBackendOptions(backend, userInitOptions);
54
- const hasSdkConfig = 'function' == typeof userInitOptions?.backend?.sdk || mergedBackend?.sdk && 'function' == typeof mergedBackend.sdk;
55
- if (mergedBackend && (backendEnabled || hasSdkConfig)) useI18nextBackend(i18nInstance, mergedBackend);
56
- const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
57
- languages,
58
- fallbackLanguage,
59
- localePathRedirect,
14
+ latestI18nInstance = await setupI18nBeforeRender(context, {
15
+ api,
16
+ userI18nInstance,
17
+ initOptions,
18
+ backend,
19
+ backendEnabled,
60
20
  i18nextDetector,
61
21
  detection,
62
- userInitOptions,
63
- mergedBackend,
64
- pathname,
65
- ssrContext: context.ssrContext
66
- });
67
- await initializeI18nInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, mergedDetection, mergedBackend, userInitOptions);
68
- if (!isBrowser() && i18nInstance.cloneInstance) {
69
- i18nInstance = i18nInstance.cloneInstance();
70
- await setupClonedInstance(i18nInstance, finalLanguage, fallbackLanguage, languages, backendEnabled, backend, i18nextDetector, detection, localePathRedirect, userInitOptions);
71
- }
72
- if (localePathRedirect) await ensureLanguageMatch(i18nInstance, finalLanguage);
73
- if (!isBrowser()) exportServerLngToWindow(context, finalLanguage);
74
- context.i18nInstance = i18nInstance;
75
- latestI18nInstance = i18nInstance;
76
- context.changeLanguage = async (newLang)=>{
77
- await changeI18nLanguage(i18nInstance, newLang, {
78
- detectionOptions: mergedDetection
79
- });
80
- };
81
- });
82
- api.wrapRoot((App)=>(props)=>{
83
- const runtimeContext = useContext(RuntimeContext);
84
- const i18nInstance = runtimeContext.i18nInstance || latestI18nInstance;
85
- const initialLang = useMemo(()=>i18nInstance?.language || (localeDetection?.fallbackLanguage ?? 'en'), [
86
- i18nInstance?.language,
87
- localeDetection?.fallbackLanguage
88
- ]);
89
- const [lang, setLang] = useState(initialLang);
90
- const [forceUpdate, setForceUpdate] = useState(0);
91
- const prevLangRef = useRef(lang);
92
- const runtimeContextRef = useRef(runtimeContext);
93
- runtimeContextRef.current = runtimeContext;
94
- useEffect(()=>{
95
- if (i18nInstance?.language) {
96
- const translator = i18nInstance.translator;
97
- if (translator) translator.language = i18nInstance.language;
98
- }
99
- }, [
100
- i18nInstance?.language
101
- ]);
102
- useEffect(()=>{
103
- prevLangRef.current = lang;
104
- }, [
105
- lang
106
- ]);
107
- useSdkResourcesLoader(i18nInstance, setForceUpdate);
108
- useLanguageSync(i18nInstance, localePathRedirect, languages, runtimeContextRef, prevLangRef, setLang);
109
- useClientSideRedirect(i18nInstance, localePathRedirect, languages, fallbackLanguage, ignoreRedirectRoutes, localisedUrls);
110
- const contextValue = useMemo(()=>createContextValue(lang, i18nInstance, entryName, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, setLang), [
111
- lang,
112
- i18nInstance,
113
- entryName,
114
- languages,
115
- localePathRedirect,
116
- ignoreRedirectRoutes,
117
- localisedUrls,
118
- forceUpdate
119
- ]);
120
- const children = props.children;
121
- const appContent = /*#__PURE__*/ jsxs(Fragment, {
122
- children: [
123
- Boolean(htmlLangAttr) && /*#__PURE__*/ jsx(Helmet, {
124
- htmlAttributes: {
125
- lang
126
- }
127
- }),
128
- /*#__PURE__*/ jsx(ModernI18nProvider, {
129
- value: contextValue,
130
- children: App ? /*#__PURE__*/ jsx(App, {
131
- ...props,
132
- children: children
133
- }) : children
134
- })
135
- ]
136
- });
137
- if (!i18nInstance) return appContent;
138
- if (I18nextProvider) {
139
- const i18nextInstanceForProvider = getI18nextInstanceForProvider(i18nInstance);
140
- return /*#__PURE__*/ jsx(I18nextProvider, {
141
- i18n: i18nextInstanceForProvider,
142
- children: appContent
143
- });
22
+ localePathRedirect,
23
+ languages,
24
+ fallbackLanguage,
25
+ resolveReactI18nextIntegration: ()=>resolveReactI18nextIntegration(reactI18next, loadReactI18nextIntegration),
26
+ setI18nextProvider: (provider)=>{
27
+ I18nextProvider = provider;
144
28
  }
145
- return appContent;
146
29
  });
30
+ });
31
+ api.wrapRoot(createI18nRootWrapper({
32
+ entryName,
33
+ htmlLangAttr,
34
+ localePathRedirect,
35
+ languages,
36
+ fallbackLanguage,
37
+ ignoreRedirectRoutes,
38
+ localisedUrls,
39
+ getLatestI18nInstance: ()=>latestI18nInstance,
40
+ getI18nextProvider: ()=>I18nextProvider
41
+ }));
147
42
  }
148
43
  });
44
+ export { useModernI18n } from "./context.mjs";
149
45
  export { I18nLink } from "./I18nLink.mjs";
150
46
  export { Link } from "./Link.mjs";
151
47
  export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
152
- export { buildLocalizedUrl, createI18nPlugin, splitUrlTarget, useModernI18n };
48
+ export { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
49
+ export { createI18nPlugin };
@@ -1,18 +1,18 @@
1
- import fs from "fs";
2
- import path_0 from "path";
1
+ import node_fs from "node:fs";
2
+ import node_path from "node:path";
3
3
  const CONVENTIONAL_LOCALES_DIRS = [
4
4
  './locales',
5
5
  './config/public/locales'
6
6
  ];
7
7
  const isDirectory = (dirPath)=>{
8
8
  try {
9
- return fs.statSync(dirPath).isDirectory();
9
+ return node_fs.statSync(dirPath).isDirectory();
10
10
  } catch {
11
11
  return false;
12
12
  }
13
13
  };
14
14
  const resolveDefaultLocalesDir = (cwd = process.cwd())=>{
15
- for (const dir of CONVENTIONAL_LOCALES_DIRS)if (isDirectory(path_0.resolve(cwd, dir))) return dir;
15
+ for (const dir of CONVENTIONAL_LOCALES_DIRS)if (isDirectory(node_path.resolve(cwd, dir))) return dir;
16
16
  return CONVENTIONAL_LOCALES_DIRS[0];
17
17
  };
18
18
  const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
@@ -38,7 +38,7 @@ function getResourceBasePath(resourcePath) {
38
38
  }
39
39
  function pathExists(resourcePath) {
40
40
  try {
41
- return fs.existsSync(getResourceBasePath(resourcePath));
41
+ return node_fs.existsSync(getResourceBasePath(resourcePath));
42
42
  } catch {
43
43
  return false;
44
44
  }
@@ -0,0 +1,103 @@
1
+ const detectorInstanceCache = new WeakMap();
2
+ const DETECTOR_SAFE_OPTION_KEYS = [
3
+ 'lowerCaseLng',
4
+ 'nonExplicitSupportedLngs',
5
+ 'load',
6
+ 'partialBundledLanguages',
7
+ 'returnNull',
8
+ 'returnEmptyString',
9
+ 'returnObjects',
10
+ 'joinArrays',
11
+ 'keySeparator',
12
+ 'nsSeparator',
13
+ 'pluralSeparator',
14
+ 'contextSeparator',
15
+ 'fallbackNS',
16
+ 'ns',
17
+ 'defaultNS',
18
+ 'debug'
19
+ ];
20
+ const stableStringify = (value)=>{
21
+ if (null == value) return JSON.stringify(value);
22
+ if ('object' != typeof value) return JSON.stringify(value);
23
+ if (Array.isArray(value)) return `[${value.map((item)=>stableStringify(item)).join(',')}]`;
24
+ const record = value;
25
+ const sortedKeys = Object.keys(record).sort();
26
+ const sortedEntries = sortedKeys.map((key)=>{
27
+ const stringifiedValue = stableStringify(record[key]);
28
+ return `${JSON.stringify(key)}:${stringifiedValue}`;
29
+ });
30
+ return `{${sortedEntries.join(',')}}`;
31
+ };
32
+ const buildDetectorConfigKey = (languages, fallbackLanguage, mergedDetection)=>stableStringify({
33
+ languages,
34
+ fallbackLanguage,
35
+ detection: mergedDetection
36
+ });
37
+ const pickSafeDetectionOptions = (userInitOptions)=>{
38
+ if (!userInitOptions) return {};
39
+ const safeOptions = {};
40
+ for (const key of DETECTOR_SAFE_OPTION_KEYS){
41
+ const value = userInitOptions[key];
42
+ if (void 0 !== value) safeOptions[key] = value;
43
+ }
44
+ if (userInitOptions.interpolation) safeOptions.interpolation = {
45
+ ...userInitOptions.interpolation
46
+ };
47
+ return safeOptions;
48
+ };
49
+ const cleanupDetectorCacheEntry = (entry)=>{
50
+ if (!entry || !entry.isTemporary) return;
51
+ const instance = entry.instance;
52
+ try {
53
+ instance?.removeAllListeners?.();
54
+ } catch (error) {}
55
+ try {
56
+ instance?.off?.('*');
57
+ } catch (error) {}
58
+ try {
59
+ instance?.services?.backendConnector?.backend?.stop?.();
60
+ } catch (error) {}
61
+ try {
62
+ instance?.services?.backendConnector?.backend?.close?.();
63
+ } catch (error) {}
64
+ };
65
+ const createDetectorInstance = (baseInstance, configKey)=>{
66
+ const cached = detectorInstanceCache.get(baseInstance);
67
+ if (cached && cached.configKey === configKey) return {
68
+ instance: cached.instance,
69
+ isTemporary: cached.isTemporary
70
+ };
71
+ if (cached) {
72
+ cleanupDetectorCacheEntry(cached);
73
+ detectorInstanceCache.delete(baseInstance);
74
+ }
75
+ const createNewInstance = ()=>{
76
+ if ('function' == typeof baseInstance.createInstance) try {
77
+ const created = baseInstance.createInstance();
78
+ if (created) return {
79
+ instance: created,
80
+ isTemporary: true
81
+ };
82
+ } catch (error) {}
83
+ if ('function' == typeof baseInstance.cloneInstance) try {
84
+ const cloned = baseInstance.cloneInstance();
85
+ if (cloned) return {
86
+ instance: cloned,
87
+ isTemporary: true
88
+ };
89
+ } catch (error) {}
90
+ return {
91
+ instance: baseInstance,
92
+ isTemporary: false
93
+ };
94
+ };
95
+ const created = createNewInstance();
96
+ if (created.isTemporary) detectorInstanceCache.set(baseInstance, {
97
+ instance: created.instance,
98
+ isTemporary: true,
99
+ configKey
100
+ });
101
+ return created;
102
+ };
103
+ export { buildDetectorConfigKey, createDetectorInstance, detectorInstanceCache, pickSafeDetectionOptions };
@@ -0,0 +1,62 @@
1
+ import { isBrowser } from "@modern-js/runtime";
2
+ import { buildDetectorConfigKey, createDetectorInstance, detectorInstanceCache, pickSafeDetectionOptions } from "./cache.mjs";
3
+ import { mergeDetectionOptions } from "./initOptions.mjs";
4
+ import { getSupportedLanguage, isLanguageSupported } from "./language.mjs";
5
+ import { detectLanguage, useI18nextLanguageDetector } from "./middleware.mjs";
6
+ const initializeI18nForDetector = async (i18nInstance, options)=>{
7
+ const mergedDetection = mergeDetectionOptions(options.i18nextDetector, options.detection, options.localePathRedirect, options.userInitOptions);
8
+ const configKey = buildDetectorConfigKey(options.languages, options.fallbackLanguage, mergedDetection);
9
+ const { instance, isTemporary } = createDetectorInstance(i18nInstance, configKey);
10
+ const safeUserOptions = pickSafeDetectionOptions(options.userInitOptions);
11
+ const initOptions = {
12
+ ...safeUserOptions,
13
+ fallbackLng: options.fallbackLanguage,
14
+ supportedLngs: options.languages,
15
+ detection: mergedDetection,
16
+ initImmediate: true,
17
+ interpolation: {
18
+ ...safeUserOptions?.interpolation || {},
19
+ escapeValue: safeUserOptions?.interpolation?.escapeValue ?? false
20
+ },
21
+ react: {
22
+ useSuspense: false
23
+ }
24
+ };
25
+ useI18nextLanguageDetector(instance);
26
+ if (instance.isInitialized) {
27
+ if (isTemporary) await instance.init(initOptions);
28
+ } else await instance.init(initOptions);
29
+ return {
30
+ detectorInstance: instance,
31
+ isTemporary
32
+ };
33
+ };
34
+ const detectLanguageFromI18nextDetector = async (i18nInstance, options)=>{
35
+ if (!options.i18nextDetector) return;
36
+ const mergedDetection = mergeDetectionOptions(options.i18nextDetector, options.detection, options.localePathRedirect, options.userInitOptions);
37
+ const { detectorInstance, isTemporary } = await initializeI18nForDetector(i18nInstance, options);
38
+ try {
39
+ const request = options.ssrContext?.request;
40
+ if (!isBrowser() && !request) return;
41
+ const detectorLang = detectLanguage(detectorInstance, request, mergedDetection);
42
+ if (detectorLang) {
43
+ const supportedLang = getSupportedLanguage(detectorLang, options.languages);
44
+ if (supportedLang) return supportedLang;
45
+ }
46
+ if (detectorInstance.isInitialized && detectorInstance.language) {
47
+ const currentLang = detectorInstance.language;
48
+ if (isLanguageSupported(currentLang, options.languages)) return currentLang;
49
+ }
50
+ } catch (error) {} finally{
51
+ if (isTemporary && detectorInstance !== i18nInstance) detectorInstanceCache.set(i18nInstance, {
52
+ instance: detectorInstance,
53
+ isTemporary: true,
54
+ configKey: buildDetectorConfigKey(options.languages, options.fallbackLanguage, mergedDetection)
55
+ });
56
+ else if (detectorInstance === i18nInstance) {
57
+ i18nInstance.isInitialized = false;
58
+ delete i18nInstance.language;
59
+ }
60
+ }
61
+ };
62
+ export { detectLanguageFromI18nextDetector };