@bleedingdev/modern-js-plugin-i18n 3.5.0-ultramodern.3 → 3.5.0-ultramodern.31

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 (168) hide show
  1. package/dist/cjs/cli/index.js +2 -1
  2. package/dist/cjs/runtime/Link.js +8 -89
  3. package/dist/cjs/runtime/context.js +18 -82
  4. package/dist/cjs/runtime/contextHelpers.js +137 -0
  5. package/dist/cjs/runtime/core.js +29 -132
  6. package/dist/cjs/runtime/i18n/detection/cache.js +149 -0
  7. package/dist/cjs/runtime/i18n/detection/detector.js +100 -0
  8. package/dist/cjs/runtime/i18n/detection/index.js +8 -263
  9. package/dist/cjs/runtime/i18n/detection/initOptions.js +69 -0
  10. package/dist/cjs/runtime/i18n/detection/language.js +62 -0
  11. package/dist/cjs/runtime/i18n/detection/path.js +49 -0
  12. package/dist/cjs/runtime/i18n/detection/priority.js +66 -0
  13. package/dist/cjs/runtime/i18n/detection/ssr.js +70 -0
  14. package/dist/cjs/runtime/i18n/detection/types.js +18 -0
  15. package/dist/cjs/runtime/linkHelpers.js +148 -0
  16. package/dist/cjs/runtime/pluginSetup.js +102 -0
  17. package/dist/cjs/runtime/providerComposition.js +113 -0
  18. package/dist/cjs/runtime/reactI18next.js +43 -0
  19. package/dist/cjs/server/apiPrefix.js +72 -0
  20. package/dist/cjs/server/detectorOptions.js +97 -0
  21. package/dist/cjs/server/index.js +17 -144
  22. package/dist/cjs/server/redirectPolicy.js +102 -0
  23. package/dist/cjs/shared/localisedUrls/config.js +49 -0
  24. package/dist/cjs/shared/localisedUrls/index.js +75 -0
  25. package/dist/cjs/shared/localisedUrls/normalise.js +87 -0
  26. package/dist/cjs/shared/localisedUrls/pathname.js +65 -0
  27. package/dist/cjs/shared/localisedUrls/patterns.js +130 -0
  28. package/dist/cjs/shared/localisedUrls/resolve.js +91 -0
  29. package/dist/cjs/shared/localisedUrls/routes.js +129 -0
  30. package/dist/cjs/shared/localisedUrls/types.js +18 -0
  31. package/dist/cjs/shared/localisedUrls.js +13 -294
  32. package/dist/esm/cli/index.mjs +2 -1
  33. package/dist/esm/runtime/Link.mjs +1 -82
  34. package/dist/esm/runtime/context.mjs +18 -82
  35. package/dist/esm/runtime/contextHelpers.mjs +84 -0
  36. package/dist/esm/runtime/core.mjs +30 -133
  37. package/dist/esm/runtime/i18n/detection/cache.mjs +102 -0
  38. package/dist/esm/runtime/i18n/detection/detector.mjs +62 -0
  39. package/dist/esm/runtime/i18n/detection/index.mjs +4 -260
  40. package/dist/esm/runtime/i18n/detection/initOptions.mjs +28 -0
  41. package/dist/esm/runtime/i18n/detection/language.mjs +21 -0
  42. package/dist/esm/runtime/i18n/detection/path.mjs +11 -0
  43. package/dist/esm/runtime/i18n/detection/priority.mjs +28 -0
  44. package/dist/esm/runtime/i18n/detection/ssr.mjs +26 -0
  45. package/dist/esm/runtime/i18n/detection/types.mjs +0 -0
  46. package/dist/esm/runtime/linkHelpers.mjs +98 -0
  47. package/dist/esm/runtime/pluginSetup.mjs +64 -0
  48. package/dist/esm/runtime/providerComposition.mjs +75 -0
  49. package/dist/esm/runtime/reactI18next.mjs +5 -0
  50. package/dist/esm/server/apiPrefix.mjs +31 -0
  51. package/dist/esm/server/detectorOptions.mjs +59 -0
  52. package/dist/esm/server/index.mjs +6 -133
  53. package/dist/esm/server/redirectPolicy.mjs +52 -0
  54. package/dist/esm/shared/localisedUrls/config.mjs +11 -0
  55. package/dist/esm/shared/localisedUrls/index.mjs +6 -0
  56. package/dist/esm/shared/localisedUrls/normalise.mjs +37 -0
  57. package/dist/esm/shared/localisedUrls/pathname.mjs +24 -0
  58. package/dist/esm/shared/localisedUrls/patterns.mjs +86 -0
  59. package/dist/esm/shared/localisedUrls/resolve.mjs +50 -0
  60. package/dist/esm/shared/localisedUrls/routes.mjs +88 -0
  61. package/dist/esm/shared/localisedUrls/types.mjs +0 -0
  62. package/dist/esm/shared/localisedUrls.mjs +1 -283
  63. package/dist/esm-node/cli/index.mjs +2 -1
  64. package/dist/esm-node/runtime/Link.mjs +1 -82
  65. package/dist/esm-node/runtime/context.mjs +18 -82
  66. package/dist/esm-node/runtime/contextHelpers.mjs +85 -0
  67. package/dist/esm-node/runtime/core.mjs +30 -133
  68. package/dist/esm-node/runtime/i18n/detection/cache.mjs +103 -0
  69. package/dist/esm-node/runtime/i18n/detection/detector.mjs +63 -0
  70. package/dist/esm-node/runtime/i18n/detection/index.mjs +4 -260
  71. package/dist/esm-node/runtime/i18n/detection/initOptions.mjs +29 -0
  72. package/dist/esm-node/runtime/i18n/detection/language.mjs +22 -0
  73. package/dist/esm-node/runtime/i18n/detection/path.mjs +12 -0
  74. package/dist/esm-node/runtime/i18n/detection/priority.mjs +29 -0
  75. package/dist/esm-node/runtime/i18n/detection/ssr.mjs +27 -0
  76. package/dist/esm-node/runtime/i18n/detection/types.mjs +1 -0
  77. package/dist/esm-node/runtime/linkHelpers.mjs +99 -0
  78. package/dist/esm-node/runtime/pluginSetup.mjs +65 -0
  79. package/dist/esm-node/runtime/providerComposition.mjs +76 -0
  80. package/dist/esm-node/runtime/reactI18next.mjs +6 -0
  81. package/dist/esm-node/server/apiPrefix.mjs +32 -0
  82. package/dist/esm-node/server/detectorOptions.mjs +60 -0
  83. package/dist/esm-node/server/index.mjs +6 -133
  84. package/dist/esm-node/server/redirectPolicy.mjs +53 -0
  85. package/dist/esm-node/shared/localisedUrls/config.mjs +12 -0
  86. package/dist/esm-node/shared/localisedUrls/index.mjs +7 -0
  87. package/dist/esm-node/shared/localisedUrls/normalise.mjs +38 -0
  88. package/dist/esm-node/shared/localisedUrls/pathname.mjs +25 -0
  89. package/dist/esm-node/shared/localisedUrls/patterns.mjs +87 -0
  90. package/dist/esm-node/shared/localisedUrls/resolve.mjs +51 -0
  91. package/dist/esm-node/shared/localisedUrls/routes.mjs +89 -0
  92. package/dist/esm-node/shared/localisedUrls/types.mjs +1 -0
  93. package/dist/esm-node/shared/localisedUrls.mjs +1 -283
  94. package/dist/types/runtime/Link.d.ts +3 -7
  95. package/dist/types/runtime/context.d.ts +2 -2
  96. package/dist/types/runtime/contextHelpers.d.ts +28 -0
  97. package/dist/types/runtime/core.d.ts +3 -7
  98. package/dist/types/runtime/hooks.d.ts +5 -0
  99. package/dist/types/runtime/i18n/detection/cache.d.ts +14 -0
  100. package/dist/types/runtime/i18n/detection/detector.d.ts +8 -0
  101. package/dist/types/runtime/i18n/detection/index.d.ts +5 -50
  102. package/dist/types/runtime/i18n/detection/initOptions.d.ts +10 -0
  103. package/dist/types/runtime/i18n/detection/language.d.ts +11 -0
  104. package/dist/types/runtime/i18n/detection/path.d.ts +5 -0
  105. package/dist/types/runtime/i18n/detection/priority.d.ts +10 -0
  106. package/dist/types/runtime/i18n/detection/ssr.d.ts +9 -0
  107. package/dist/types/runtime/i18n/detection/types.d.ts +29 -0
  108. package/dist/types/runtime/i18n/react-i18next.d.ts +1 -6
  109. package/dist/types/runtime/linkHelpers.d.ts +17 -0
  110. package/dist/types/runtime/pluginSetup.d.ts +32 -0
  111. package/dist/types/runtime/providerComposition.d.ts +15 -0
  112. package/dist/types/runtime/reactI18next.d.ts +7 -0
  113. package/dist/types/runtime/utils.d.ts +7 -2
  114. package/dist/types/server/apiPrefix.d.ts +7 -0
  115. package/dist/types/server/detectorOptions.d.ts +21 -0
  116. package/dist/types/server/index.d.ts +1 -6
  117. package/dist/types/server/redirectPolicy.d.ts +16 -0
  118. package/dist/types/shared/localisedUrls/config.d.ts +9 -0
  119. package/dist/types/shared/localisedUrls/index.d.ts +7 -0
  120. package/dist/types/shared/localisedUrls/normalise.d.ts +10 -0
  121. package/dist/types/shared/localisedUrls/pathname.d.ts +3 -0
  122. package/dist/types/shared/localisedUrls/patterns.d.ts +5 -0
  123. package/dist/types/shared/localisedUrls/resolve.d.ts +8 -0
  124. package/dist/types/shared/localisedUrls/routes.d.ts +3 -0
  125. package/dist/types/shared/localisedUrls/types.d.ts +14 -0
  126. package/dist/types/shared/localisedUrls.d.ts +2 -36
  127. package/package.json +10 -10
  128. package/src/cli/index.ts +3 -2
  129. package/src/runtime/Link.tsx +11 -138
  130. package/src/runtime/context.tsx +42 -196
  131. package/src/runtime/contextHelpers.ts +232 -0
  132. package/src/runtime/core.tsx +43 -274
  133. package/src/runtime/hooks.ts +3 -0
  134. package/src/runtime/i18n/detection/cache.ts +175 -0
  135. package/src/runtime/i18n/detection/detector.ts +157 -0
  136. package/src/runtime/i18n/detection/index.ts +10 -641
  137. package/src/runtime/i18n/detection/initOptions.ts +74 -0
  138. package/src/runtime/i18n/detection/language.ts +64 -0
  139. package/src/runtime/i18n/detection/path.ts +41 -0
  140. package/src/runtime/i18n/detection/priority.ts +82 -0
  141. package/src/runtime/i18n/detection/ssr.ts +47 -0
  142. package/src/runtime/i18n/detection/types.ts +33 -0
  143. package/src/runtime/i18n/react-i18next.ts +1 -6
  144. package/src/runtime/linkHelpers.ts +174 -0
  145. package/src/runtime/pluginSetup.ts +189 -0
  146. package/src/runtime/providerComposition.tsx +142 -0
  147. package/src/runtime/reactI18next.ts +20 -0
  148. package/src/runtime/utils.ts +7 -5
  149. package/src/server/apiPrefix.ts +67 -0
  150. package/src/server/detectorOptions.ts +105 -0
  151. package/src/server/index.ts +14 -319
  152. package/src/server/redirectPolicy.ts +152 -0
  153. package/src/shared/localisedUrls/config.ts +18 -0
  154. package/src/shared/localisedUrls/index.ts +13 -0
  155. package/src/shared/localisedUrls/normalise.ts +67 -0
  156. package/src/shared/localisedUrls/pathname.ts +57 -0
  157. package/src/shared/localisedUrls/patterns.ts +160 -0
  158. package/src/shared/localisedUrls/resolve.ts +118 -0
  159. package/src/shared/localisedUrls/routes.ts +226 -0
  160. package/src/shared/localisedUrls/types.ts +18 -0
  161. package/src/shared/localisedUrls.ts +20 -623
  162. package/tests/link.test.tsx +111 -0
  163. package/tests/linkTypes.test.ts +15 -4
  164. package/tests/localisedUrls.test.ts +54 -2
  165. package/tests/reactI18nextRuntimeBoundary.test.ts +6 -3
  166. package/tests/routerAdapter.test.tsx +56 -0
  167. package/tests/type-fixture/fixture-globals.d.ts +11 -0
  168. package/tests/type-fixture/tsconfig.json +1 -0
@@ -2,92 +2,11 @@ import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { useModernI18n } from "./context.mjs";
5
+ import { interpolateRouteParams, mergeClassNames, normalizeSearch, splitActiveProps, warnOnce } from "./linkHelpers.mjs";
5
6
  import { canonicalPath } from "./localizedPaths.mjs";
6
7
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
8
  import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
8
9
  const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
9
- const warnedTargets = new Set();
10
- const warnOnce = (key, message)=>{
11
- if ('development' !== process.env.NODE_ENV || warnedTargets.has(key)) return;
12
- warnedTargets.add(key);
13
- console.warn(message);
14
- };
15
- const interpolateRouteParams = (pathname, params)=>{
16
- if (!/[$:*{]/.test(pathname)) return pathname;
17
- const resolveParam = (name)=>{
18
- const value = params?.[name];
19
- return void 0 === value ? void 0 : String(value);
20
- };
21
- const segments = pathname.split('/').map((segment)=>{
22
- if (!segment) return segment;
23
- if (segment.startsWith('{-$') && segment.endsWith('}')) {
24
- const value = resolveParam(segment.slice(3, -1));
25
- return void 0 === value ? null : encodeURIComponent(value);
26
- }
27
- if ('$' === segment || '*' === segment) {
28
- const value = resolveParam('_splat') ?? resolveParam('*');
29
- return void 0 === value ? null : value.split('/').map(encodeURIComponent).join('/');
30
- }
31
- if (segment.startsWith('$')) {
32
- const value = resolveParam(segment.slice(1));
33
- if (void 0 === value) {
34
- warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`);
35
- return segment;
36
- }
37
- return encodeURIComponent(value);
38
- }
39
- if (segment.startsWith(':')) {
40
- const optional = segment.endsWith('?');
41
- const name = segment.slice(1, optional ? -1 : void 0);
42
- const value = resolveParam(name);
43
- if (void 0 === value) {
44
- if (optional) return null;
45
- warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`);
46
- return segment;
47
- }
48
- return encodeURIComponent(value);
49
- }
50
- return segment;
51
- }).filter((segment)=>null !== segment);
52
- return segments.join('/') || '/';
53
- };
54
- const normalizeSearch = (search, searchFromTo)=>{
55
- if (search && 'object' == typeof search) {
56
- const entries = Object.entries(search).filter(([, value])=>null != value);
57
- const searchObject = Object.fromEntries(entries.map(([key, value])=>[
58
- key,
59
- String(value)
60
- ]));
61
- const params = new URLSearchParams(searchObject);
62
- const serialized = params.toString();
63
- return {
64
- searchString: serialized ? `?${serialized}` : '',
65
- searchObject
66
- };
67
- }
68
- const raw = 'string' == typeof search && search ? search : searchFromTo;
69
- if (!raw) return {
70
- searchString: '',
71
- searchObject: void 0
72
- };
73
- const searchString = raw.startsWith('?') ? raw : `?${raw}`;
74
- const searchObject = {};
75
- new URLSearchParams(searchString).forEach((value, key)=>{
76
- searchObject[key] = value;
77
- });
78
- return {
79
- searchString,
80
- searchObject
81
- };
82
- };
83
- const splitActiveProps = (active, activeProps)=>{
84
- if (!active || !activeProps) return {};
85
- return activeProps;
86
- };
87
- const mergeClassNames = (...values)=>{
88
- const classNames = values.filter((value)=>'string' == typeof value && value.length > 0);
89
- return classNames.length > 0 ? classNames.join(' ') : void 0;
90
- };
91
10
  const Link = (props)=>{
92
11
  const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, prefetch, preload, ...rest } = props;
93
12
  const adapter = useI18nRouterAdapter();
@@ -1,10 +1,8 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { isBrowser } from "@modern-js/runtime";
4
3
  import { createContext, useCallback, useContext, useEffect, useMemo } from "react";
5
- import { cacheUserLanguage } from "./i18n/detection/index.mjs";
4
+ import { cacheI18nLanguage, changeModernI18nLanguage, getPathLanguage, isI18nLanguageSupported, isI18nResourcesReady, translateI18n } from "./contextHelpers.mjs";
6
5
  import { useI18nRouterAdapter } from "./routerAdapter.mjs";
7
- import { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, shouldIgnoreRedirect } from "./utils.mjs";
8
6
  const modernI18nContextKey = Symbol.for('@modern-js/plugin-i18n/runtime/ModernI18nContext');
9
7
  const getModernI18nContext = ()=>{
10
8
  const globalStore = globalThis;
@@ -18,14 +16,10 @@ const ModernI18nProvider = ({ children, value })=>/*#__PURE__*/ jsx(ModernI18nCo
18
16
  });
19
17
  const useModernI18n = ()=>{
20
18
  const context = useContext(ModernI18nContext);
21
- if (!context) throw new Error('useModernI18n must be used within a ModernI18nProvider');
19
+ if (!context) throw new Error('useModernI18n must be used within ModernI18nProvider');
22
20
  const { language: contextLanguage, i18nInstance, languages, localePathRedirect, ignoreRedirectRoutes, localisedUrls, updateLanguage } = context;
23
21
  const { navigate, location, hasRouter } = useI18nRouterAdapter();
24
- const pathLanguage = useMemo(()=>{
25
- if (!localePathRedirect || !location?.pathname) return;
26
- const detected = detectLanguageFromPath(location.pathname, languages || [], localePathRedirect);
27
- return detected.detected ? detected.language : void 0;
28
- }, [
22
+ const pathLanguage = useMemo(()=>getPathLanguage(location?.pathname, languages, localePathRedirect), [
29
23
  languages,
30
24
  localePathRedirect,
31
25
  location?.pathname
@@ -36,56 +30,24 @@ const useModernI18n = ()=>{
36
30
  updateLanguage?.(pathLanguage);
37
31
  i18nInstance?.setLang?.(pathLanguage);
38
32
  i18nInstance?.changeLanguage?.(pathLanguage);
39
- if (isBrowser()) {
40
- const detectionOptions = i18nInstance.options?.detection;
41
- cacheUserLanguage(i18nInstance, pathLanguage, detectionOptions);
42
- }
33
+ cacheI18nLanguage(i18nInstance, pathLanguage);
43
34
  }, [
44
35
  contextLanguage,
45
36
  i18nInstance,
46
37
  pathLanguage,
47
38
  updateLanguage
48
39
  ]);
49
- const changeLanguage = useCallback(async (newLang)=>{
50
- try {
51
- if (!newLang || 'string' != typeof newLang) throw new Error('Language must be a non-empty string');
52
- await i18nInstance?.setLang?.(newLang);
53
- await i18nInstance?.changeLanguage?.(newLang);
54
- if (isBrowser()) {
55
- const detectionOptions = i18nInstance.options?.detection;
56
- cacheUserLanguage(i18nInstance, newLang, detectionOptions);
57
- }
58
- if (localePathRedirect && isBrowser() && hasRouter && navigate && location) {
59
- const currentPath = location.pathname;
60
- const entryPath = getEntryPath();
61
- const relativePath = currentPath.replace(entryPath, '');
62
- const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
63
- if (pathLanguage.detected && pathLanguage.language === newLang) return;
64
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
65
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
66
- const newUrl = entryPath + newPath + location.search + location.hash;
67
- await navigate(newUrl, {
68
- replace: true
69
- });
70
- }
71
- } else if (localePathRedirect && isBrowser() && !hasRouter) {
72
- const currentPath = window.location.pathname;
73
- const entryPath = getEntryPath();
74
- const relativePath = currentPath.replace(entryPath, '');
75
- const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
76
- if (pathLanguage.detected && pathLanguage.language === newLang) return;
77
- if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
78
- const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
79
- const newUrl = entryPath + newPath + window.location.search + window.location.hash;
80
- window.history.pushState(null, '', newUrl);
81
- }
82
- }
83
- if (updateLanguage) updateLanguage(newLang);
84
- } catch (error) {
85
- console.error('Failed to change language:', error);
86
- throw error;
87
- }
88
- }, [
40
+ const changeLanguage = useCallback((newLang)=>changeModernI18nLanguage(newLang, {
41
+ i18nInstance,
42
+ updateLanguage,
43
+ localePathRedirect,
44
+ ignoreRedirectRoutes,
45
+ localisedUrls,
46
+ languages,
47
+ hasRouter,
48
+ navigate,
49
+ location
50
+ }), [
89
51
  i18nInstance,
90
52
  updateLanguage,
91
53
  localePathRedirect,
@@ -96,40 +58,14 @@ const useModernI18n = ()=>{
96
58
  navigate,
97
59
  location
98
60
  ]);
99
- const t = useCallback((key, ...args)=>{
100
- if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t is required');
101
- return i18nInstance.t(key, ...args);
102
- }, [
61
+ const t = useCallback((key, ...args)=>translateI18n(i18nInstance, key, ...args), [
103
62
  currentLanguage,
104
63
  i18nInstance
105
64
  ]);
106
- const isLanguageSupported = useCallback((lang)=>languages?.includes(lang) || false, [
65
+ const isLanguageSupported = useCallback((lang)=>isI18nLanguageSupported(languages, lang), [
107
66
  languages
108
67
  ]);
109
- const isResourcesReady = useMemo(()=>{
110
- if (!i18nInstance?.isInitialized) return false;
111
- const backend = i18nInstance?.services?.backend;
112
- if (backend && 'function' == typeof backend.isLoading) {
113
- const loadingResources = backend.getLoadingResources();
114
- const isCurrentLanguageLoading = loadingResources.some(({ language })=>language === currentLanguage);
115
- if (isCurrentLanguageLoading) return false;
116
- }
117
- const store = i18nInstance.store;
118
- if (!store?.data) return false;
119
- const langData = store.data[currentLanguage];
120
- if (!langData || 'object' != typeof langData) return false;
121
- const options = i18nInstance.options;
122
- const namespaces = options?.ns || options?.defaultNS || [
123
- 'translation'
124
- ];
125
- const requiredNamespaces = Array.isArray(namespaces) ? namespaces : [
126
- namespaces
127
- ];
128
- return requiredNamespaces.every((ns)=>{
129
- const nsData = langData[ns];
130
- return nsData && 'object' == typeof nsData && Object.keys(nsData).length > 0;
131
- });
132
- }, [
68
+ const isResourcesReady = useMemo(()=>isI18nResourcesReady(i18nInstance, currentLanguage), [
133
69
  currentLanguage,
134
70
  i18nInstance
135
71
  ]);
@@ -0,0 +1,85 @@
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 changeModernI18nLanguage(newLang, options) {
17
+ const { i18nInstance, updateLanguage, localePathRedirect, ignoreRedirectRoutes, localisedUrls, languages, hasRouter, navigate, location } = options;
18
+ try {
19
+ if (!newLang || 'string' != typeof newLang) throw new Error('Language must be non-empty string');
20
+ await i18nInstance?.setLang?.(newLang);
21
+ await i18nInstance?.changeLanguage?.(newLang);
22
+ cacheI18nLanguage(i18nInstance, newLang);
23
+ if (localePathRedirect && isBrowser() && hasRouter && navigate && location) {
24
+ const currentPath = location.pathname;
25
+ const entryPath = getEntryPath();
26
+ const relativePath = currentPath.replace(entryPath, '');
27
+ const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
28
+ if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
29
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
30
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
31
+ const newUrl = entryPath + newPath + location.search + location.hash;
32
+ await navigate(newUrl, {
33
+ replace: true
34
+ });
35
+ }
36
+ } else if (localePathRedirect && isBrowser() && !hasRouter) {
37
+ const currentPath = window.location.pathname;
38
+ const entryPath = getEntryPath();
39
+ const relativePath = currentPath.replace(entryPath, '');
40
+ const pathLanguage = detectLanguageFromPath(currentPath, languages || [], localePathRedirect);
41
+ if (pathLanguage.detected && pathLanguage.language === newLang) return void updateLanguage?.(newLang);
42
+ if (!shouldIgnoreRedirect(relativePath, languages || [], ignoreRedirectRoutes)) {
43
+ const newPath = buildLocalizedUrl(relativePath, newLang, languages || [], localisedUrls);
44
+ const newUrl = entryPath + newPath + window.location.search + window.location.hash;
45
+ window.history.pushState(null, '', newUrl);
46
+ }
47
+ }
48
+ if (updateLanguage) updateLanguage(newLang);
49
+ } catch (error) {
50
+ console.error('Failed change language:', error);
51
+ throw error;
52
+ }
53
+ }
54
+ function translateI18n(i18nInstance, key, ...args) {
55
+ if ('function' != typeof i18nInstance.t) throw new Error('i18nInstance.t required');
56
+ return i18nInstance.t(key, ...args);
57
+ }
58
+ function isI18nLanguageSupported(languages, lang) {
59
+ return languages?.includes(lang) || false;
60
+ }
61
+ function isI18nResourcesReady(i18nInstance, currentLanguage) {
62
+ if (!i18nInstance?.isInitialized) return false;
63
+ const backend = i18nInstance?.services?.backend;
64
+ if (backend && 'function' == typeof backend.isLoading) {
65
+ const loadingResources = backend.getLoadingResources();
66
+ const isCurrentLanguageLoading = loadingResources.some(({ language })=>language === currentLanguage);
67
+ if (isCurrentLanguageLoading) return false;
68
+ }
69
+ const store = i18nInstance.store;
70
+ if (!store?.data) return false;
71
+ const langData = store.data[currentLanguage];
72
+ if (!langData || 'object' != typeof langData) return false;
73
+ const options = i18nInstance.options;
74
+ const namespaces = options?.ns || options?.defaultNS || [
75
+ 'translation'
76
+ ];
77
+ const requiredNamespaces = Array.isArray(namespaces) ? namespaces : [
78
+ namespaces
79
+ ];
80
+ return requiredNamespaces.every((ns)=>{
81
+ const nsData = langData[ns];
82
+ return nsData && 'object' == typeof nsData && Object.keys(nsData).length > 0;
83
+ });
84
+ }
85
+ export { cacheI18nLanguage, 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 { 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 };
@@ -0,0 +1,103 @@
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 sortedKeys = Object.keys(value).sort();
26
+ const sortedEntries = sortedKeys.map((key)=>{
27
+ const stringifiedValue = stableStringify(value[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 };