@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.
- package/LICENSE +21 -0
- package/dist/cjs/cli.js +97 -0
- package/dist/cjs/federation/boundary.js +100 -0
- package/dist/cjs/federation/registration.js +43 -0
- package/dist/cjs/languageSynchronization.js +44 -0
- package/dist/cjs/navigation.js +220 -0
- package/dist/cjs/options.js +18 -0
- package/dist/cjs/runtime-no-react-i18next.js +46 -0
- package/dist/cjs/runtime.js +46 -0
- package/dist/cjs/runtimeComposition.js +60 -0
- package/dist/cjs/server.js +50 -0
- package/dist/cjs/urlStrategy.js +51 -0
- package/dist/esm/cli.mjs +57 -0
- package/dist/esm/federation/boundary.mjs +62 -0
- package/dist/esm/federation/registration.mjs +5 -0
- package/dist/esm/languageSynchronization.mjs +6 -0
- package/dist/esm/navigation.mjs +182 -0
- package/dist/esm/options.mjs +0 -0
- package/dist/esm/runtime-no-react-i18next.mjs +6 -0
- package/dist/esm/runtime.mjs +6 -0
- package/dist/esm/runtimeComposition.mjs +22 -0
- package/dist/esm/server.mjs +10 -0
- package/dist/esm/urlStrategy.mjs +13 -0
- package/dist/esm-node/cli.mjs +58 -0
- package/dist/esm-node/federation/boundary.mjs +63 -0
- package/dist/esm-node/federation/registration.mjs +6 -0
- package/dist/esm-node/languageSynchronization.mjs +7 -0
- package/dist/esm-node/navigation.mjs +183 -0
- package/dist/esm-node/options.mjs +1 -0
- package/dist/esm-node/runtime-no-react-i18next.mjs +7 -0
- package/dist/esm-node/runtime.mjs +7 -0
- package/dist/esm-node/runtimeComposition.mjs +23 -0
- package/dist/esm-node/server.mjs +11 -0
- package/dist/esm-node/urlStrategy.mjs +14 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/federation/boundary.d.ts +9 -0
- package/dist/types/federation/registration.d.ts +2 -0
- package/dist/types/languageSynchronization.d.ts +3 -0
- package/dist/types/navigation.d.ts +3 -0
- package/dist/types/options.d.ts +13 -0
- package/dist/types/runtime-no-react-i18next.d.ts +2 -0
- package/dist/types/runtime.d.ts +2 -0
- package/dist/types/runtimeComposition.d.ts +4 -0
- package/dist/types/server.d.ts +3 -0
- package/dist/types/urlStrategy.d.ts +4 -0
- package/package.json +139 -5
- package/src/cli.ts +72 -0
- package/src/federation/boundary.tsx +129 -0
- package/src/federation/registration.ts +8 -0
- package/src/languageSynchronization.tsx +23 -0
- package/src/navigation.tsx +399 -0
- package/src/options.ts +18 -0
- package/src/runtime-no-react-i18next.ts +5 -0
- package/src/runtime.ts +5 -0
- package/src/runtimeComposition.ts +33 -0
- package/src/server.ts +27 -0
- package/src/urlStrategy.ts +19 -0
- 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
|
+
};
|
package/src/runtime.ts
ADDED
|
@@ -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.
|