@bleedingdev/modern-js-i18n-integration 0.0.0 → 3.9.0-ultramodern.5

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/cli.js +97 -0
  3. package/dist/cjs/federation/boundary.js +100 -0
  4. package/dist/cjs/federation/registration.js +43 -0
  5. package/dist/cjs/languageSynchronization.js +44 -0
  6. package/dist/cjs/navigation.js +220 -0
  7. package/dist/cjs/options.js +18 -0
  8. package/dist/cjs/runtime-no-react-i18next.js +46 -0
  9. package/dist/cjs/runtime.js +46 -0
  10. package/dist/cjs/runtimeComposition.js +60 -0
  11. package/dist/cjs/server.js +50 -0
  12. package/dist/cjs/urlStrategy.js +51 -0
  13. package/dist/esm/cli.mjs +57 -0
  14. package/dist/esm/federation/boundary.mjs +62 -0
  15. package/dist/esm/federation/registration.mjs +5 -0
  16. package/dist/esm/languageSynchronization.mjs +6 -0
  17. package/dist/esm/navigation.mjs +182 -0
  18. package/dist/esm/options.mjs +0 -0
  19. package/dist/esm/runtime-no-react-i18next.mjs +6 -0
  20. package/dist/esm/runtime.mjs +6 -0
  21. package/dist/esm/runtimeComposition.mjs +22 -0
  22. package/dist/esm/server.mjs +10 -0
  23. package/dist/esm/urlStrategy.mjs +13 -0
  24. package/dist/esm-node/cli.mjs +58 -0
  25. package/dist/esm-node/federation/boundary.mjs +63 -0
  26. package/dist/esm-node/federation/registration.mjs +6 -0
  27. package/dist/esm-node/languageSynchronization.mjs +7 -0
  28. package/dist/esm-node/navigation.mjs +183 -0
  29. package/dist/esm-node/options.mjs +1 -0
  30. package/dist/esm-node/runtime-no-react-i18next.mjs +7 -0
  31. package/dist/esm-node/runtime.mjs +7 -0
  32. package/dist/esm-node/runtimeComposition.mjs +23 -0
  33. package/dist/esm-node/server.mjs +11 -0
  34. package/dist/esm-node/urlStrategy.mjs +14 -0
  35. package/dist/types/cli.d.ts +4 -0
  36. package/dist/types/federation/boundary.d.ts +9 -0
  37. package/dist/types/federation/registration.d.ts +2 -0
  38. package/dist/types/languageSynchronization.d.ts +3 -0
  39. package/dist/types/navigation.d.ts +3 -0
  40. package/dist/types/options.d.ts +13 -0
  41. package/dist/types/runtime-no-react-i18next.d.ts +2 -0
  42. package/dist/types/runtime.d.ts +2 -0
  43. package/dist/types/runtimeComposition.d.ts +4 -0
  44. package/dist/types/server.d.ts +3 -0
  45. package/dist/types/urlStrategy.d.ts +4 -0
  46. package/package.json +139 -5
  47. package/src/cli.ts +72 -0
  48. package/src/federation/boundary.tsx +129 -0
  49. package/src/federation/registration.ts +8 -0
  50. package/src/languageSynchronization.tsx +23 -0
  51. package/src/navigation.tsx +399 -0
  52. package/src/options.ts +18 -0
  53. package/src/runtime-no-react-i18next.ts +5 -0
  54. package/src/runtime.ts +5 -0
  55. package/src/runtimeComposition.ts +33 -0
  56. package/src/server.ts +27 -0
  57. package/src/urlStrategy.ts +19 -0
  58. package/README.md +0 -3
@@ -0,0 +1,129 @@
1
+ import {
2
+ type FederatedI18nBoundaryProps,
3
+ getActualI18nextInstance,
4
+ ModernI18nContext,
5
+ ModernI18nProvider,
6
+ ReactI18nextProviderContext,
7
+ } from '@modern-js/plugin-i18n/runtime/consumer';
8
+ import { type FC, useContext, useMemo } from 'react';
9
+
10
+ export type { FederatedI18nBoundaryProps } from '@modern-js/plugin-i18n/runtime/consumer';
11
+
12
+ /**
13
+ * Keeps a federated surface's translation resources inside its delivery unit.
14
+ * The host supplies only the active language; the remote owns and versions the
15
+ * resources used below this boundary.
16
+ */
17
+ export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
18
+ children,
19
+ defaultNamespace,
20
+ fallbackLanguage,
21
+ resources,
22
+ supportedLanguages,
23
+ }) => {
24
+ const parent = useContext(ModernI18nContext);
25
+ const I18nextProvider = useContext(ReactI18nextProviderContext);
26
+ if (!parent) {
27
+ throw new Error(
28
+ 'FederatedI18nBoundary must be used within ModernI18nProvider',
29
+ );
30
+ }
31
+
32
+ const languages =
33
+ supportedLanguages ?? parent.languages ?? Object.keys(resources);
34
+ const scopedInstance = useMemo(() => {
35
+ const parentInstance = getActualI18nextInstance(parent.i18nInstance);
36
+ const clone = parentInstance.cloneInstance?.({
37
+ defaultNS: defaultNamespace,
38
+ fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
39
+ forkResourceStore: true,
40
+ initImmediate: false,
41
+ lng: parent.language,
42
+ ns: [defaultNamespace],
43
+ resources,
44
+ supportedLngs: languages,
45
+ });
46
+ if (!clone) {
47
+ throw new Error(
48
+ 'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
49
+ );
50
+ }
51
+ const resourceStore = clone.store;
52
+ const parentResourceStore = parentInstance.store;
53
+ const sharesNestedResourceState = Object.entries(
54
+ resourceStore?.data ?? {},
55
+ ).some(([language, namespaces]) => {
56
+ const parentNamespaces = parentResourceStore?.data?.[language];
57
+ return (
58
+ namespaces === parentNamespaces ||
59
+ Object.entries(namespaces).some(
60
+ ([namespace, resource]) =>
61
+ resource !== null &&
62
+ typeof resource === 'object' &&
63
+ resource === parentNamespaces?.[namespace],
64
+ )
65
+ );
66
+ });
67
+ if (
68
+ clone === parentInstance ||
69
+ resourceStore === parentResourceStore ||
70
+ resourceStore?.data === parentResourceStore?.data ||
71
+ sharesNestedResourceState
72
+ ) {
73
+ throw new Error(
74
+ 'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
75
+ );
76
+ }
77
+ if (
78
+ !resourceStore?.addResourceBundle ||
79
+ !clone.removeResourceBundle ||
80
+ !resourceStore.data
81
+ ) {
82
+ throw new Error(
83
+ 'FederatedI18nBoundary requires an isolated mutable i18n resource store',
84
+ );
85
+ }
86
+ for (const [language, namespaces] of Object.entries(resourceStore.data)) {
87
+ for (const namespace of Object.keys(namespaces)) {
88
+ clone.removeResourceBundle(language, namespace);
89
+ }
90
+ }
91
+ for (const [language, namespaces] of Object.entries(resources)) {
92
+ for (const [namespace, resource] of Object.entries(namespaces)) {
93
+ resourceStore.addResourceBundle(
94
+ language,
95
+ namespace,
96
+ resource as Record<string, string>,
97
+ true,
98
+ true,
99
+ );
100
+ }
101
+ }
102
+ return clone;
103
+ }, [
104
+ defaultNamespace,
105
+ fallbackLanguage,
106
+ languages,
107
+ parent.i18nInstance,
108
+ parent.language,
109
+ resources,
110
+ ]);
111
+ const value = useMemo(
112
+ () => ({
113
+ ...parent,
114
+ i18nInstance: scopedInstance,
115
+ language: parent.language,
116
+ languages,
117
+ }),
118
+ [languages, parent, scopedInstance],
119
+ );
120
+
121
+ const scopedContent = (
122
+ <ModernI18nProvider value={value}>{children}</ModernI18nProvider>
123
+ );
124
+ return I18nextProvider ? (
125
+ <I18nextProvider i18n={scopedInstance}>{scopedContent}</I18nextProvider>
126
+ ) : (
127
+ scopedContent
128
+ );
129
+ };
@@ -0,0 +1,8 @@
1
+ import type { RuntimePluginAPI } from '@modern-js/plugin/runtime';
2
+ import { FederatedI18nBoundary } from './boundary';
3
+
4
+ export function registerFederatedI18nBoundary(api: RuntimePluginAPI<{}>): void {
5
+ api.resolveComponent((component, { name }) =>
6
+ name === 'i18n.FederatedI18nBoundary' ? FederatedI18nBoundary : component,
7
+ );
8
+ }
@@ -0,0 +1,23 @@
1
+ import { useLanguageSync } from '@modern-js/i18n-runtime-extensions';
2
+ import type { I18nLanguageSynchronizationProps } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
3
+
4
+ /** Bind the shared controller to this native provider's instance and state. */
5
+ export const I18nLanguageSynchronization = ({
6
+ i18nInstance,
7
+ localePathRedirect,
8
+ languages,
9
+ pathname,
10
+ prevLangRef,
11
+ setLang,
12
+ children,
13
+ }: I18nLanguageSynchronizationProps) => {
14
+ const synchronizeLanguage = useLanguageSync(
15
+ i18nInstance,
16
+ localePathRedirect,
17
+ languages,
18
+ pathname,
19
+ prevLangRef,
20
+ setLang,
21
+ );
22
+ return children(synchronizeLanguage);
23
+ };
@@ -0,0 +1,399 @@
1
+ import {
2
+ I18nNavigationProvider,
3
+ type I18nRouterAdapter,
4
+ type I18nRouterLinkTarget,
5
+ useNativeI18nRouterAdapter,
6
+ } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
7
+ import { isBrowser, RuntimeContext } from '@modern-js/runtime';
8
+ import {
9
+ InternalRuntimeContext,
10
+ type TInternalRuntimeContext,
11
+ type TRuntimeContext,
12
+ } from '@modern-js/runtime/context';
13
+ import { getRouterRuntimeState } from '@modern-js/runtime-extensions/router-state';
14
+ import type React from 'react';
15
+ import { useCallback, useContext, useSyncExternalStore } from 'react';
16
+
17
+ type I18nRouterFramework = 'react-router' | 'tanstack' | string;
18
+
19
+ interface I18nRouterLocation {
20
+ pathname: string;
21
+ search: string;
22
+ hash: string;
23
+ }
24
+
25
+ interface I18nRouterNavigateOptions {
26
+ replace?: boolean;
27
+ state?: unknown;
28
+ }
29
+
30
+ type I18nRouterNavigate = (
31
+ href: string,
32
+ options?: I18nRouterNavigateOptions,
33
+ ) => void | Promise<void>;
34
+
35
+ type I18nRouterLink = React.ComponentType<{
36
+ to: string;
37
+ children?: React.ReactNode;
38
+ [key: string]: unknown;
39
+ }>;
40
+
41
+ type RuntimeContextWithRouter = TRuntimeContext & {
42
+ router?: {
43
+ useInRouterContext?: () => boolean;
44
+ useRouter?: (options?: { warn?: boolean }) => unknown;
45
+ useLocation?: () => unknown;
46
+ useNavigate?: () => I18nRouterNavigate;
47
+ useParams?: () => Record<string, string>;
48
+ useHref?: () => unknown;
49
+ Link?: I18nRouterLink;
50
+ };
51
+ };
52
+
53
+ type InternalRuntimeContextWithRouter = TInternalRuntimeContext & {
54
+ router?: RuntimeContextWithRouter['router'];
55
+ };
56
+
57
+ type RouterInstance = {
58
+ navigate?: (...args: any[]) => unknown;
59
+ state?: {
60
+ location?: unknown;
61
+ matches?: Array<{ params?: Record<string, string> }>;
62
+ };
63
+ stores?: {
64
+ location?: {
65
+ get?: () => unknown;
66
+ subscribe?: (listener: () => void) => () => void;
67
+ };
68
+ matches?: {
69
+ get?: () => Array<{ params?: Record<string, string> }>;
70
+ };
71
+ };
72
+ subscribe?:
73
+ | ((listener: () => void) => () => void)
74
+ | ((eventType: string, listener: () => void) => () => void);
75
+ };
76
+
77
+ const normalizeUrlPart = (value: unknown, prefix: '?' | '#'): string => {
78
+ if (typeof value !== 'string' || !value) {
79
+ return '';
80
+ }
81
+ return value.startsWith(prefix) ? value : `${prefix}${value}`;
82
+ };
83
+
84
+ const normalizeLocation = (location: unknown): I18nRouterLocation | null => {
85
+ if (!location || typeof location !== 'object') {
86
+ return null;
87
+ }
88
+
89
+ const locationValue = location as {
90
+ pathname?: unknown;
91
+ search?: unknown;
92
+ searchStr?: unknown;
93
+ hash?: unknown;
94
+ };
95
+
96
+ if (typeof locationValue.pathname !== 'string') {
97
+ return null;
98
+ }
99
+
100
+ return {
101
+ pathname: locationValue.pathname,
102
+ search: normalizeUrlPart(
103
+ typeof locationValue.search === 'string'
104
+ ? locationValue.search
105
+ : locationValue.searchStr,
106
+ '?',
107
+ ),
108
+ hash: normalizeUrlPart(locationValue.hash, '#'),
109
+ };
110
+ };
111
+
112
+ const getWindowLocation = (): I18nRouterLocation | null => {
113
+ if (!isBrowser()) {
114
+ return null;
115
+ }
116
+
117
+ return {
118
+ pathname: window.location.pathname,
119
+ search: window.location.search,
120
+ hash: window.location.hash,
121
+ };
122
+ };
123
+
124
+ const getRouterFramework = (
125
+ runtimeContext: RuntimeContextWithRouter,
126
+ internalContext: InternalRuntimeContextWithRouter,
127
+ inReactRouter: boolean,
128
+ ): I18nRouterFramework | undefined => {
129
+ const framework =
130
+ getRouterRuntimeState(internalContext)?.framework ||
131
+ getRouterRuntimeState(runtimeContext)?.framework;
132
+
133
+ if (framework) {
134
+ return framework;
135
+ }
136
+
137
+ if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) {
138
+ return 'tanstack';
139
+ }
140
+
141
+ if (
142
+ internalContext.router?.useLocation ||
143
+ internalContext.router?.useHref ||
144
+ runtimeContext.router?.useLocation ||
145
+ runtimeContext.router?.useHref
146
+ ) {
147
+ return 'react-router';
148
+ }
149
+
150
+ if (inReactRouter) {
151
+ return 'react-router';
152
+ }
153
+
154
+ return undefined;
155
+ };
156
+
157
+ const getRouterInstance = (
158
+ internalContext: InternalRuntimeContextWithRouter,
159
+ contextRouter?: RouterInstance | null,
160
+ ): RouterInstance | null => {
161
+ if (contextRouter) {
162
+ return contextRouter;
163
+ }
164
+
165
+ const router = getRouterRuntimeState(internalContext)?.instance;
166
+ if (!router || typeof router !== 'object') {
167
+ return null;
168
+ }
169
+ return router as RouterInstance;
170
+ };
171
+
172
+ const getRouterStateLocation = (
173
+ internalContext: InternalRuntimeContextWithRouter,
174
+ contextRouter?: RouterInstance | null,
175
+ ): I18nRouterLocation | null => {
176
+ const router = getRouterInstance(internalContext, contextRouter);
177
+ return (
178
+ normalizeLocation(router?.stores?.location?.get?.()) ||
179
+ normalizeLocation(router?.state?.location)
180
+ );
181
+ };
182
+
183
+ const getRouterParams = (
184
+ internalContext: InternalRuntimeContextWithRouter,
185
+ contextRouter?: RouterInstance | null,
186
+ ): Record<string, string> => {
187
+ const router = getRouterInstance(internalContext, contextRouter);
188
+ const matches = router?.stores?.matches?.get?.() || router?.state?.matches;
189
+ if (!Array.isArray(matches)) {
190
+ return {};
191
+ }
192
+
193
+ return matches.reduce<Record<string, string>>((params, match) => {
194
+ if (match?.params) {
195
+ Object.assign(params, match.params);
196
+ }
197
+ return params;
198
+ }, {});
199
+ };
200
+
201
+ const getRouterSnapshot = (
202
+ internalContext: InternalRuntimeContextWithRouter,
203
+ contextRouter?: RouterInstance | null,
204
+ ) => {
205
+ const location = getRouterStateLocation(internalContext, contextRouter);
206
+ const params = getRouterParams(internalContext, contextRouter);
207
+ return JSON.stringify([
208
+ location?.pathname ?? '',
209
+ location?.search ?? '',
210
+ location?.hash ?? '',
211
+ Object.entries(params).sort(([left], [right]) => left.localeCompare(right)),
212
+ ]);
213
+ };
214
+
215
+ const useIntegratedRouterAdapter = (): I18nRouterAdapter => {
216
+ const runtimeContext = useContext(RuntimeContext) as RuntimeContextWithRouter;
217
+ const internalContext = useContext(
218
+ InternalRuntimeContext,
219
+ ) as InternalRuntimeContextWithRouter;
220
+ const routerApi = internalContext.router || runtimeContext.router;
221
+ const native = useNativeI18nRouterAdapter();
222
+ const inReactRouter = native.hasRouter;
223
+ const reactRouterNavigate = native.navigate;
224
+ const reactRouterLocation = inReactRouter ? native.location : null;
225
+ const reactRouterParams = native.params;
226
+ const framework = getRouterFramework(
227
+ runtimeContext,
228
+ internalContext,
229
+ inReactRouter,
230
+ );
231
+ const contextUseRouter =
232
+ !inReactRouter && framework === 'tanstack'
233
+ ? internalContext.router?.useRouter || runtimeContext.router?.useRouter
234
+ : undefined;
235
+ const contextRouter = contextUseRouter
236
+ ? (contextUseRouter({ warn: false }) as RouterInstance | null)
237
+ : null;
238
+ const hasRouter =
239
+ framework === 'tanstack' ||
240
+ framework === 'react-router' ||
241
+ Boolean(reactRouterNavigate);
242
+
243
+ const subscribeToRouter = useCallback(
244
+ (update: () => void) => {
245
+ const router = getRouterInstance(internalContext, contextRouter);
246
+ if (!router) {
247
+ return () => undefined;
248
+ }
249
+
250
+ const unsubscribers: Array<() => void> = [];
251
+
252
+ if (
253
+ framework === 'react-router' &&
254
+ !inReactRouter &&
255
+ typeof router.subscribe === 'function'
256
+ ) {
257
+ const subscribe = router.subscribe as (
258
+ this: RouterInstance,
259
+ listener: () => void,
260
+ ) => () => void;
261
+ const unsubscribe = subscribe.call(router, update);
262
+ if (typeof unsubscribe === 'function') {
263
+ unsubscribers.push(unsubscribe);
264
+ }
265
+ }
266
+
267
+ if (
268
+ framework === 'tanstack' &&
269
+ typeof router.stores?.location?.subscribe === 'function'
270
+ ) {
271
+ const unsubscribe = router.stores.location.subscribe(update);
272
+ if (typeof unsubscribe === 'function') {
273
+ unsubscribers.push(unsubscribe);
274
+ }
275
+ }
276
+
277
+ if (framework === 'tanstack' && typeof router.subscribe === 'function') {
278
+ const subscribe = router.subscribe as (
279
+ this: RouterInstance,
280
+ eventType: string,
281
+ listener: () => void,
282
+ ) => () => void;
283
+ for (const eventType of ['onBeforeNavigate', 'onBeforeLoad']) {
284
+ const unsubscribe = subscribe.call(router, eventType, update);
285
+ if (typeof unsubscribe === 'function') {
286
+ unsubscribers.push(unsubscribe);
287
+ }
288
+ }
289
+ }
290
+
291
+ return () => {
292
+ for (const unsubscribe of unsubscribers) {
293
+ unsubscribe();
294
+ }
295
+ };
296
+ },
297
+ [contextRouter, framework, inReactRouter, internalContext],
298
+ );
299
+ const getSnapshot = useCallback(
300
+ () => getRouterSnapshot(internalContext, contextRouter),
301
+ [contextRouter, internalContext],
302
+ );
303
+ useSyncExternalStore(subscribeToRouter, getSnapshot, getSnapshot);
304
+
305
+ const navigate = useCallback<I18nRouterNavigate>(
306
+ (href, options) => {
307
+ const router = getRouterInstance(internalContext, contextRouter);
308
+ const activeFramework = getRouterFramework(
309
+ runtimeContext,
310
+ internalContext,
311
+ inReactRouter,
312
+ );
313
+
314
+ if (activeFramework === 'tanstack') {
315
+ if (typeof router?.navigate === 'function') {
316
+ return router.navigate({
317
+ to: href,
318
+ replace: options?.replace,
319
+ ...(options?.state === undefined ? {} : { state: options.state }),
320
+ }) as void | Promise<void>;
321
+ }
322
+ throw new Error('TanStack router instance is not available.');
323
+ }
324
+
325
+ if (reactRouterNavigate) {
326
+ return reactRouterNavigate(href, options);
327
+ }
328
+
329
+ if (activeFramework === 'react-router') {
330
+ if (typeof router?.navigate === 'function') {
331
+ return router.navigate(href, options) as void | Promise<void>;
332
+ }
333
+ throw new Error('React Router instance is not available.');
334
+ }
335
+ },
336
+ [
337
+ contextRouter,
338
+ internalContext,
339
+ inReactRouter,
340
+ reactRouterNavigate,
341
+ runtimeContext,
342
+ ],
343
+ );
344
+
345
+ const location =
346
+ (reactRouterLocation
347
+ ? normalizeLocation(reactRouterLocation)
348
+ : getRouterStateLocation(internalContext, contextRouter)) ||
349
+ getWindowLocation();
350
+ const params = inReactRouter
351
+ ? (reactRouterParams as Record<string, string>)
352
+ : getRouterParams(internalContext, contextRouter);
353
+ const Link =
354
+ framework === 'tanstack'
355
+ ? internalContext.router?.Link || runtimeContext.router?.Link || null
356
+ : framework === 'react-router' || inReactRouter
357
+ ? (routerApi?.Link ?? null)
358
+ : null;
359
+
360
+ return {
361
+ framework,
362
+ hasRouter,
363
+ location,
364
+ navigate: hasRouter ? navigate : null,
365
+ Link,
366
+ params,
367
+ ...(framework === 'tanstack'
368
+ ? { createLinkProps: createTanstackLinkProps }
369
+ : {}),
370
+ };
371
+ };
372
+
373
+ const createTanstackLinkProps = (target: I18nRouterLinkTarget) => {
374
+ const preload =
375
+ target.preload !== undefined
376
+ ? target.preload
377
+ : target.prefetch === 'none'
378
+ ? false
379
+ : target.prefetch;
380
+ return {
381
+ to: target.pathname,
382
+ ...(target.search ? { search: target.search } : {}),
383
+ ...(target.hash ? { hash: target.hash } : {}),
384
+ ...(target.hashScrollIntoView === undefined
385
+ ? {}
386
+ : { hashScrollIntoView: target.hashScrollIntoView }),
387
+ ...(preload === undefined ? {} : { preload }),
388
+ };
389
+ };
390
+
391
+ /** Supplies the actual selected router; native links still render its Link. */
392
+ export const I18nRouterNavigationProvider = ({
393
+ children,
394
+ }: React.PropsWithChildren) => {
395
+ const value = useIntegratedRouterAdapter();
396
+ return (
397
+ <I18nNavigationProvider value={value}>{children}</I18nNavigationProvider>
398
+ );
399
+ };
package/src/options.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
2
+ import type { I18nPluginOptions as NativeRuntimeOptions } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
3
+ import type { I18nPluginOptions as NativeServerOptions } from '@modern-js/plugin-i18n/server';
4
+
5
+ export type IntegratedLocaleDetectionOptions = NonNullable<
6
+ NativeRuntimeOptions['localeDetection']
7
+ > & {
8
+ localisedUrls?: LocalisedUrlsOption;
9
+ localeDetectionByEntry?: Record<string, IntegratedLocaleDetectionOptions>;
10
+ };
11
+
12
+ export type I18nPluginOptions = NativeRuntimeOptions & {
13
+ localeDetection?: IntegratedLocaleDetectionOptions;
14
+ };
15
+
16
+ export type I18nServerPluginOptions = NativeServerOptions & {
17
+ localeDetection: IntegratedLocaleDetectionOptions;
18
+ };
@@ -0,0 +1,5 @@
1
+ import { i18nPlugin as nativeI18nPlugin } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
2
+ import { composeI18nPlugin } from './runtimeComposition';
3
+
4
+ export const i18nPlugin = composeI18nPlugin(nativeI18nPlugin);
5
+ export default i18nPlugin;
package/src/runtime.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { i18nPlugin as nativeI18nPlugin } from '@modern-js/plugin-i18n/runtime';
2
+ import { composeI18nPlugin } from './runtimeComposition';
3
+
4
+ export const i18nPlugin = composeI18nPlugin(nativeI18nPlugin);
5
+ export default i18nPlugin;
@@ -0,0 +1,33 @@
1
+ import type { I18nPluginOptions as NativeI18nPluginOptions } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
2
+ import type { RuntimePlugin } from '@modern-js/runtime';
3
+ import { registerFederatedI18nBoundary } from './federation/registration';
4
+ import { I18nLanguageSynchronization } from './languageSynchronization';
5
+ import { I18nRouterNavigationProvider } from './navigation';
6
+ import type { I18nPluginOptions } from './options';
7
+ import { createI18nUrlStrategy } from './urlStrategy';
8
+
9
+ export const composeI18nPlugin =
10
+ (nativeI18nPlugin: (options: NativeI18nPluginOptions) => RuntimePlugin) =>
11
+ (options: I18nPluginOptions = {}): RuntimePlugin => {
12
+ const {
13
+ localisedUrls,
14
+ localeDetectionByEntry: _entryOptions,
15
+ ...nativeLocaleDetection
16
+ } = options.localeDetection ?? {};
17
+ const native = nativeI18nPlugin({
18
+ ...options,
19
+ localeDetection: nativeLocaleDetection,
20
+ urlStrategy: options.urlStrategy ?? createI18nUrlStrategy(localisedUrls),
21
+ NavigationProvider:
22
+ options.NavigationProvider ?? I18nRouterNavigationProvider,
23
+ LanguageSynchronization:
24
+ options.LanguageSynchronization ?? I18nLanguageSynchronization,
25
+ });
26
+ return {
27
+ ...native,
28
+ setup(api) {
29
+ registerFederatedI18nBoundary(api);
30
+ return native.setup?.(api);
31
+ },
32
+ };
33
+ };
package/src/server.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { getLocaleDetectionOptions } from '@modern-js/plugin-i18n/config';
2
+ import { i18nServerPlugin as nativeI18nServerPlugin } from '@modern-js/plugin-i18n/server';
3
+ import type {
4
+ I18nServerPluginOptions,
5
+ IntegratedLocaleDetectionOptions,
6
+ } from './options';
7
+ import { createI18nUrlStrategy } from './urlStrategy';
8
+
9
+ export const i18nServerPlugin = (options: I18nServerPluginOptions) =>
10
+ nativeI18nServerPlugin({
11
+ ...options,
12
+ resolveUrlStrategy:
13
+ options.resolveUrlStrategy ??
14
+ (entryName =>
15
+ createI18nUrlStrategy(
16
+ options.localeDetection
17
+ ? (
18
+ getLocaleDetectionOptions(
19
+ entryName,
20
+ options.localeDetection,
21
+ ) as IntegratedLocaleDetectionOptions
22
+ ).localisedUrls
23
+ : undefined,
24
+ )),
25
+ });
26
+
27
+ export default i18nServerPlugin;
@@ -0,0 +1,19 @@
1
+ import {
2
+ canonicalTargetPathname,
3
+ isDefaultLocaleRedirectSkipPath,
4
+ type LocalisedUrlsOption,
5
+ localiseTargetPathname,
6
+ } from '@modern-js/i18n-runtime-extensions';
7
+ import type { I18nUrlStrategy } from '@modern-js/plugin-i18n/runtime/no-react-i18next';
8
+
9
+ /** Each integration owns its map; no process-wide configuration is installed. */
10
+ export const createI18nUrlStrategy = (
11
+ localisedUrls?: LocalisedUrlsOption,
12
+ ): I18nUrlStrategy => ({
13
+ localizePathname: (pathname, language, languages) =>
14
+ localiseTargetPathname(pathname, language, [...languages], localisedUrls),
15
+ canonicalPathname: (pathname, languages) =>
16
+ canonicalTargetPathname(pathname, [...languages], localisedUrls),
17
+ shouldSkipRedirect: (pathname, languages) =>
18
+ isDefaultLocaleRedirectSkipPath(pathname, [...languages]),
19
+ });
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @bleedingdev/modern-js-i18n-integration
2
-
3
- This 0.0.0 version only registers the package name so npm trusted publishing can be configured. It contains no runtime implementation. Use the subsequent UltraModern.js release, published from the repository workflow with provenance.