@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
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import type { BaseLocaleDetectionOptions } from '../shared/type.js';
|
|
3
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
3
4
|
import type { I18nInstance } from './i18n/index.js';
|
|
5
|
+
export interface I18nLanguageSynchronizationProps {
|
|
6
|
+
i18nInstance: I18nInstance | undefined;
|
|
7
|
+
localePathRedirect: boolean;
|
|
8
|
+
languages: string[];
|
|
9
|
+
pathname: string | undefined;
|
|
10
|
+
prevLangRef: React.MutableRefObject<string>;
|
|
11
|
+
setLang: (language: string) => void;
|
|
12
|
+
children: (synchronizeLanguage: (language: string) => void) => React.ReactNode;
|
|
13
|
+
}
|
|
4
14
|
interface I18nRootWrapperOptions {
|
|
5
15
|
entryName?: string;
|
|
6
16
|
htmlLangAttr: boolean;
|
|
@@ -8,7 +18,9 @@ interface I18nRootWrapperOptions {
|
|
|
8
18
|
languages: string[];
|
|
9
19
|
fallbackLanguage: string;
|
|
10
20
|
ignoreRedirectRoutes?: BaseLocaleDetectionOptions['ignoreRedirectRoutes'];
|
|
11
|
-
|
|
21
|
+
urlStrategy?: I18nUrlStrategy;
|
|
22
|
+
NavigationProvider?: React.ComponentType<React.PropsWithChildren>;
|
|
23
|
+
LanguageSynchronization?: React.ComponentType<I18nLanguageSynchronizationProps>;
|
|
12
24
|
getLatestI18nInstance: () => I18nInstance | undefined;
|
|
13
25
|
getI18nextProvider: () => React.ComponentType<any> | null | undefined;
|
|
14
26
|
}
|
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
3
2
|
interface I18nRouterLocation {
|
|
4
3
|
pathname: string;
|
|
5
4
|
search: string;
|
|
6
5
|
hash: string;
|
|
7
6
|
}
|
|
8
|
-
|
|
7
|
+
type I18nRouterNavigate = (href: string, options?: {
|
|
9
8
|
replace?: boolean;
|
|
10
9
|
state?: unknown;
|
|
11
|
-
}
|
|
12
|
-
type I18nRouterNavigate = (href: string, options?: I18nRouterNavigateOptions) => void | Promise<void>;
|
|
10
|
+
}) => void | Promise<void>;
|
|
13
11
|
type I18nRouterLink = React.ComponentType<{
|
|
14
12
|
to: string;
|
|
15
13
|
children?: React.ReactNode;
|
|
16
14
|
[key: string]: unknown;
|
|
17
15
|
}>;
|
|
18
|
-
interface
|
|
19
|
-
|
|
16
|
+
export interface I18nRouterLinkTarget {
|
|
17
|
+
pathname: string;
|
|
18
|
+
href: string;
|
|
19
|
+
search?: Record<string, unknown>;
|
|
20
|
+
hash?: string;
|
|
21
|
+
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
22
|
+
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
23
|
+
preload?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface I18nRouterAdapter {
|
|
26
|
+
framework?: string;
|
|
20
27
|
hasRouter: boolean;
|
|
21
28
|
location: I18nRouterLocation | null;
|
|
22
29
|
navigate: I18nRouterNavigate | null;
|
|
23
30
|
Link: I18nRouterLink | null;
|
|
24
31
|
params: Record<string, string>;
|
|
32
|
+
/** Convert an already resolved target to the selected router's own props. */
|
|
33
|
+
createLinkProps?: (target: I18nRouterLinkTarget) => {
|
|
34
|
+
to: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
25
37
|
}
|
|
38
|
+
export declare const I18nNavigationProvider: ({ children, value, }: React.PropsWithChildren<{
|
|
39
|
+
value: I18nRouterAdapter;
|
|
40
|
+
}>) => React.JSX.Element;
|
|
41
|
+
/** Read native router hooks only when their own provider is present. */
|
|
42
|
+
export declare const useNativeI18nRouterAdapter: () => I18nRouterAdapter;
|
|
26
43
|
export declare const useI18nRouterAdapter: () => I18nRouterAdapter;
|
|
27
44
|
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ModernI18nContext, ReactI18nextProviderContext, } from './contexts.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
2
|
+
export { splitUrlTarget } from '@modern-js/runtime-utils/url';
|
|
2
3
|
export declare const getPathname: (context: {
|
|
3
4
|
ssrContext?: {
|
|
4
5
|
request?: {
|
|
@@ -15,15 +16,6 @@ export declare const getEntryPath: () => string;
|
|
|
15
16
|
* @returns The detected language or fallback language
|
|
16
17
|
*/
|
|
17
18
|
export declare const getLanguageFromPath: (pathname: string, languages: string[], fallbackLanguage: string) => string;
|
|
18
|
-
/**
|
|
19
|
-
* Split a link target into its pathname, search and hash parts without
|
|
20
|
-
* relying on `new URL` (SSR-hot path; targets are relative).
|
|
21
|
-
*/
|
|
22
|
-
export declare const splitUrlTarget: (target: string) => {
|
|
23
|
-
pathname: string;
|
|
24
|
-
search: string;
|
|
25
|
-
hash: string;
|
|
26
|
-
};
|
|
27
19
|
/**
|
|
28
20
|
* Helper function to build localized URL
|
|
29
21
|
* @param target - The language-agnostic target; may include `?search` and `#hash`
|
|
@@ -31,7 +23,7 @@ export declare const splitUrlTarget: (target: string) => {
|
|
|
31
23
|
* @param languages - Array of supported languages
|
|
32
24
|
* @returns The localized URL path with search and hash re-appended verbatim
|
|
33
25
|
*/
|
|
34
|
-
export declare const buildLocalizedUrl: (target: string, language: string, languages: string[],
|
|
26
|
+
export declare const buildLocalizedUrl: (target: string, language: string, languages: string[], urlStrategy?: I18nUrlStrategy) => string;
|
|
35
27
|
export declare const detectLanguageFromPath: (pathname: string, languages: string[], localePathRedirect: boolean) => {
|
|
36
28
|
detected: boolean;
|
|
37
29
|
language?: string;
|
|
@@ -39,4 +31,4 @@ export declare const detectLanguageFromPath: (pathname: string, languages: strin
|
|
|
39
31
|
/**
|
|
40
32
|
* Check if the given pathname should ignore automatic locale redirect
|
|
41
33
|
*/
|
|
42
|
-
export declare const shouldIgnoreRedirect: (pathname: string, languages: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
|
|
34
|
+
export declare const shouldIgnoreRedirect: (pathname: string, languages: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean), urlStrategy?: I18nUrlStrategy) => boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
+
import type { I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
3
|
+
/**
|
|
4
|
+
* Mapped locale URLs work without any extra plugin. `localeDetection.localisedUrls`
|
|
5
|
+
* alone is enough: the server derives the URL policy from the map it is given.
|
|
6
|
+
* An explicit `resolveUrlStrategy` still wins, so an integration can replace the
|
|
7
|
+
* policy wholesale.
|
|
8
|
+
*/
|
|
9
|
+
export declare const createDefaultUrlStrategy: (localisedUrls?: LocalisedUrlsOption) => I18nUrlStrategy;
|
|
10
|
+
/** A non-empty map is the only configuration that needs a derived policy. */
|
|
11
|
+
export declare const resolveDefaultUrlStrategy: (localisedUrls: unknown) => I18nUrlStrategy | undefined;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ServerPlugin } from '@modern-js/server-runtime';
|
|
2
2
|
import type { LocaleDetectionOptions } from '../shared/type.js';
|
|
3
|
+
import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
3
4
|
export { collectApiPrefixes, matchesApiPrefix } from './apiPrefix.js';
|
|
4
5
|
export interface I18nPluginOptions {
|
|
5
6
|
localeDetection: LocaleDetectionOptions;
|
|
6
7
|
staticRoutePrefixes: string[];
|
|
8
|
+
resolveUrlStrategy?: (entryName: string) => I18nUrlStrategy | undefined;
|
|
7
9
|
}
|
|
8
10
|
export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
|
|
9
11
|
export default i18nServerPlugin;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mapped-URL policy moved to `../shared` so the client runtime can derive
|
|
3
|
+
* the same policy the server does — SSR and a hydrated `<Link>` have to agree
|
|
4
|
+
* on the mapping or client navigation targets a path the server will not serve.
|
|
5
|
+
* Re-exported here to keep the server-side import path stable.
|
|
6
|
+
*/
|
|
7
|
+
export { createMappedUrlStrategy, resolveMappedUrlStrategy, } from '../shared/mappedUrlStrategy.js';
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type I18nUrlStrategy } from '../shared/urlStrategy.js';
|
|
2
2
|
interface LocaleRedirectRequest {
|
|
3
3
|
url: string;
|
|
4
|
-
header
|
|
4
|
+
header?: () => {
|
|
5
5
|
host?: string;
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Check if pathname is a static or federation resource request.
|
|
14
|
-
*
|
|
15
|
-
* This includes configured staticRoutePrefixes, the shared default skip policy
|
|
16
|
-
* from ADR-0002, and language-prefixed variants such as /en/static/app.js.
|
|
17
|
-
*/
|
|
8
|
+
export declare const shouldIgnoreRedirect: (pathname: string, urlPath: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean), urlStrategy?: I18nUrlStrategy, languages?: string[]) => boolean;
|
|
9
|
+
/** Native configured/static exclusions remain in effect for every strategy. */
|
|
18
10
|
export declare const isStaticResourceRequest: (pathname: string, staticRoutePrefixes: string[], languages?: string[]) => boolean;
|
|
19
11
|
export declare const getLanguageFromPath: (req: LocaleRedirectRequest, urlPath: string, languages: string[]) => string | null;
|
|
20
|
-
export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[],
|
|
12
|
+
export declare const buildLocalizedUrl: (req: LocaleRedirectRequest, urlPath: string, language: string, languages: string[], urlStrategy?: I18nUrlStrategy) => string;
|
|
21
13
|
export declare const createLocaleRedirectResponse: (location: string) => Response;
|
|
22
14
|
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
+
import type { I18nUrlStrategy } from './urlStrategy.js';
|
|
3
|
+
/**
|
|
4
|
+
* The fork's native locale-redirect exclusions (federation artifacts, static
|
|
5
|
+
* and upload prefixes). Re-exported from this fork-owned seam so the native
|
|
6
|
+
* runtime and server redirect paths apply them without importing the
|
|
7
|
+
* extensions package themselves.
|
|
8
|
+
*/
|
|
9
|
+
export { isDefaultLocaleRedirectSkipPath } from '@modern-js/i18n-runtime-extensions';
|
|
10
|
+
/**
|
|
11
|
+
* Build the URL policy from a mapped-locale-URL configuration.
|
|
12
|
+
*
|
|
13
|
+
* `localeDetection.localisedUrls` is plain data, so unlike a `urlStrategy`
|
|
14
|
+
* object it survives the JSON boundary between `modern.config.ts` and the
|
|
15
|
+
* generated runtime registration. It is therefore the one configuration both
|
|
16
|
+
* sides can rely on for mapped URLs.
|
|
17
|
+
*
|
|
18
|
+
* This is shared rather than server-only on purpose. The server and the client
|
|
19
|
+
* have to agree on the mapping or a hydrated `<Link>` navigates to a path the
|
|
20
|
+
* server will not serve: SSR renders `/cs/obchodni-podminky` while a
|
|
21
|
+
* client-side navigation asks for `/cs/terms-of-service`. The helpers below are
|
|
22
|
+
* pure pathname functions from `@modern-js/i18n-runtime-extensions`, so the
|
|
23
|
+
* client cost is a few hundred bytes of string work and no i18n engine.
|
|
24
|
+
*/
|
|
25
|
+
export declare const createMappedUrlStrategy: (localisedUrls?: LocalisedUrlsOption) => I18nUrlStrategy;
|
|
26
|
+
/** Only a non-empty map needs a derived policy. */
|
|
27
|
+
export declare const resolveMappedUrlStrategy: (localeDetection: unknown) => I18nUrlStrategy | undefined;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { LocalisedUrlsOption } from '@modern-js/i18n-runtime-extensions';
|
|
2
1
|
import type { LanguageDetectorOptions, Resources } from '../runtime/i18n/instance.js';
|
|
3
2
|
export interface BaseLocaleDetectionOptions {
|
|
4
3
|
localePathRedirect?: boolean;
|
|
@@ -8,18 +7,15 @@ export interface BaseLocaleDetectionOptions {
|
|
|
8
7
|
detection?: LanguageDetectorOptions;
|
|
9
8
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
10
|
+
* Canonical-path -> per-language path map, e.g.
|
|
11
|
+
* `{ '/products': { en: '/products', cs: '/produkty' } }`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* behavior (`/en/about`).
|
|
18
|
-
*
|
|
19
|
-
* Strictly opt-in: without a map, `localePathRedirect` + `languages` behave
|
|
20
|
-
* exactly like upstream Modern.js.
|
|
13
|
+
* Plain data on purpose: the generated runtime registration carries plugin
|
|
14
|
+
* options as JSON, so a map survives the trip where a `urlStrategy` object
|
|
15
|
+
* would lose its methods. The CLI expands file-system routes from it, and
|
|
16
|
+
* the server derives its URL strategy from it, with no extra plugin.
|
|
21
17
|
*/
|
|
22
|
-
localisedUrls?:
|
|
18
|
+
localisedUrls?: boolean | Record<string, Record<string, string>>;
|
|
23
19
|
}
|
|
24
20
|
export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
|
|
25
21
|
localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Per-plugin URL policy. Search and hash remain owned by native URL handling. */
|
|
2
|
+
export interface I18nUrlStrategy {
|
|
3
|
+
localizePathname(pathname: string, language: string, languages: readonly string[]): string;
|
|
4
|
+
canonicalPathname(pathname: string, languages: readonly string[]): string;
|
|
5
|
+
/** Adds exclusions; it cannot enable redirects excluded by native rules. */
|
|
6
|
+
shouldSkipRedirect?(pathname: string, languages: readonly string[]): boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A URL strategy reaches the runtime through the generated runtime
|
|
10
|
+
* registration, which carries plugin options as JSON. Functions do not survive
|
|
11
|
+
* that, so a configured strategy can arrive as an empty object — which is
|
|
12
|
+
* truthy and would make every call site throw. Anything that is not a complete
|
|
13
|
+
* strategy is treated as absent, and the caller falls back to the built-in
|
|
14
|
+
* language-prefix behaviour.
|
|
15
|
+
*/
|
|
16
|
+
export declare const asI18nUrlStrategy: (candidate: unknown) => I18nUrlStrategy | undefined;
|
|
17
|
+
/** `true` when a value was configured as a strategy but cannot be used as one. */
|
|
18
|
+
export declare const isUnusableI18nUrlStrategy: (candidate: unknown) => boolean;
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.9.0-ultramodern.
|
|
20
|
+
"version": "3.9.0-ultramodern.21",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
},
|
|
56
56
|
"default": "./dist/esm/runtime/context.mjs"
|
|
57
57
|
},
|
|
58
|
+
"./runtime/contexts": {
|
|
59
|
+
"types": "./dist/types/runtime/contexts.d.ts",
|
|
60
|
+
"node": {
|
|
61
|
+
"module": "./dist/esm/runtime/contexts.mjs"
|
|
62
|
+
},
|
|
63
|
+
"default": "./dist/esm/runtime/contexts.mjs"
|
|
64
|
+
},
|
|
58
65
|
"./runtime/no-react-i18next": {
|
|
59
66
|
"types": "./dist/types/runtime/no-react-i18next.d.ts",
|
|
60
67
|
"node": {
|
|
@@ -80,6 +87,14 @@
|
|
|
80
87
|
"./i18n": {
|
|
81
88
|
"types": "./dist/types/runtime/i18n/index.d.ts",
|
|
82
89
|
"default": "./dist/esm/runtime/i18n/index.mjs"
|
|
90
|
+
},
|
|
91
|
+
"./config": {
|
|
92
|
+
"types": "./dist/types/shared/utils.d.ts",
|
|
93
|
+
"node": {
|
|
94
|
+
"import": "./dist/esm-node/shared/utils.mjs",
|
|
95
|
+
"require": "./dist/cjs/shared/utils.js"
|
|
96
|
+
},
|
|
97
|
+
"default": "./dist/esm/shared/utils.mjs"
|
|
83
98
|
}
|
|
84
99
|
},
|
|
85
100
|
"typesVersions": {
|
|
@@ -96,6 +111,9 @@
|
|
|
96
111
|
"runtime/consumer": [
|
|
97
112
|
"./dist/types/runtime/context.d.ts"
|
|
98
113
|
],
|
|
114
|
+
"runtime/contexts": [
|
|
115
|
+
"./dist/types/runtime/contexts.d.ts"
|
|
116
|
+
],
|
|
99
117
|
"runtime/no-react-i18next": [
|
|
100
118
|
"./dist/types/runtime/no-react-i18next.d.ts"
|
|
101
119
|
],
|
|
@@ -107,26 +125,29 @@
|
|
|
107
125
|
],
|
|
108
126
|
"i18n": [
|
|
109
127
|
"./dist/types/runtime/i18n/index.d.ts"
|
|
128
|
+
],
|
|
129
|
+
"config": [
|
|
130
|
+
"./dist/types/shared/utils.d.ts"
|
|
110
131
|
]
|
|
111
132
|
}
|
|
112
133
|
},
|
|
113
134
|
"dependencies": {
|
|
114
|
-
"@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.
|
|
115
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.
|
|
116
|
-
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.
|
|
117
|
-
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.
|
|
118
|
-
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.
|
|
119
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.
|
|
120
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.
|
|
135
|
+
"@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.21",
|
|
136
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.21",
|
|
137
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.21",
|
|
138
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.9.0-ultramodern.21",
|
|
139
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.9.0-ultramodern.21",
|
|
140
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.21",
|
|
141
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.21",
|
|
121
142
|
"@swc/helpers": "^0.5.23",
|
|
122
143
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
123
144
|
"i18next-chained-backend": "^5.0.6",
|
|
124
|
-
"i18next-fs-backend": "^2.6.
|
|
145
|
+
"i18next-fs-backend": "^2.6.8",
|
|
125
146
|
"i18next-http-backend": "^4.0.2",
|
|
126
|
-
"i18next-http-middleware": "^3.9.
|
|
147
|
+
"i18next-http-middleware": "^3.9.9"
|
|
127
148
|
},
|
|
128
149
|
"peerDependencies": {
|
|
129
|
-
"@modern-js/runtime": "3.9.0-ultramodern.
|
|
150
|
+
"@modern-js/runtime": "3.9.0-ultramodern.21",
|
|
130
151
|
"i18next": ">=25.7.4",
|
|
131
152
|
"react": "^19.2.8",
|
|
132
153
|
"react-dom": "^19.2.8",
|
|
@@ -141,16 +162,18 @@
|
|
|
141
162
|
}
|
|
142
163
|
},
|
|
143
164
|
"devDependencies": {
|
|
144
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.
|
|
145
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.
|
|
146
|
-
"@
|
|
147
|
-
"@
|
|
165
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.21",
|
|
166
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.21",
|
|
167
|
+
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.21",
|
|
168
|
+
"@rslib/core": "1.0.2",
|
|
169
|
+
"@scripts/rstest-config": "2.66.0",
|
|
170
|
+
"@types/node": "^26.6.2",
|
|
148
171
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
149
172
|
"esbuild": "^0.28.2",
|
|
150
173
|
"i18next": "26.4.2",
|
|
151
|
-
"react": "^19.
|
|
152
|
-
"react-dom": "^19.
|
|
153
|
-
"react-i18next": "17.0.
|
|
174
|
+
"react": "^19.3.0",
|
|
175
|
+
"react-dom": "^19.3.0",
|
|
176
|
+
"react-i18next": "17.0.15",
|
|
154
177
|
"ts-node": "^10.9.2",
|
|
155
178
|
"typescript": "^7.0.2"
|
|
156
179
|
},
|
|
@@ -161,6 +184,6 @@
|
|
|
161
184
|
"scripts": {
|
|
162
185
|
"dev": "rslib build --watch",
|
|
163
186
|
"build": "rslib build",
|
|
164
|
-
"test": "rstest
|
|
187
|
+
"test": "rstest"
|
|
165
188
|
}
|
|
166
189
|
}
|
package/rslib.config.mts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { rslibConfig } from '@modern-js/rslib';
|
|
2
2
|
import { defineConfig } from '@rslib/core';
|
|
3
3
|
|
|
4
|
-
export default defineConfig(
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
...rslibConfig,
|
|
6
|
+
// Keep `@modern-js/plugin-i18n/runtime/contexts` a package self-reference in
|
|
7
|
+
// the output: Module Federation shares the i18n contexts by that request.
|
|
8
|
+
output: {
|
|
9
|
+
externals: ['@modern-js/plugin-i18n/runtime/contexts'],
|
|
10
|
+
},
|
|
11
|
+
});
|
package/rstest.config.mts
CHANGED
|
@@ -29,13 +29,14 @@ export default {
|
|
|
29
29
|
include: [
|
|
30
30
|
'tests/i18nUtils.test.ts',
|
|
31
31
|
'tests/localisedUrls.test.ts',
|
|
32
|
-
'tests/
|
|
33
|
-
'tests/
|
|
32
|
+
'tests/localisedRouteGeneration.test.ts',
|
|
33
|
+
'tests/mappedUrlStrategyDerivation.test.ts',
|
|
34
|
+
'tests/defaultUrlStrategy.test.ts',
|
|
35
|
+
'tests/urlStrategySerialization.test.ts',
|
|
34
36
|
'tests/backendDefaults.test.ts',
|
|
35
37
|
'tests/redirectPolicy.test.ts',
|
|
36
|
-
'tests/peerCohort.test.ts',
|
|
37
38
|
'tests/reactI18nextRuntimeBoundary.test.ts',
|
|
38
|
-
'tests/
|
|
39
|
+
'tests/federatedI18nBoundaryResolver.test.tsx',
|
|
39
40
|
],
|
|
40
41
|
extends: commonConfig,
|
|
41
42
|
}),
|
|
@@ -45,7 +46,7 @@ export default {
|
|
|
45
46
|
include: [
|
|
46
47
|
'tests/routerAdapter.test.tsx',
|
|
47
48
|
'tests/link.test.tsx',
|
|
48
|
-
'tests/
|
|
49
|
+
'tests/navigation.test.tsx',
|
|
49
50
|
],
|
|
50
51
|
extends: commonConfig,
|
|
51
52
|
}),
|
package/src/cli/index.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
4
|
-
import {
|
|
5
|
-
applyLocalisedUrlsToRoutes,
|
|
6
|
-
resolveLocalisedUrlsConfig,
|
|
7
|
-
} from '@modern-js/i18n-runtime-extensions';
|
|
8
4
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
NestedRouteForCli,
|
|
12
|
-
PageRoute,
|
|
13
|
-
} from '@modern-js/types';
|
|
5
|
+
import type { Entrypoint } from '@modern-js/types';
|
|
6
|
+
import { logger } from '@modern-js/utils';
|
|
14
7
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
8
|
+
import { isUnusableI18nUrlStrategy } from '../shared/urlStrategy';
|
|
15
9
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
16
10
|
import { applyDetectedBackendPaths, detectLocalesDirectory } from './locales';
|
|
11
|
+
import { applyLocalisedRoutes } from './localisedRoutes';
|
|
17
12
|
import '../runtime/types';
|
|
18
13
|
|
|
19
14
|
export type TransformRuntimeConfigFn = (
|
|
@@ -120,12 +115,28 @@ export const i18nPlugin = (
|
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
// Build final config with base config and transformed extended config
|
|
123
|
-
const config = {
|
|
118
|
+
const config: Record<string, unknown> = {
|
|
124
119
|
entryName: entrypoint.entryName,
|
|
125
120
|
localeDetection: localeDetectionOptions,
|
|
126
121
|
backend: backendOptions,
|
|
127
122
|
...extendedConfig,
|
|
128
123
|
};
|
|
124
|
+
|
|
125
|
+
// This config is written into the generated runtime registration as
|
|
126
|
+
// JSON, so an object carrying functions arrives at the runtime with none
|
|
127
|
+
// of them. Drop it rather than shipping a broken `{}` that every call
|
|
128
|
+
// site would then have to survive, and say what to configure instead.
|
|
129
|
+
if (isUnusableI18nUrlStrategy(config.urlStrategy)) {
|
|
130
|
+
delete config.urlStrategy;
|
|
131
|
+
logger.warn(
|
|
132
|
+
'[i18n] `urlStrategy` cannot be passed through `modern.config.ts`: ' +
|
|
133
|
+
'the generated runtime registration carries plugin options as ' +
|
|
134
|
+
'JSON, which drops its methods. Configure ' +
|
|
135
|
+
'`localeDetection.localisedUrls` instead — the runtime and the ' +
|
|
136
|
+
'server both derive the URL policy from it — or register a ' +
|
|
137
|
+
'runtime plugin that supplies the strategy directly.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
129
140
|
const { reactI18next } = extendedConfig as { reactI18next?: boolean };
|
|
130
141
|
const runtimePluginPath =
|
|
131
142
|
customPlugin?.runtime?.path ||
|
|
@@ -144,39 +155,28 @@ export const i18nPlugin = (
|
|
|
144
155
|
};
|
|
145
156
|
});
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
167
|
-
if (!localisedUrlsConfig.enabled) {
|
|
168
|
-
return { entrypoint, routes };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
158
|
+
// Mapped locale URLs are a property of `localeDetection.localisedUrls`, not
|
|
159
|
+
// of any particular app-tools composition, so the route expansion lives
|
|
160
|
+
// here rather than in an opt-in fork plugin. A bare `appTools()` consumer
|
|
161
|
+
// that declares the map gets `/cs/obchodni-podminky` generated alongside
|
|
162
|
+
// `/cs/terms-of-service`; without this the localized path 404s under SSR.
|
|
163
|
+
//
|
|
164
|
+
// This is deliberately not registered from `@modern-js/i18n-integration`:
|
|
165
|
+
// that package peer-depends on `@modern-js/runtime`, and pulling it into
|
|
166
|
+
// the default app-tools path closes an Nx cycle
|
|
167
|
+
// (app-tools -> app-tools-extensions -> i18n-integration -> runtime -> app-tools).
|
|
168
|
+
// `@modern-js/i18n-runtime-extensions` is already a direct dependency here,
|
|
169
|
+
// so nothing new is added to the graph.
|
|
170
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes }) => ({
|
|
171
|
+
entrypoint,
|
|
172
|
+
routes: applyLocalisedRoutes(
|
|
173
|
+
routes as any,
|
|
174
|
+
localeDetection
|
|
175
|
+
? getLocaleDetectionOptions(entrypoint.entryName, localeDetection)
|
|
176
|
+
: undefined,
|
|
172
177
|
entrypoint,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
languages,
|
|
176
|
-
localisedUrlsConfig.map,
|
|
177
|
-
) as (NestedRouteForCli | PageRoute)[],
|
|
178
|
-
};
|
|
179
|
-
});
|
|
178
|
+
) as typeof routes,
|
|
179
|
+
}));
|
|
180
180
|
|
|
181
181
|
api._internalServerPlugins(({ plugins }) => {
|
|
182
182
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyLocalisedUrlsToRoutes,
|
|
3
|
+
resolveLocalisedUrlsConfig,
|
|
4
|
+
} from '@modern-js/i18n-runtime-extensions';
|
|
5
|
+
import { getEntrypointRoutesOwner } from '@modern-js/runtime/cli';
|
|
6
|
+
import type {
|
|
7
|
+
Entrypoint,
|
|
8
|
+
NestedRouteForCli,
|
|
9
|
+
PageRoute,
|
|
10
|
+
} from '@modern-js/types';
|
|
11
|
+
import type { BaseLocaleDetectionOptions } from '../shared/type';
|
|
12
|
+
|
|
13
|
+
type FileSystemRoutes = (NestedRouteForCli | PageRoute)[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Expand file-system routes from `localeDetection.localisedUrls`.
|
|
17
|
+
*
|
|
18
|
+
* Fork-owned: the localised-URL engine lives in
|
|
19
|
+
* `@modern-js/i18n-runtime-extensions`, which upstream-owned files must not
|
|
20
|
+
* import. `cli/index.ts` calls this through a relative import so the seam, not
|
|
21
|
+
* the edge, is what appears in the upstream-shaped file.
|
|
22
|
+
*
|
|
23
|
+
* Returns the routes unchanged unless locale-path redirects are on, languages
|
|
24
|
+
* are declared, and a non-empty map is configured — an upstream-style config
|
|
25
|
+
* with `localePathRedirect` and `languages` but no map keeps plain
|
|
26
|
+
* locale-prefix behaviour.
|
|
27
|
+
*/
|
|
28
|
+
export const applyLocalisedRoutes = (
|
|
29
|
+
routes: FileSystemRoutes,
|
|
30
|
+
localeDetection: BaseLocaleDetectionOptions | undefined,
|
|
31
|
+
entrypoint?: Entrypoint & Parameters<typeof getEntrypointRoutesOwner>[0],
|
|
32
|
+
): FileSystemRoutes => {
|
|
33
|
+
const {
|
|
34
|
+
localePathRedirect,
|
|
35
|
+
languages = [],
|
|
36
|
+
localisedUrls,
|
|
37
|
+
} = localeDetection ?? {};
|
|
38
|
+
const resolved = resolveLocalisedUrlsConfig(localisedUrls);
|
|
39
|
+
|
|
40
|
+
if (!localePathRedirect || !languages.length || !resolved.enabled) {
|
|
41
|
+
return routes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return applyLocalisedUrlsToRoutes(
|
|
45
|
+
routes,
|
|
46
|
+
languages,
|
|
47
|
+
resolved.map,
|
|
48
|
+
entrypoint &&
|
|
49
|
+
getEntrypointRoutesOwner(entrypoint) === '@modern-js/plugin-tanstack'
|
|
50
|
+
? 'canonical'
|
|
51
|
+
: 'physical',
|
|
52
|
+
) as FileSystemRoutes;
|
|
53
|
+
};
|