@bleedingdev/modern-js-plugin-i18n 3.9.0-ultramodern.2 → 3.9.0-ultramodern.21
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/dist/cjs/cli/index.js +10 -21
- package/dist/cjs/cli/localisedRoutes.js +47 -0
- package/dist/cjs/runtime/Link.js +19 -32
- package/dist/cjs/runtime/context.js +30 -73
- package/dist/cjs/runtime/contextHelpers.js +5 -5
- package/dist/cjs/runtime/contexts.js +49 -0
- package/dist/cjs/runtime/core.js +27 -14
- package/dist/cjs/runtime/hooks.js +7 -8
- package/dist/cjs/runtime/localizedPaths.js +11 -9
- package/dist/cjs/runtime/navigation.js +48 -0
- package/dist/cjs/runtime/providerComposition.js +37 -13
- package/dist/cjs/runtime/routerAdapter.js +33 -140
- package/dist/cjs/runtime/sharedContexts.js +43 -0
- package/dist/cjs/runtime/utils.js +21 -19
- package/dist/cjs/server/defaultUrlStrategy.js +48 -0
- package/dist/cjs/server/index.js +11 -7
- package/dist/cjs/server/mappedUrlStrategy.js +43 -0
- package/dist/cjs/server/redirectPolicy.js +33 -24
- package/dist/cjs/shared/mappedUrlStrategy.js +63 -0
- package/dist/cjs/shared/urlStrategy.js +48 -0
- package/dist/esm/cli/index.mjs +10 -21
- package/dist/esm/cli/localisedRoutes.mjs +9 -0
- package/dist/esm/runtime/Link.mjs +19 -32
- package/dist/esm/runtime/context.mjs +20 -72
- package/dist/esm/runtime/contextHelpers.mjs +5 -5
- package/dist/esm/runtime/contexts.mjs +5 -0
- package/dist/esm/runtime/core.mjs +13 -6
- package/dist/esm/runtime/hooks.mjs +7 -8
- package/dist/esm/runtime/localizedPaths.mjs +11 -9
- package/dist/esm/runtime/navigation.mjs +7 -0
- package/dist/esm/runtime/providerComposition.mjs +38 -14
- package/dist/esm/runtime/routerAdapter.mjs +28 -141
- package/dist/esm/runtime/sharedContexts.mjs +1 -0
- package/dist/esm/runtime/utils.mjs +19 -17
- package/dist/esm/server/defaultUrlStrategy.mjs +7 -0
- package/dist/esm/server/index.mjs +11 -7
- package/dist/esm/server/mappedUrlStrategy.mjs +1 -0
- package/dist/esm/server/redirectPolicy.mjs +33 -24
- package/dist/esm/shared/mappedUrlStrategy.mjs +19 -0
- package/dist/esm/shared/urlStrategy.mjs +7 -0
- package/dist/esm-node/cli/index.mjs +10 -21
- package/dist/esm-node/cli/localisedRoutes.mjs +10 -0
- package/dist/esm-node/runtime/Link.mjs +19 -32
- package/dist/esm-node/runtime/context.mjs +20 -72
- package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
- package/dist/esm-node/runtime/contexts.mjs +6 -0
- package/dist/esm-node/runtime/core.mjs +13 -6
- package/dist/esm-node/runtime/hooks.mjs +7 -8
- package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
- package/dist/esm-node/runtime/navigation.mjs +8 -0
- package/dist/esm-node/runtime/providerComposition.mjs +38 -14
- package/dist/esm-node/runtime/routerAdapter.mjs +28 -141
- package/dist/esm-node/runtime/sharedContexts.mjs +2 -0
- package/dist/esm-node/runtime/utils.mjs +19 -17
- package/dist/esm-node/server/defaultUrlStrategy.mjs +8 -0
- package/dist/esm-node/server/index.mjs +11 -7
- package/dist/esm-node/server/mappedUrlStrategy.mjs +2 -0
- package/dist/esm-node/server/redirectPolicy.mjs +33 -24
- package/dist/esm-node/shared/mappedUrlStrategy.mjs +20 -0
- package/dist/esm-node/shared/urlStrategy.mjs +8 -0
- package/dist/types/cli/localisedRoutes.d.ts +19 -0
- package/dist/types/runtime/context.d.ts +8 -11
- package/dist/types/runtime/contextHelpers.d.ts +2 -2
- package/dist/types/runtime/contexts.d.ts +6 -0
- package/dist/types/runtime/core.d.ts +12 -2
- package/dist/types/runtime/hooks.d.ts +4 -5
- package/dist/types/runtime/localizedPaths.d.ts +4 -4
- package/dist/types/runtime/navigation.d.ts +10 -0
- package/dist/types/runtime/providerComposition.d.ts +13 -1
- package/dist/types/runtime/routerAdapter.d.ts +23 -6
- package/dist/types/runtime/sharedContexts.d.ts +1 -0
- package/dist/types/runtime/utils.d.ts +4 -12
- package/dist/types/server/defaultUrlStrategy.d.ts +11 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/mappedUrlStrategy.d.ts +7 -0
- package/dist/types/server/redirectPolicy.d.ts +5 -13
- package/dist/types/shared/mappedUrlStrategy.d.ts +27 -0
- package/dist/types/shared/type.d.ts +7 -11
- package/dist/types/shared/urlStrategy.d.ts +18 -0
- package/package.json +42 -19
- package/rslib.config.mts +8 -1
- package/rstest.config.mts +6 -5
- package/src/cli/index.ts +42 -42
- package/src/cli/localisedRoutes.ts +53 -0
- package/src/runtime/Link.tsx +20 -40
- package/src/runtime/context.tsx +33 -157
- package/src/runtime/contextHelpers.ts +7 -5
- package/src/runtime/contexts.ts +16 -0
- package/src/runtime/core.tsx +53 -6
- package/src/runtime/hooks.ts +14 -9
- package/src/runtime/i18n/instance.ts +1 -2
- package/src/runtime/localizedPaths.ts +24 -19
- package/src/runtime/navigation.tsx +20 -0
- package/src/runtime/providerComposition.tsx +71 -23
- package/src/runtime/routerAdapter.tsx +65 -339
- package/src/runtime/sharedContexts.ts +8 -0
- package/src/runtime/utils.ts +49 -31
- package/src/server/defaultUrlStrategy.ts +27 -0
- package/src/server/index.ts +39 -9
- package/src/server/mappedUrlStrategy.ts +10 -0
- package/src/server/redirectPolicy.ts +85 -70
- package/src/shared/mappedUrlStrategy.ts +59 -0
- package/src/shared/type.ts +7 -11
- package/src/shared/urlStrategy.ts +38 -0
- package/tests/backendDefaults.test.ts +2 -23
- package/tests/defaultUrlStrategy.test.ts +107 -0
- package/tests/federatedI18nBoundaryResolver.test.tsx +65 -0
- package/tests/i18nUtils.test.ts +31 -65
- package/tests/link.test.tsx +13 -511
- package/tests/localisedRouteGeneration.test.ts +200 -0
- package/tests/localisedUrls.test.ts +16 -208
- package/tests/mappedUrlStrategyDerivation.test.ts +62 -0
- package/tests/navigation.test.tsx +403 -0
- package/tests/reactI18nextRuntimeBoundary.test.ts +8 -83
- package/tests/redirectPolicy.test.ts +89 -27
- package/tests/routerAdapter.test.tsx +98 -1171
- package/tests/urlStrategySerialization.test.ts +122 -0
- package/tsconfig.json +4 -2
- package/tests/federatedI18nBoundary.client.test.tsx +0 -129
- package/tests/federatedI18nBoundary.test.tsx +0 -132
- package/tests/linkTypes.test.ts +0 -59
- package/tests/localisedUrlRewriteMatrix.fork.test.ts +0 -147
- package/tests/peerCohort.test.ts +0 -25
- package/tests/type-fixture/fixture-globals.d.ts +0 -11
- package/tests/type-fixture/i18nInstanceTypes.fixture.ts +0 -29
- package/tests/type-fixture/linkTypes.fixture.tsx +0 -51
- package/tests/type-fixture/tsconfig.json +0 -16
package/src/runtime/Link.tsx
CHANGED
|
@@ -104,11 +104,11 @@ export const Link = <TTo extends string = string>(
|
|
|
104
104
|
} = props as LinkBaseProps & { to: string; params?: LinkParams };
|
|
105
105
|
|
|
106
106
|
const adapter = useI18nRouterAdapter();
|
|
107
|
-
const { language, supportedLanguages,
|
|
107
|
+
const { language, supportedLanguages, urlStrategy } = useModernI18n();
|
|
108
108
|
|
|
109
109
|
const config: LocalizedPathsConfig = {
|
|
110
110
|
languages: supportedLanguages,
|
|
111
|
-
|
|
111
|
+
urlStrategy,
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
@@ -139,7 +139,7 @@ export const Link = <TTo extends string = string>(
|
|
|
139
139
|
interpolated,
|
|
140
140
|
language,
|
|
141
141
|
supportedLanguages,
|
|
142
|
-
|
|
142
|
+
urlStrategy,
|
|
143
143
|
);
|
|
144
144
|
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
145
145
|
const { searchString, searchObject } = normalizeSearch(
|
|
@@ -164,7 +164,7 @@ export const Link = <TTo extends string = string>(
|
|
|
164
164
|
isBareHash,
|
|
165
165
|
language,
|
|
166
166
|
supportedLanguages,
|
|
167
|
-
|
|
167
|
+
urlStrategy,
|
|
168
168
|
]);
|
|
169
169
|
|
|
170
170
|
const isActive = useMemo(() => {
|
|
@@ -190,7 +190,7 @@ export const Link = <TTo extends string = string>(
|
|
|
190
190
|
adapter.location,
|
|
191
191
|
activeOptions?.exact,
|
|
192
192
|
supportedLanguages,
|
|
193
|
-
|
|
193
|
+
urlStrategy,
|
|
194
194
|
]);
|
|
195
195
|
|
|
196
196
|
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
@@ -214,7 +214,7 @@ export const Link = <TTo extends string = string>(
|
|
|
214
214
|
);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const { Link: RouterLink, hasRouter
|
|
217
|
+
const { Link: RouterLink, hasRouter } = adapter;
|
|
218
218
|
|
|
219
219
|
if (!hasRouter || !RouterLink) {
|
|
220
220
|
const { replace: _replace, ...anchorProps } = rest;
|
|
@@ -252,43 +252,23 @@ export const Link = <TTo extends string = string>(
|
|
|
252
252
|
...(activeStyle as React.CSSProperties | undefined),
|
|
253
253
|
};
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// TanStack navigation.
|
|
269
|
-
return (
|
|
270
|
-
<RouterLink
|
|
271
|
-
to={target.localizedPathname}
|
|
272
|
-
{...(target.searchObject ? { search: target.searchObject } : {})}
|
|
273
|
-
{...(target.hash ? { hash: target.hash } : {})}
|
|
274
|
-
{...(hashScrollIntoView === undefined ? {} : { hashScrollIntoView })}
|
|
275
|
-
{...(tanstackPreload === undefined ? {} : { preload: tanstackPreload })}
|
|
276
|
-
{...rest}
|
|
277
|
-
{...activeRest}
|
|
278
|
-
{...activeAttributes}
|
|
279
|
-
className={mergedClassName}
|
|
280
|
-
style={mergedStyle}
|
|
281
|
-
>
|
|
282
|
-
{children}
|
|
283
|
-
</RouterLink>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
255
|
+
const routerProps = adapter.createLinkProps?.({
|
|
256
|
+
pathname: target.localizedPathname,
|
|
257
|
+
href: target.href,
|
|
258
|
+
search: target.searchObject,
|
|
259
|
+
hash: target.hash,
|
|
260
|
+
hashScrollIntoView,
|
|
261
|
+
prefetch,
|
|
262
|
+
preload,
|
|
263
|
+
}) ?? {
|
|
264
|
+
to: target.href,
|
|
265
|
+
...(prefetch === undefined ? {} : { prefetch }),
|
|
266
|
+
...(preload === undefined ? {} : { preload }),
|
|
267
|
+
};
|
|
286
268
|
|
|
287
269
|
return (
|
|
288
270
|
<RouterLink
|
|
289
|
-
|
|
290
|
-
{...(prefetch === undefined ? {} : { prefetch })}
|
|
291
|
-
{...(preload === undefined ? {} : { preload })}
|
|
271
|
+
{...routerProps}
|
|
292
272
|
{...rest}
|
|
293
273
|
{...activeRest}
|
|
294
274
|
{...activeAttributes}
|
package/src/runtime/context.tsx
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { RuntimeComponentResolverContext } from '@modern-js/runtime/context';
|
|
2
2
|
import type { ComponentType, FC, ReactNode } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
} from 'react';
|
|
3
|
+
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
|
4
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
10
5
|
import {
|
|
11
6
|
changeModernI18nLanguage,
|
|
12
7
|
getPathLanguage,
|
|
@@ -16,10 +11,16 @@ import {
|
|
|
16
11
|
} from './contextHelpers';
|
|
17
12
|
import type { I18nInstance } from './i18n';
|
|
18
13
|
import type { Resources } from './i18n/instance';
|
|
19
|
-
|
|
14
|
+
|
|
20
15
|
import { useI18nRouterAdapter } from './routerAdapter';
|
|
16
|
+
import {
|
|
17
|
+
ModernI18nContext,
|
|
18
|
+
ReactI18nextProviderContext,
|
|
19
|
+
} from './sharedContexts';
|
|
20
|
+
|
|
21
|
+
export { getActualI18nextInstance } from './i18n/instance';
|
|
21
22
|
|
|
22
|
-
interface ModernI18nContextValue {
|
|
23
|
+
export interface ModernI18nContextValue {
|
|
23
24
|
language: string;
|
|
24
25
|
i18nInstance: I18nInstance;
|
|
25
26
|
// Plugin configuration for useModernI18n hook
|
|
@@ -27,39 +28,15 @@ interface ModernI18nContextValue {
|
|
|
27
28
|
languages?: string[];
|
|
28
29
|
localePathRedirect?: boolean;
|
|
29
30
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
30
|
-
|
|
31
|
+
urlStrategy?: I18nUrlStrategy;
|
|
31
32
|
// Callback to update language in context
|
|
32
33
|
updateLanguage?: (newLang: string) => void;
|
|
33
34
|
synchronizeLanguage?: (newLang: string) => void;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
'@modern-js/plugin-i18n/runtime/ModernI18nContext',
|
|
38
|
-
);
|
|
39
|
-
const reactI18nextProviderContextKey = Symbol.for(
|
|
40
|
-
'@modern-js/plugin-i18n/runtime/ReactI18nextProviderContext',
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
type GlobalContextStore<T> = typeof globalThis & {
|
|
44
|
-
[key: symbol]: ReturnType<typeof createContext<T>> | undefined;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const getGlobalContext = <T,>(key: symbol, defaultValue: T) => {
|
|
48
|
-
const globalStore = globalThis as GlobalContextStore<T>;
|
|
49
|
-
globalStore[key] ??= createContext<T>(defaultValue);
|
|
50
|
-
return globalStore[key];
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const ModernI18nContext = getGlobalContext<ModernI18nContextValue | null>(
|
|
54
|
-
modernI18nContextKey,
|
|
55
|
-
null,
|
|
56
|
-
);
|
|
57
|
-
const ReactI18nextProviderContext = getGlobalContext<ComponentType<any> | null>(
|
|
58
|
-
reactI18nextProviderContextKey,
|
|
59
|
-
null,
|
|
60
|
-
);
|
|
37
|
+
export { ModernI18nContext, ReactI18nextProviderContext };
|
|
61
38
|
|
|
62
|
-
interface ModernI18nProviderProps {
|
|
39
|
+
export interface ModernI18nProviderProps {
|
|
63
40
|
children: ReactNode;
|
|
64
41
|
i18nextProvider?: ComponentType<any> | null;
|
|
65
42
|
value: ModernI18nContextValue;
|
|
@@ -92,123 +69,22 @@ export interface FederatedI18nBoundaryProps {
|
|
|
92
69
|
supportedLanguages?: string[];
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* resources used below this boundary.
|
|
99
|
-
*/
|
|
100
|
-
export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = ({
|
|
101
|
-
children,
|
|
102
|
-
defaultNamespace,
|
|
103
|
-
fallbackLanguage,
|
|
104
|
-
resources,
|
|
105
|
-
supportedLanguages,
|
|
106
|
-
}) => {
|
|
107
|
-
const parent = useContext(ModernI18nContext);
|
|
108
|
-
const I18nextProvider = useContext(ReactI18nextProviderContext);
|
|
109
|
-
if (!parent) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
'FederatedI18nBoundary must be used within ModernI18nProvider',
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const languages =
|
|
116
|
-
supportedLanguages ?? parent.languages ?? Object.keys(resources);
|
|
117
|
-
const scopedInstance = useMemo(() => {
|
|
118
|
-
const parentInstance = getActualI18nextInstance(parent.i18nInstance);
|
|
119
|
-
const clone = parentInstance.cloneInstance?.({
|
|
120
|
-
defaultNS: defaultNamespace,
|
|
121
|
-
fallbackLng: fallbackLanguage ?? languages[0] ?? parent.language,
|
|
122
|
-
forkResourceStore: true,
|
|
123
|
-
initImmediate: false,
|
|
124
|
-
lng: parent.language,
|
|
125
|
-
ns: [defaultNamespace],
|
|
126
|
-
resources,
|
|
127
|
-
supportedLngs: languages,
|
|
128
|
-
});
|
|
129
|
-
if (!clone) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
'FederatedI18nBoundary requires an i18n instance with cloneInstance support',
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
const resourceStore = clone.store;
|
|
135
|
-
const parentResourceStore = parentInstance.store;
|
|
136
|
-
const sharesNestedResourceState = Object.entries(
|
|
137
|
-
resourceStore?.data ?? {},
|
|
138
|
-
).some(([language, namespaces]) => {
|
|
139
|
-
const parentNamespaces = parentResourceStore?.data?.[language];
|
|
140
|
-
return (
|
|
141
|
-
namespaces === parentNamespaces ||
|
|
142
|
-
Object.entries(namespaces).some(
|
|
143
|
-
([namespace, resource]) =>
|
|
144
|
-
resource !== null &&
|
|
145
|
-
typeof resource === 'object' &&
|
|
146
|
-
resource === parentNamespaces?.[namespace],
|
|
147
|
-
)
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
if (
|
|
151
|
-
clone === parentInstance ||
|
|
152
|
-
resourceStore === parentResourceStore ||
|
|
153
|
-
resourceStore?.data === parentResourceStore?.data ||
|
|
154
|
-
sharesNestedResourceState
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
'FederatedI18nBoundary cloneInstance did not isolate the host resource store',
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (
|
|
161
|
-
!resourceStore?.addResourceBundle ||
|
|
162
|
-
!clone.removeResourceBundle ||
|
|
163
|
-
!resourceStore.data
|
|
164
|
-
) {
|
|
165
|
-
throw new Error(
|
|
166
|
-
'FederatedI18nBoundary requires an isolated mutable i18n resource store',
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
for (const [language, namespaces] of Object.entries(resourceStore.data)) {
|
|
170
|
-
for (const namespace of Object.keys(namespaces)) {
|
|
171
|
-
clone.removeResourceBundle(language, namespace);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const [language, namespaces] of Object.entries(resources)) {
|
|
175
|
-
for (const [namespace, resource] of Object.entries(namespaces)) {
|
|
176
|
-
resourceStore.addResourceBundle(
|
|
177
|
-
language,
|
|
178
|
-
namespace,
|
|
179
|
-
resource as Record<string, string>,
|
|
180
|
-
true,
|
|
181
|
-
true,
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return clone;
|
|
186
|
-
}, [
|
|
187
|
-
defaultNamespace,
|
|
188
|
-
fallbackLanguage,
|
|
189
|
-
languages,
|
|
190
|
-
parent.i18nInstance,
|
|
191
|
-
parent.language,
|
|
192
|
-
resources,
|
|
193
|
-
]);
|
|
194
|
-
const value = useMemo(
|
|
195
|
-
() => ({
|
|
196
|
-
...parent,
|
|
197
|
-
i18nInstance: scopedInstance,
|
|
198
|
-
language: parent.language,
|
|
199
|
-
languages,
|
|
200
|
-
}),
|
|
201
|
-
[languages, parent, scopedInstance],
|
|
72
|
+
const MissingFederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = () => {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'FederatedI18nBoundary requires the @modern-js/i18n-integration runtime plugin',
|
|
202
75
|
);
|
|
76
|
+
};
|
|
203
77
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
78
|
+
export const FederatedI18nBoundary: FC<FederatedI18nBoundaryProps> = props => {
|
|
79
|
+
const resolveComponent = useContext(RuntimeComponentResolverContext);
|
|
80
|
+
const Boundary =
|
|
81
|
+
resolveComponent?.(MissingFederatedI18nBoundary, {
|
|
82
|
+
name: 'i18n.FederatedI18nBoundary',
|
|
83
|
+
}) ?? MissingFederatedI18nBoundary;
|
|
84
|
+
if (Boundary === FederatedI18nBoundary) {
|
|
85
|
+
throw new Error('FederatedI18nBoundary resolver returned its own wrapper');
|
|
86
|
+
}
|
|
87
|
+
return <Boundary {...props} />;
|
|
212
88
|
};
|
|
213
89
|
|
|
214
90
|
export interface UseModernI18nReturn<
|
|
@@ -219,7 +95,7 @@ export interface UseModernI18nReturn<
|
|
|
219
95
|
t: (key: string | string[], ...args: any[]) => string;
|
|
220
96
|
i18nInstance: TInstance;
|
|
221
97
|
supportedLanguages: string[];
|
|
222
|
-
|
|
98
|
+
urlStrategy?: I18nUrlStrategy;
|
|
223
99
|
isLanguageSupported: (lang: string) => boolean;
|
|
224
100
|
// Indicates whether translation resources for current language are ready
|
|
225
101
|
isResourcesReady: boolean;
|
|
@@ -258,7 +134,7 @@ export const useModernI18n = <
|
|
|
258
134
|
languages,
|
|
259
135
|
localePathRedirect,
|
|
260
136
|
ignoreRedirectRoutes,
|
|
261
|
-
|
|
137
|
+
urlStrategy,
|
|
262
138
|
updateLanguage,
|
|
263
139
|
synchronizeLanguage,
|
|
264
140
|
} = context;
|
|
@@ -295,7 +171,7 @@ export const useModernI18n = <
|
|
|
295
171
|
updateLanguage,
|
|
296
172
|
localePathRedirect,
|
|
297
173
|
ignoreRedirectRoutes,
|
|
298
|
-
|
|
174
|
+
urlStrategy,
|
|
299
175
|
languages,
|
|
300
176
|
hasRouter,
|
|
301
177
|
navigate,
|
|
@@ -306,7 +182,7 @@ export const useModernI18n = <
|
|
|
306
182
|
updateLanguage,
|
|
307
183
|
localePathRedirect,
|
|
308
184
|
ignoreRedirectRoutes,
|
|
309
|
-
|
|
185
|
+
urlStrategy,
|
|
310
186
|
languages,
|
|
311
187
|
hasRouter,
|
|
312
188
|
navigate,
|
|
@@ -341,7 +217,7 @@ export const useModernI18n = <
|
|
|
341
217
|
// narrows to the concrete instance type via the TInstance type argument.
|
|
342
218
|
i18nInstance: i18nInstance as TInstance,
|
|
343
219
|
supportedLanguages: languages || [],
|
|
344
|
-
|
|
220
|
+
urlStrategy,
|
|
345
221
|
isLanguageSupported,
|
|
346
222
|
isResourcesReady,
|
|
347
223
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
2
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
3
3
|
import type { I18nInstance } from './i18n';
|
|
4
4
|
import type { SdkBackend } from './i18n/backend/sdk-backend';
|
|
5
5
|
import { cacheUserLanguage } from './i18n/detection';
|
|
@@ -66,7 +66,7 @@ interface ChangeModernI18nLanguageOptions {
|
|
|
66
66
|
updateLanguage?: (newLang: string) => void;
|
|
67
67
|
localePathRedirect?: boolean;
|
|
68
68
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
69
|
-
|
|
69
|
+
urlStrategy?: I18nUrlStrategy;
|
|
70
70
|
languages?: string[];
|
|
71
71
|
hasRouter: boolean;
|
|
72
72
|
navigate?: Navigate | null;
|
|
@@ -82,7 +82,7 @@ export async function changeModernI18nLanguage(
|
|
|
82
82
|
updateLanguage,
|
|
83
83
|
localePathRedirect,
|
|
84
84
|
ignoreRedirectRoutes,
|
|
85
|
-
|
|
85
|
+
urlStrategy,
|
|
86
86
|
languages,
|
|
87
87
|
hasRouter,
|
|
88
88
|
navigate,
|
|
@@ -123,13 +123,14 @@ export async function changeModernI18nLanguage(
|
|
|
123
123
|
relativePath,
|
|
124
124
|
languages || [],
|
|
125
125
|
ignoreRedirectRoutes,
|
|
126
|
+
urlStrategy,
|
|
126
127
|
)
|
|
127
128
|
) {
|
|
128
129
|
const newPath = buildLocalizedUrl(
|
|
129
130
|
relativePath,
|
|
130
131
|
newLang,
|
|
131
132
|
languages || [],
|
|
132
|
-
|
|
133
|
+
urlStrategy,
|
|
133
134
|
);
|
|
134
135
|
const newUrl = entryPath + newPath + location.search + location.hash;
|
|
135
136
|
|
|
@@ -155,13 +156,14 @@ export async function changeModernI18nLanguage(
|
|
|
155
156
|
relativePath,
|
|
156
157
|
languages || [],
|
|
157
158
|
ignoreRedirectRoutes,
|
|
159
|
+
urlStrategy,
|
|
158
160
|
)
|
|
159
161
|
) {
|
|
160
162
|
const newPath = buildLocalizedUrl(
|
|
161
163
|
relativePath,
|
|
162
164
|
newLang,
|
|
163
165
|
languages || [],
|
|
164
|
-
|
|
166
|
+
urlStrategy,
|
|
165
167
|
);
|
|
166
168
|
const newUrl =
|
|
167
169
|
entryPath + newPath + window.location.search + window.location.hash;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { createContext } from 'react';
|
|
3
|
+
import type { ModernI18nContextValue } from './context';
|
|
4
|
+
import type { I18nRouterAdapter } from './routerAdapter';
|
|
5
|
+
|
|
6
|
+
// Every other i18n runtime module imports these through
|
|
7
|
+
// `@modern-js/plugin-i18n/runtime/contexts` so Module Federation shares one
|
|
8
|
+
// copy of them. Their values stay provider-owned.
|
|
9
|
+
export const ModernI18nContext = createContext<ModernI18nContextValue | null>(
|
|
10
|
+
null,
|
|
11
|
+
);
|
|
12
|
+
export const ReactI18nextProviderContext =
|
|
13
|
+
createContext<ComponentType<any> | null>(null);
|
|
14
|
+
export const I18nNavigationContext = createContext<I18nRouterAdapter | null>(
|
|
15
|
+
null,
|
|
16
|
+
);
|
package/src/runtime/core.tsx
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { RuntimePlugin } from '@modern-js/runtime';
|
|
2
2
|
import type React from 'react';
|
|
3
|
+
import { resolveMappedUrlStrategy } from '../shared/mappedUrlStrategy';
|
|
3
4
|
import type {
|
|
4
5
|
BaseBackendOptions,
|
|
5
6
|
BaseLocaleDetectionOptions,
|
|
6
7
|
} from '../shared/type';
|
|
8
|
+
import { asI18nUrlStrategy, type I18nUrlStrategy } from '../shared/urlStrategy';
|
|
7
9
|
import type { I18nInitOptions, I18nInstance } from './i18n';
|
|
10
|
+
import { I18nRouterNavigationProvider } from './navigation';
|
|
8
11
|
import {
|
|
9
12
|
type RuntimeContextWithI18n,
|
|
10
13
|
setupI18nBeforeRender,
|
|
11
14
|
} from './pluginSetup';
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
createI18nRootWrapper,
|
|
17
|
+
type I18nLanguageSynchronizationProps,
|
|
18
|
+
} from './providerComposition';
|
|
19
|
+
|
|
20
|
+
export type { I18nLanguageSynchronizationProps } from './providerComposition';
|
|
21
|
+
|
|
13
22
|
import {
|
|
14
23
|
type LoadReactI18nextIntegration,
|
|
15
24
|
resolveReactI18nextIntegration,
|
|
@@ -17,16 +26,28 @@ import {
|
|
|
17
26
|
import './types';
|
|
18
27
|
|
|
19
28
|
export type { I18nSdkLoader, I18nSdkLoadOptions } from '../shared/type';
|
|
29
|
+
export type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
20
30
|
export type {
|
|
21
31
|
I18nInitOptions,
|
|
22
32
|
I18nInstance,
|
|
23
33
|
Resources,
|
|
24
34
|
TranslateFn,
|
|
25
35
|
} from './i18n/instance';
|
|
36
|
+
export {
|
|
37
|
+
I18nRouterNavigationProvider,
|
|
38
|
+
useIntegratedRouterAdapter,
|
|
39
|
+
} from './navigation';
|
|
26
40
|
export type {
|
|
27
41
|
LoadReactI18nextIntegration,
|
|
28
42
|
ReactI18nextIntegration,
|
|
29
43
|
} from './reactI18next';
|
|
44
|
+
export {
|
|
45
|
+
I18nNavigationProvider,
|
|
46
|
+
type I18nRouterAdapter,
|
|
47
|
+
type I18nRouterLinkTarget,
|
|
48
|
+
useI18nRouterAdapter,
|
|
49
|
+
useNativeI18nRouterAdapter,
|
|
50
|
+
} from './routerAdapter';
|
|
30
51
|
|
|
31
52
|
export interface I18nPluginOptions {
|
|
32
53
|
entryName?: string;
|
|
@@ -37,6 +58,9 @@ export interface I18nPluginOptions {
|
|
|
37
58
|
initOptions?: I18nInitOptions;
|
|
38
59
|
htmlLangAttr?: boolean;
|
|
39
60
|
reactI18next?: boolean;
|
|
61
|
+
urlStrategy?: I18nUrlStrategy;
|
|
62
|
+
NavigationProvider?: React.ComponentType<React.PropsWithChildren>;
|
|
63
|
+
LanguageSynchronization?: React.ComponentType<I18nLanguageSynchronizationProps>;
|
|
40
64
|
[key: string]: any;
|
|
41
65
|
}
|
|
42
66
|
|
|
@@ -55,6 +79,9 @@ export const createI18nPlugin =
|
|
|
55
79
|
backend,
|
|
56
80
|
htmlLangAttr = false,
|
|
57
81
|
reactI18next = true,
|
|
82
|
+
urlStrategy,
|
|
83
|
+
NavigationProvider,
|
|
84
|
+
LanguageSynchronization,
|
|
58
85
|
} = options;
|
|
59
86
|
const {
|
|
60
87
|
localePathRedirect = false,
|
|
@@ -63,9 +90,23 @@ export const createI18nPlugin =
|
|
|
63
90
|
fallbackLanguage = 'en',
|
|
64
91
|
detection,
|
|
65
92
|
ignoreRedirectRoutes,
|
|
66
|
-
localisedUrls,
|
|
67
93
|
} = localeDetection || {};
|
|
68
94
|
const { enabled: backendEnabled = false } = backend || {};
|
|
95
|
+
// A strategy configured in `modern.config.ts` reaches here through JSON,
|
|
96
|
+
// which drops its methods. Anything unusable is treated as absent so the
|
|
97
|
+
// built-in language-prefix behaviour applies instead of throwing.
|
|
98
|
+
//
|
|
99
|
+
// Falling back to `localeDetection.localisedUrls` makes mapped locale
|
|
100
|
+
// URLs work for a bare `appTools()` consumer: the map is plain data, so
|
|
101
|
+
// it survives the JSON boundary that a `urlStrategy` object does not.
|
|
102
|
+
// The server already derives its policy from the same field, and the two
|
|
103
|
+
// must agree — otherwise SSR renders `/cs/obchodni-podminky` and the
|
|
104
|
+
// hydrated `<Link>` navigates to `/cs/terms-of-service`, which 404s.
|
|
105
|
+
// An explicit, usable `urlStrategy` (supplied by a composing runtime
|
|
106
|
+
// plugin) still wins.
|
|
107
|
+
const resolvedUrlStrategy =
|
|
108
|
+
asI18nUrlStrategy(urlStrategy) ??
|
|
109
|
+
resolveMappedUrlStrategy(localeDetection);
|
|
69
110
|
let latestI18nInstance: I18nInstance | undefined;
|
|
70
111
|
let I18nextProvider: React.ComponentType<any> | null;
|
|
71
112
|
|
|
@@ -100,7 +141,15 @@ export const createI18nPlugin =
|
|
|
100
141
|
languages,
|
|
101
142
|
fallbackLanguage,
|
|
102
143
|
ignoreRedirectRoutes,
|
|
103
|
-
|
|
144
|
+
urlStrategy: resolvedUrlStrategy,
|
|
145
|
+
// The selected router, not react-router, is what a localized `<Link>`
|
|
146
|
+
// has to navigate through, and the i18n language has to follow a
|
|
147
|
+
// client-side navigation to a mapped URL. Both need the actual router
|
|
148
|
+
// instance, so a bare `appTools()` consumer gets the integrated
|
|
149
|
+
// adapter by default; a composing runtime plugin can still replace it.
|
|
150
|
+
NavigationProvider:
|
|
151
|
+
NavigationProvider ?? I18nRouterNavigationProvider,
|
|
152
|
+
LanguageSynchronization,
|
|
104
153
|
getLatestI18nInstance: () => latestI18nInstance,
|
|
105
154
|
getI18nextProvider: () => I18nextProvider,
|
|
106
155
|
}),
|
|
@@ -128,12 +177,10 @@ export {
|
|
|
128
177
|
type LinkProps,
|
|
129
178
|
} from './Link';
|
|
130
179
|
export {
|
|
131
|
-
canonicalPath,
|
|
132
180
|
type LocalizedPathsConfig,
|
|
133
|
-
localizePath,
|
|
134
181
|
type UseLocalizedLocationReturn,
|
|
135
182
|
type UseLocalizedPathsReturn,
|
|
136
183
|
useLocalizedLocation,
|
|
137
184
|
useLocalizedPaths,
|
|
138
185
|
} from './localizedPaths';
|
|
139
|
-
export {
|
|
186
|
+
export { splitUrlTarget } from './utils';
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import { isBrowser } from '@modern-js/runtime';
|
|
3
2
|
import type React from 'react';
|
|
4
3
|
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy';
|
|
5
5
|
import {
|
|
6
6
|
cacheI18nLanguage,
|
|
7
7
|
changeI18nInstanceLanguage,
|
|
@@ -54,9 +54,8 @@ export function createContextValue(
|
|
|
54
54
|
languages: string[],
|
|
55
55
|
localePathRedirect: boolean,
|
|
56
56
|
ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined,
|
|
57
|
-
localisedUrls: LocalisedUrlsOption | undefined,
|
|
58
57
|
setLang: (lang: string) => void,
|
|
59
|
-
|
|
58
|
+
urlStrategy?: I18nUrlStrategy,
|
|
60
59
|
) {
|
|
61
60
|
const instance = i18nInstance || createMinimalI18nInstance(lang);
|
|
62
61
|
return {
|
|
@@ -66,9 +65,8 @@ export function createContextValue(
|
|
|
66
65
|
languages,
|
|
67
66
|
localePathRedirect,
|
|
68
67
|
ignoreRedirectRoutes,
|
|
69
|
-
|
|
68
|
+
urlStrategy,
|
|
70
69
|
updateLanguage: setLang,
|
|
71
|
-
synchronizeLanguage,
|
|
72
70
|
};
|
|
73
71
|
}
|
|
74
72
|
|
|
@@ -179,7 +177,7 @@ export function useClientSideRedirect(
|
|
|
179
177
|
languages: string[],
|
|
180
178
|
fallbackLanguage: string,
|
|
181
179
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean),
|
|
182
|
-
|
|
180
|
+
urlStrategy?: I18nUrlStrategy,
|
|
183
181
|
) {
|
|
184
182
|
const urlRef = useRef('');
|
|
185
183
|
const { navigate, location, hasRouter } = useI18nRouterAdapter();
|
|
@@ -220,7 +218,14 @@ export function useClientSideRedirect(
|
|
|
220
218
|
const entryPath = getEntryPath();
|
|
221
219
|
const relativePath = currentPathname.replace(entryPath, '');
|
|
222
220
|
|
|
223
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
shouldIgnoreRedirect(
|
|
223
|
+
relativePath,
|
|
224
|
+
languages,
|
|
225
|
+
ignoreRedirectRoutes,
|
|
226
|
+
urlStrategy,
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
224
229
|
return;
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -241,7 +246,7 @@ export function useClientSideRedirect(
|
|
|
241
246
|
relativePath,
|
|
242
247
|
targetLanguage,
|
|
243
248
|
languages,
|
|
244
|
-
|
|
249
|
+
urlStrategy,
|
|
245
250
|
);
|
|
246
251
|
const newUrl = entryPath + newPath + currentSearch + currentHash;
|
|
247
252
|
|
|
@@ -265,7 +270,7 @@ export function useClientSideRedirect(
|
|
|
265
270
|
languages,
|
|
266
271
|
fallbackLanguage,
|
|
267
272
|
ignoreRedirectRoutes,
|
|
268
|
-
|
|
273
|
+
urlStrategy,
|
|
269
274
|
]);
|
|
270
275
|
}
|
|
271
276
|
|
|
@@ -74,8 +74,7 @@ export type TranslateFn = (
|
|
|
74
74
|
// declares a top-level `[key: string]: any` and overloaded call-signature
|
|
75
75
|
// properties; both make i18next's `i18n` structurally unassignable to this
|
|
76
76
|
// type, so the documented `i18nInstance: i18next` usage does not typecheck.
|
|
77
|
-
// Do NOT restore upstream's shape when resolving a sync merge
|
|
78
|
-
// tests/type-fixture/i18nInstanceTypes.fixture.ts.
|
|
77
|
+
// Do NOT restore upstream's shape when resolving a sync merge.
|
|
79
78
|
export interface I18nInstance {
|
|
80
79
|
language: string;
|
|
81
80
|
isInitialized?: boolean;
|