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

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
@@ -0,0 +1,174 @@
1
+ const warnedTargets = new Set<string>();
2
+
3
+ export const warnOnce = (key: string, message: string) => {
4
+ if (process.env.NODE_ENV !== 'development' || warnedTargets.has(key)) {
5
+ return;
6
+ }
7
+ warnedTargets.add(key);
8
+ console.warn(message);
9
+ };
10
+
11
+ export type LinkParams = Record<string, string | number | undefined>;
12
+ type NormalizedSearchValue = string | string[];
13
+
14
+ /**
15
+ * Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
16
+ * (`$` / `*`) segments with concrete values before localization, so
17
+ * pattern-mapped slugs localize correctly.
18
+ */
19
+ export const interpolateRouteParams = (
20
+ pathname: string,
21
+ params?: LinkParams,
22
+ ): string => {
23
+ if (!/[$:*{]/.test(pathname)) {
24
+ return pathname;
25
+ }
26
+
27
+ const resolveParam = (name: string): string | undefined => {
28
+ const value = params?.[name];
29
+ return value === undefined ? undefined : String(value);
30
+ };
31
+
32
+ const segments = pathname
33
+ .split('/')
34
+ .map(segment => {
35
+ if (!segment) {
36
+ return segment;
37
+ }
38
+
39
+ if (segment.startsWith('{-$') && segment.endsWith('}')) {
40
+ const value = resolveParam(segment.slice(3, -1));
41
+ return value === undefined ? null : encodeURIComponent(value);
42
+ }
43
+
44
+ if (segment === '$' || segment === '*') {
45
+ const value = resolveParam('_splat') ?? resolveParam('*');
46
+ return value === undefined
47
+ ? null
48
+ : value.split('/').map(encodeURIComponent).join('/');
49
+ }
50
+
51
+ if (segment.startsWith('$')) {
52
+ const value = resolveParam(segment.slice(1));
53
+ if (value === undefined) {
54
+ warnOnce(
55
+ `missing-param:${pathname}:${segment}`,
56
+ `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`,
57
+ );
58
+ return segment;
59
+ }
60
+ return encodeURIComponent(value);
61
+ }
62
+
63
+ if (segment.startsWith(':')) {
64
+ const optional = segment.endsWith('?');
65
+ const name = segment.slice(1, optional ? -1 : undefined);
66
+ const value = resolveParam(name);
67
+ if (value === undefined) {
68
+ if (optional) {
69
+ return null;
70
+ }
71
+ warnOnce(
72
+ `missing-param:${pathname}:${segment}`,
73
+ `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`,
74
+ );
75
+ return segment;
76
+ }
77
+ return encodeURIComponent(value);
78
+ }
79
+
80
+ return segment;
81
+ })
82
+ .filter(segment => segment !== null);
83
+
84
+ return segments.join('/') || '/';
85
+ };
86
+
87
+ export const normalizeSearch = (
88
+ search: string | Record<string, unknown> | undefined,
89
+ searchFromTo: string,
90
+ ): {
91
+ searchString: string;
92
+ searchObject: Record<string, NormalizedSearchValue> | undefined;
93
+ } => {
94
+ if (search && typeof search === 'object') {
95
+ const searchObject: Record<string, NormalizedSearchValue> = {};
96
+ const params = new URLSearchParams();
97
+
98
+ for (const [key, value] of Object.entries(search)) {
99
+ if (value === undefined || value === null) {
100
+ continue;
101
+ }
102
+
103
+ if (Array.isArray(value)) {
104
+ const values = value
105
+ .filter(item => item !== undefined && item !== null)
106
+ .map(String);
107
+ if (values.length === 0) {
108
+ continue;
109
+ }
110
+ searchObject[key] = values;
111
+ for (const item of values) {
112
+ params.append(key, item);
113
+ }
114
+ continue;
115
+ }
116
+
117
+ const stringValue = String(value);
118
+ searchObject[key] = stringValue;
119
+ params.append(key, stringValue);
120
+ }
121
+
122
+ const serialized = params.toString();
123
+ return {
124
+ searchString: serialized ? `?${serialized}` : '',
125
+ searchObject:
126
+ Object.keys(searchObject).length > 0 ? searchObject : undefined,
127
+ };
128
+ }
129
+
130
+ const raw = typeof search === 'string' ? search : searchFromTo;
131
+ if (!raw) {
132
+ return { searchString: '', searchObject: undefined };
133
+ }
134
+
135
+ const searchString = raw.startsWith('?') ? raw : `?${raw}`;
136
+ const searchObject: Record<string, NormalizedSearchValue> = {};
137
+ new URLSearchParams(searchString).forEach((value, key) => {
138
+ const existing = searchObject[key];
139
+ if (existing === undefined) {
140
+ searchObject[key] = value;
141
+ } else if (Array.isArray(existing)) {
142
+ existing.push(value);
143
+ } else {
144
+ searchObject[key] = [existing, value];
145
+ }
146
+ });
147
+
148
+ return { searchString, searchObject };
149
+ };
150
+
151
+ type ActivePropsResult<TActiveProps extends Record<string, unknown>> = Record<
152
+ string,
153
+ unknown
154
+ > &
155
+ Partial<TActiveProps>;
156
+
157
+ export const splitActiveProps = <TActiveProps extends Record<string, unknown>>(
158
+ active: boolean,
159
+ activeProps?: TActiveProps,
160
+ ): ActivePropsResult<TActiveProps> => {
161
+ if (!active || !activeProps) {
162
+ return {};
163
+ }
164
+ return activeProps;
165
+ };
166
+
167
+ export const mergeClassNames = (
168
+ ...values: Array<unknown>
169
+ ): string | undefined => {
170
+ const classNames = values.filter(
171
+ (value): value is string => typeof value === 'string' && value.length > 0,
172
+ );
173
+ return classNames.length > 0 ? classNames.join(' ') : undefined;
174
+ };
@@ -0,0 +1,189 @@
1
+ import { isBrowser } from '@modern-js/runtime';
2
+ import type { TInternalRuntimeContext } from '@modern-js/runtime/internal';
3
+ import { merge } from '@modern-js/runtime-utils/merge';
4
+ import type {
5
+ BaseBackendOptions,
6
+ BaseLocaleDetectionOptions,
7
+ } from '../shared/type';
8
+ import type { I18nInitOptions, I18nInstance } from './i18n';
9
+ import { getI18nInstance } from './i18n';
10
+ import { mergeBackendOptions } from './i18n/backend';
11
+ import {
12
+ detectLanguageWithPriority,
13
+ exportServerLngToWindow,
14
+ mergeDetectionOptions,
15
+ } from './i18n/detection';
16
+ import { useI18nextLanguageDetector } from './i18n/detection/middleware';
17
+ import type { ReactI18nextIntegration } from './reactI18next';
18
+ import { getPathname } from './utils';
19
+
20
+ type I18nLifecycleHelpers = {
21
+ useI18nextBackend: typeof import('./i18n/backend/middleware')['useI18nextBackend'];
22
+ changeI18nLanguage: typeof import('./i18n/utils')['changeI18nLanguage'];
23
+ ensureLanguageMatch: typeof import('./i18n/utils')['ensureLanguageMatch'];
24
+ initializeI18nInstance: typeof import('./i18n/utils')['initializeI18nInstance'];
25
+ setupClonedInstance: typeof import('./i18n/utils')['setupClonedInstance'];
26
+ };
27
+
28
+ let i18nLifecycleHelpersPromise: Promise<I18nLifecycleHelpers> | undefined;
29
+
30
+ function loadI18nLifecycleHelpers(): Promise<I18nLifecycleHelpers> {
31
+ i18nLifecycleHelpersPromise ??= Promise.all([
32
+ import('./i18n/backend/middleware'),
33
+ import('./i18n/utils'),
34
+ ]).then(([backendMiddleware, utils]) => ({
35
+ useI18nextBackend: backendMiddleware.useI18nextBackend,
36
+ changeI18nLanguage: utils.changeI18nLanguage,
37
+ ensureLanguageMatch: utils.ensureLanguageMatch,
38
+ initializeI18nInstance: utils.initializeI18nInstance,
39
+ setupClonedInstance: utils.setupClonedInstance,
40
+ }));
41
+
42
+ return i18nLifecycleHelpersPromise;
43
+ }
44
+
45
+ interface RuntimeConfigWithI18n {
46
+ i18n?: {
47
+ initOptions?: I18nInitOptions;
48
+ };
49
+ }
50
+
51
+ interface I18nRuntimeApi {
52
+ getRuntimeConfig: () => RuntimeConfigWithI18n;
53
+ }
54
+
55
+ export interface RuntimeContextWithI18n extends TInternalRuntimeContext {
56
+ i18nInstance?: I18nInstance;
57
+ changeLanguage?: (lang: string) => Promise<void>;
58
+ }
59
+
60
+ interface SetupI18nBeforeRenderOptions {
61
+ api: I18nRuntimeApi;
62
+ userI18nInstance?: I18nInstance;
63
+ initOptions?: I18nInitOptions;
64
+ backend?: BaseBackendOptions;
65
+ backendEnabled: boolean;
66
+ i18nextDetector: boolean;
67
+ detection?: BaseLocaleDetectionOptions['detection'];
68
+ localePathRedirect: boolean;
69
+ languages: string[];
70
+ fallbackLanguage: string;
71
+ resolveReactI18nextIntegration: () => Promise<ReactI18nextIntegration | null>;
72
+ setI18nextProvider: (
73
+ provider: ReactI18nextIntegration['I18nextProvider'],
74
+ ) => void;
75
+ }
76
+
77
+ export async function setupI18nBeforeRender(
78
+ context: RuntimeContextWithI18n,
79
+ options: SetupI18nBeforeRenderOptions,
80
+ ): Promise<I18nInstance> {
81
+ const {
82
+ api,
83
+ userI18nInstance,
84
+ initOptions,
85
+ backend,
86
+ backendEnabled,
87
+ i18nextDetector,
88
+ detection,
89
+ localePathRedirect,
90
+ languages,
91
+ fallbackLanguage,
92
+ resolveReactI18nextIntegration,
93
+ setI18nextProvider,
94
+ } = options;
95
+ const {
96
+ useI18nextBackend,
97
+ changeI18nLanguage,
98
+ ensureLanguageMatch,
99
+ initializeI18nInstance,
100
+ setupClonedInstance,
101
+ } = await loadI18nLifecycleHelpers();
102
+ let i18nInstance = await getI18nInstance(userI18nInstance);
103
+ const { i18n: otherConfig } = api.getRuntimeConfig();
104
+ const { initOptions: otherInitOptions } = otherConfig || {};
105
+ const userInitOptions = merge(otherInitOptions || {}, initOptions || {});
106
+
107
+ const reactI18nextIntegration = await resolveReactI18nextIntegration();
108
+ setI18nextProvider(reactI18nextIntegration?.I18nextProvider ?? null);
109
+ if (reactI18nextIntegration?.initReactI18next) {
110
+ i18nInstance.use(reactI18nextIntegration.initReactI18next);
111
+ }
112
+
113
+ const pathname = getPathname(context);
114
+ if (i18nextDetector) {
115
+ useI18nextLanguageDetector(i18nInstance);
116
+ }
117
+
118
+ const mergedDetection = mergeDetectionOptions(
119
+ i18nextDetector,
120
+ detection,
121
+ localePathRedirect,
122
+ userInitOptions,
123
+ );
124
+ const mergedBackend = mergeBackendOptions(backend, userInitOptions);
125
+
126
+ // Register Backend BEFORE detectLanguageWithPriority because detection may init.
127
+ const hasSdkConfig =
128
+ typeof userInitOptions?.backend?.sdk === 'function' ||
129
+ (mergedBackend?.sdk && typeof mergedBackend.sdk === 'function');
130
+ if (mergedBackend && (backendEnabled || hasSdkConfig)) {
131
+ useI18nextBackend(i18nInstance, mergedBackend);
132
+ }
133
+
134
+ const { finalLanguage } = await detectLanguageWithPriority(i18nInstance, {
135
+ languages,
136
+ fallbackLanguage,
137
+ localePathRedirect,
138
+ i18nextDetector,
139
+ detection,
140
+ userInitOptions,
141
+ mergedBackend,
142
+ pathname,
143
+ ssrContext: context.ssrContext,
144
+ });
145
+
146
+ await initializeI18nInstance(
147
+ i18nInstance,
148
+ finalLanguage,
149
+ fallbackLanguage,
150
+ languages,
151
+ mergedDetection,
152
+ mergedBackend,
153
+ userInitOptions,
154
+ );
155
+
156
+ if (!isBrowser() && i18nInstance.cloneInstance) {
157
+ i18nInstance = i18nInstance.cloneInstance();
158
+ await setupClonedInstance(
159
+ i18nInstance,
160
+ finalLanguage,
161
+ fallbackLanguage,
162
+ languages,
163
+ backendEnabled,
164
+ backend,
165
+ i18nextDetector,
166
+ detection,
167
+ localePathRedirect,
168
+ userInitOptions,
169
+ );
170
+ }
171
+
172
+ if (localePathRedirect) {
173
+ await ensureLanguageMatch(i18nInstance, finalLanguage);
174
+ }
175
+
176
+ if (!isBrowser()) {
177
+ exportServerLngToWindow(context, finalLanguage);
178
+ }
179
+ context.i18nInstance = i18nInstance;
180
+
181
+ // Add changeLanguage method to context for other runtime plugins to use.
182
+ context.changeLanguage = async (newLang: string) => {
183
+ await changeI18nLanguage(i18nInstance, newLang, {
184
+ detectionOptions: mergedDetection,
185
+ });
186
+ };
187
+
188
+ return i18nInstance;
189
+ }
@@ -0,0 +1,144 @@
1
+ import { RuntimeContext } from '@modern-js/runtime';
2
+ import { Helmet } from '@modern-js/runtime/head';
3
+ import type React from 'react';
4
+ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
5
+ import type { BaseLocaleDetectionOptions } from '../shared/type';
6
+ import { ModernI18nProvider } from './context';
7
+ import {
8
+ createContextValue,
9
+ useClientSideRedirect,
10
+ useLanguageSync,
11
+ useSdkResourcesLoader,
12
+ } from './hooks';
13
+ import type { I18nInstance } from './i18n';
14
+ import { getI18nextInstanceForProvider } from './i18n/instance';
15
+ import type { RuntimeContextWithI18n } from './pluginSetup';
16
+
17
+ interface I18nRootWrapperOptions {
18
+ entryName?: string;
19
+ htmlLangAttr: boolean;
20
+ localePathRedirect: boolean;
21
+ languages: string[];
22
+ fallbackLanguage: string;
23
+ ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
24
+ localisedUrls?: BaseLocaleDetectionOptions['localisedUrls'];
25
+ getLatestI18nInstance: () => I18nInstance | undefined;
26
+ getI18nextProvider: () => React.ComponentType<any> | null | undefined;
27
+ }
28
+
29
+ export const createI18nRootWrapper =
30
+ (options: I18nRootWrapperOptions) => (App: React.ComponentType<any>) => {
31
+ return (props: Record<string, unknown>) => {
32
+ const {
33
+ entryName,
34
+ htmlLangAttr,
35
+ localePathRedirect,
36
+ languages,
37
+ fallbackLanguage,
38
+ ignoreRedirectRoutes,
39
+ localisedUrls,
40
+ getLatestI18nInstance,
41
+ getI18nextProvider,
42
+ } = options;
43
+ const runtimeContext = useContext(
44
+ RuntimeContext,
45
+ ) as RuntimeContextWithI18n;
46
+ const i18nInstance =
47
+ runtimeContext.i18nInstance || getLatestI18nInstance();
48
+ const initialLang = useMemo(
49
+ () => i18nInstance?.language || fallbackLanguage,
50
+ [i18nInstance?.language, fallbackLanguage],
51
+ );
52
+ const [lang, setLang] = useState(initialLang);
53
+ const [forceUpdate, setForceUpdate] = useState(0);
54
+ const prevLangRef = useRef(lang);
55
+ const runtimeContextRef = useRef(runtimeContext);
56
+ runtimeContextRef.current = runtimeContext;
57
+
58
+ useEffect(() => {
59
+ if (i18nInstance?.language) {
60
+ const translator = (i18nInstance as any).translator;
61
+ if (translator) {
62
+ translator.language = i18nInstance.language;
63
+ }
64
+ }
65
+ }, [i18nInstance?.language]);
66
+
67
+ useEffect(() => {
68
+ prevLangRef.current = lang;
69
+ }, [lang]);
70
+
71
+ useSdkResourcesLoader(i18nInstance, setForceUpdate);
72
+ useLanguageSync(
73
+ i18nInstance,
74
+ localePathRedirect,
75
+ languages,
76
+ runtimeContextRef,
77
+ prevLangRef,
78
+ setLang,
79
+ );
80
+
81
+ // Handle client-side redirect for static deployments.
82
+ useClientSideRedirect(
83
+ i18nInstance,
84
+ localePathRedirect,
85
+ languages,
86
+ fallbackLanguage,
87
+ ignoreRedirectRoutes,
88
+ localisedUrls,
89
+ );
90
+
91
+ const contextValue = useMemo(
92
+ () =>
93
+ createContextValue(
94
+ lang,
95
+ i18nInstance,
96
+ entryName,
97
+ languages,
98
+ localePathRedirect,
99
+ ignoreRedirectRoutes,
100
+ localisedUrls,
101
+ setLang,
102
+ ),
103
+ [
104
+ lang,
105
+ i18nInstance,
106
+ entryName,
107
+ languages,
108
+ localePathRedirect,
109
+ ignoreRedirectRoutes,
110
+ localisedUrls,
111
+ forceUpdate,
112
+ ],
113
+ );
114
+
115
+ const children = (props as React.PropsWithChildren).children;
116
+ let appContent: React.ReactNode = App ? (
117
+ <App {...props}>{children}</App>
118
+ ) : (
119
+ children
120
+ );
121
+
122
+ if (i18nInstance) {
123
+ const I18nextProvider = getI18nextProvider();
124
+ if (I18nextProvider) {
125
+ const i18nextInstanceForProvider =
126
+ getI18nextInstanceForProvider(i18nInstance);
127
+ appContent = (
128
+ <I18nextProvider i18n={i18nextInstanceForProvider}>
129
+ {appContent}
130
+ </I18nextProvider>
131
+ );
132
+ }
133
+ }
134
+
135
+ return (
136
+ <>
137
+ {Boolean(htmlLangAttr) && <Helmet htmlAttributes={{ lang }} />}
138
+ <ModernI18nProvider value={contextValue}>
139
+ {appContent}
140
+ </ModernI18nProvider>
141
+ </>
142
+ );
143
+ };
144
+ };
@@ -0,0 +1,20 @@
1
+ import type React from 'react';
2
+
3
+ export interface ReactI18nextIntegration {
4
+ I18nextProvider: React.ComponentType<any> | null;
5
+ initReactI18next: any | null;
6
+ }
7
+
8
+ export type LoadReactI18nextIntegration =
9
+ () => Promise<ReactI18nextIntegration | null>;
10
+
11
+ export const resolveReactI18nextIntegration = async (
12
+ reactI18next: boolean,
13
+ loadReactI18nextIntegration?: LoadReactI18nextIntegration,
14
+ ): Promise<ReactI18nextIntegration | null> => {
15
+ if (!reactI18next) {
16
+ return null;
17
+ }
18
+
19
+ return loadReactI18nextIntegration?.() ?? null;
20
+ };
@@ -15,31 +15,31 @@ import {
15
15
  import type React from 'react';
16
16
  import { useCallback, useContext, useEffect, useState } from 'react';
17
17
 
18
- export type I18nRouterFramework = 'react-router' | 'tanstack' | string;
18
+ type I18nRouterFramework = 'react-router' | 'tanstack' | string;
19
19
 
20
- export interface I18nRouterLocation {
20
+ interface I18nRouterLocation {
21
21
  pathname: string;
22
22
  search: string;
23
23
  hash: string;
24
24
  }
25
25
 
26
- export interface I18nRouterNavigateOptions {
26
+ interface I18nRouterNavigateOptions {
27
27
  replace?: boolean;
28
28
  state?: unknown;
29
29
  }
30
30
 
31
- export type I18nRouterNavigate = (
31
+ type I18nRouterNavigate = (
32
32
  href: string,
33
33
  options?: I18nRouterNavigateOptions,
34
34
  ) => void | Promise<void>;
35
35
 
36
- export type I18nRouterLink = React.ComponentType<{
36
+ type I18nRouterLink = React.ComponentType<{
37
37
  to: string;
38
38
  children?: React.ReactNode;
39
39
  [key: string]: unknown;
40
40
  }>;
41
41
 
42
- export interface I18nRouterAdapter {
42
+ interface I18nRouterAdapter {
43
43
  framework?: I18nRouterFramework;
44
44
  hasRouter: boolean;
45
45
  location: I18nRouterLocation | null;
@@ -1,12 +1,17 @@
1
1
  import { isBrowser } from '@modern-js/runtime';
2
- import {
3
- getGlobalBasename,
4
- type TInternalRuntimeContext,
5
- } from '@modern-js/runtime/context';
2
+ import { getGlobalBasename } from '@modern-js/runtime/context';
6
3
  import type { LocalisedUrlsOption } from '../shared/localisedUrls';
7
- import { localiseTargetPathname } from '../shared/localisedUrls';
8
-
9
- export const getPathname = (context: TInternalRuntimeContext): string => {
4
+ import {
5
+ localiseTargetPathname,
6
+ shouldSkipLocaleRedirect,
7
+ } from '../shared/localisedUrls';
8
+
9
+ // Structural parameter: hooks.ts passes a public-TRuntimeContext-based
10
+ // context while core.tsx passes the internal one; both carry the request
11
+ // pathname shape this helper needs.
12
+ export const getPathname = (context: {
13
+ ssrContext?: { request?: { pathname?: string } };
14
+ }): string => {
10
15
  if (isBrowser()) {
11
16
  return window.location.pathname;
12
17
  }
@@ -127,32 +132,5 @@ export const shouldIgnoreRedirect = (
127
132
  languages: string[],
128
133
  ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
129
134
  ): boolean => {
130
- if (!ignoreRedirectRoutes) {
131
- return false;
132
- }
133
-
134
- // Remove language prefix if present (e.g., /en/api -> /api)
135
- const segments = pathname.split('/').filter(Boolean);
136
- let pathWithoutLang = pathname;
137
- if (segments.length > 0 && languages.includes(segments[0])) {
138
- // Remove language prefix
139
- pathWithoutLang = `/${segments.slice(1).join('/')}`;
140
- }
141
-
142
- // Normalize path (ensure it starts with /)
143
- const normalizedPath = pathWithoutLang.startsWith('/')
144
- ? pathWithoutLang
145
- : `/${pathWithoutLang}`;
146
-
147
- if (typeof ignoreRedirectRoutes === 'function') {
148
- return ignoreRedirectRoutes(normalizedPath);
149
- }
150
-
151
- // Check if pathname matches any of the ignore patterns
152
- return ignoreRedirectRoutes.some(pattern => {
153
- // Support both exact match and prefix match
154
- return (
155
- normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`)
156
- );
157
- });
135
+ return shouldSkipLocaleRedirect(pathname, languages, ignoreRedirectRoutes);
158
136
  };
@@ -0,0 +1,67 @@
1
+ type ApiPrefixInput = string | string[] | undefined;
2
+
3
+ const normalizeApiPrefix = (prefix: string): string | null => {
4
+ const trimmedPrefix = prefix.trim();
5
+ if (!trimmedPrefix) {
6
+ return null;
7
+ }
8
+
9
+ const prefixedPath = trimmedPrefix.startsWith('/')
10
+ ? trimmedPrefix
11
+ : `/${trimmedPrefix}`;
12
+ const withoutWildcard = prefixedPath.replace(/\/\*$/, '');
13
+ const normalizedPrefix =
14
+ withoutWildcard.length > 1
15
+ ? withoutWildcard.replace(/\/+$/, '')
16
+ : withoutWildcard;
17
+
18
+ return normalizedPrefix === '/' ? null : normalizedPrefix;
19
+ };
20
+
21
+ export const collectApiPrefixes = (
22
+ routes: Array<{ isApi?: boolean; urlPath?: string }>,
23
+ bffPrefix?: ApiPrefixInput,
24
+ ): string[] => {
25
+ const prefixes = new Set<string>();
26
+
27
+ for (const route of routes) {
28
+ if (!route.isApi || !route.urlPath) {
29
+ continue;
30
+ }
31
+
32
+ const normalizedPrefix = normalizeApiPrefix(route.urlPath);
33
+ if (normalizedPrefix) {
34
+ prefixes.add(normalizedPrefix);
35
+ }
36
+ }
37
+
38
+ const bffPrefixes = Array.isArray(bffPrefix)
39
+ ? bffPrefix
40
+ : bffPrefix
41
+ ? [bffPrefix]
42
+ : [];
43
+
44
+ for (const prefix of bffPrefixes) {
45
+ const normalizedPrefix = normalizeApiPrefix(prefix);
46
+ if (normalizedPrefix) {
47
+ prefixes.add(normalizedPrefix);
48
+ }
49
+ }
50
+
51
+ return [...prefixes];
52
+ };
53
+
54
+ export const matchesApiPrefix = (
55
+ pathname: string,
56
+ apiPrefixes: string[],
57
+ ): boolean => {
58
+ const normalizedPathname = pathname.startsWith('/')
59
+ ? pathname
60
+ : `/${pathname}`;
61
+
62
+ return apiPrefixes.some(
63
+ prefix =>
64
+ normalizedPathname === prefix ||
65
+ normalizedPathname.startsWith(`${prefix}/`),
66
+ );
67
+ };