@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.9 → 3.5.0-ultramodern.91

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