@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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/README.md +221 -11
- package/dist/cjs/cli/index.js +32 -5
- package/dist/cjs/runtime/I18nLink.js +17 -28
- package/dist/cjs/runtime/Link.js +264 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/context.js +41 -10
- package/dist/cjs/runtime/hooks.js +17 -10
- package/dist/cjs/runtime/i18n/backend/config.js +9 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +15 -10
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +47 -8
- package/dist/cjs/runtime/i18n/backend/index.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.common.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/backend/middleware.node.js +13 -9
- package/dist/cjs/runtime/i18n/backend/sdk-backend.js +9 -5
- package/dist/cjs/runtime/i18n/backend/sdk-event.js +16 -11
- package/dist/cjs/runtime/i18n/detection/config.js +9 -5
- package/dist/cjs/runtime/i18n/detection/index.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.js +9 -5
- package/dist/cjs/runtime/i18n/detection/middleware.node.js +9 -5
- package/dist/cjs/runtime/i18n/index.js +9 -5
- package/dist/cjs/runtime/i18n/instance.js +17 -37
- package/dist/cjs/runtime/i18n/react-i18next.js +53 -0
- package/dist/cjs/runtime/i18n/utils.js +9 -17
- package/dist/cjs/runtime/index.js +50 -15
- package/dist/cjs/runtime/localizedPaths.js +102 -0
- package/dist/cjs/runtime/routerAdapter.js +167 -0
- package/dist/cjs/runtime/utils.js +80 -97
- package/dist/cjs/server/index.js +62 -14
- package/dist/cjs/shared/deepMerge.js +12 -8
- package/dist/cjs/shared/detection.js +9 -5
- package/dist/cjs/shared/localisedUrls.js +351 -0
- package/dist/cjs/shared/utils.js +15 -11
- package/dist/esm/cli/index.mjs +23 -0
- package/dist/esm/runtime/I18nLink.mjs +7 -22
- package/dist/esm/runtime/Link.mjs +221 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/context.mjs +34 -7
- package/dist/esm/runtime/hooks.mjs +9 -6
- package/dist/esm/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +24 -3
- package/dist/esm/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm/runtime/i18n/instance.mjs +1 -19
- package/dist/esm/runtime/i18n/react-i18next.mjs +15 -0
- package/dist/esm/runtime/i18n/utils.mjs +0 -12
- package/dist/esm/runtime/index.mjs +23 -13
- package/dist/esm/runtime/localizedPaths.mjs +55 -0
- package/dist/esm/runtime/routerAdapter.mjs +129 -0
- package/dist/esm/runtime/utils.mjs +19 -31
- package/dist/esm/server/index.mjs +46 -8
- package/dist/esm/shared/localisedUrls.mjs +283 -0
- package/dist/esm-node/cli/index.mjs +23 -0
- package/dist/esm-node/runtime/I18nLink.mjs +7 -22
- package/dist/esm-node/runtime/Link.mjs +222 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/context.mjs +34 -7
- package/dist/esm-node/runtime/hooks.mjs +9 -6
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +1 -1
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +24 -3
- package/dist/esm-node/runtime/i18n/backend/middleware.node.mjs +3 -3
- package/dist/esm-node/runtime/i18n/instance.mjs +1 -19
- package/dist/esm-node/runtime/i18n/react-i18next.mjs +16 -0
- package/dist/esm-node/runtime/i18n/utils.mjs +0 -12
- package/dist/esm-node/runtime/index.mjs +23 -13
- package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +130 -0
- package/dist/esm-node/runtime/utils.mjs +19 -31
- package/dist/esm-node/server/index.mjs +46 -8
- package/dist/esm-node/shared/localisedUrls.mjs +284 -0
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/runtime/I18nLink.d.ts +6 -0
- package/dist/types/runtime/Link.d.ts +66 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/context.d.ts +3 -0
- package/dist/types/runtime/hooks.d.ts +4 -2
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +3 -2
- package/dist/types/runtime/i18n/backend/middleware.node.d.ts +1 -1
- package/dist/types/runtime/i18n/instance.d.ts +4 -6
- package/dist/types/runtime/i18n/react-i18next.d.ts +7 -0
- package/dist/types/runtime/index.d.ts +6 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/routerAdapter.d.ts +26 -0
- package/dist/types/runtime/types.d.ts +1 -1
- package/dist/types/runtime/utils.d.ts +13 -9
- package/dist/types/server/index.d.ts +6 -0
- package/dist/types/shared/localisedUrls.d.ts +36 -0
- package/dist/types/shared/type.d.ts +14 -0
- package/package.json +24 -24
- package/rstest.config.mts +44 -0
- package/src/cli/index.ts +44 -1
- package/src/runtime/I18nLink.tsx +14 -51
- package/src/runtime/Link.tsx +430 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/context.tsx +45 -7
- package/src/runtime/hooks.ts +13 -4
- package/src/runtime/i18n/backend/defaults.node.ts +40 -2
- package/src/runtime/i18n/backend/defaults.ts +3 -1
- package/src/runtime/i18n/backend/middleware.node.ts +1 -1
- package/src/runtime/i18n/instance.ts +3 -30
- package/src/runtime/i18n/react-i18next.ts +25 -0
- package/src/runtime/i18n/utils.ts +4 -26
- package/src/runtime/index.tsx +47 -12
- package/src/runtime/localizedPaths.ts +107 -0
- package/src/runtime/routerAdapter.tsx +332 -0
- package/src/runtime/types.ts +1 -1
- package/src/runtime/utils.ts +33 -38
- package/src/server/index.ts +108 -11
- package/src/shared/localisedUrls.ts +623 -0
- package/src/shared/type.ts +14 -0
- package/tests/backendDefaults.test.ts +51 -0
- package/tests/i18nUtils.test.ts +59 -0
- package/tests/link.test.tsx +525 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/localisedUrls.test.ts +536 -0
- package/tests/routerAdapter.test.tsx +456 -0
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
- package/dist/esm/rslib-runtime.mjs +0 -18
- package/dist/esm-node/rslib-runtime.mjs +0 -19
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { LinkParamsProp, LinkTargetPathname, ValidateLinkTo } from './canonicalRoutes';
|
|
3
|
+
export type LinkParams = Record<string, string | number | undefined>;
|
|
4
|
+
/**
|
|
5
|
+
* Interpolate `$param`, `:param`, optional (`{-$param}` / `:param?`) and splat
|
|
6
|
+
* (`$` / `*`) segments with concrete values before localization, so
|
|
7
|
+
* pattern-mapped slugs localize correctly.
|
|
8
|
+
*/
|
|
9
|
+
export declare const interpolateRouteParams: (pathname: string, params?: LinkParams) => string;
|
|
10
|
+
export interface LinkActiveOptions {
|
|
11
|
+
/**
|
|
12
|
+
* `true`: active only when the location matches the target exactly.
|
|
13
|
+
* `false`: also active when the location is nested under the target.
|
|
14
|
+
* Defaults to prefix matching, except for `/` which defaults to exact.
|
|
15
|
+
*/
|
|
16
|
+
exact?: boolean;
|
|
17
|
+
}
|
|
18
|
+
type AnchorRest = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'children'>;
|
|
19
|
+
export interface LinkBaseProps extends AnchorRest {
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
/** Hash fragment without the leading `#`. Overrides a `#hash` inside `to`. */
|
|
22
|
+
hash?: string;
|
|
23
|
+
/** Search params. Object form is passed natively to TanStack Link. */
|
|
24
|
+
search?: string | Record<string, unknown>;
|
|
25
|
+
hashScrollIntoView?: boolean | ScrollIntoViewOptions;
|
|
26
|
+
replace?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Prefetching behavior, forwarded to the underlying router link:
|
|
29
|
+
* react-router gets it verbatim (Modern.js `PrefetchLink` supports it),
|
|
30
|
+
* TanStack receives it as its native `preload` prop (`'none'` -> `false`).
|
|
31
|
+
* Stripped from plain `<a>` fallbacks (external / no-router targets).
|
|
32
|
+
*/
|
|
33
|
+
prefetch?: 'intent' | 'render' | 'viewport' | 'none';
|
|
34
|
+
/**
|
|
35
|
+
* Native preload value of the underlying router link. When set, it wins
|
|
36
|
+
* over `prefetch` on the TanStack branch.
|
|
37
|
+
*/
|
|
38
|
+
preload?: unknown;
|
|
39
|
+
activeOptions?: LinkActiveOptions;
|
|
40
|
+
/** Extra anchor props applied when the link is active. */
|
|
41
|
+
activeProps?: AnchorRest & Record<string, unknown>;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
export type LinkProps<TTo extends string = string> = LinkBaseProps & {
|
|
45
|
+
to: TTo;
|
|
46
|
+
} & ValidateLinkTo<TTo> & LinkParamsProp<LinkTargetPathname<TTo>>;
|
|
47
|
+
/**
|
|
48
|
+
* The standard UltraModern link: a vanilla link in every respect except that
|
|
49
|
+
* it localizes canonical, language-agnostic paths automatically.
|
|
50
|
+
*
|
|
51
|
+
* - `to` accepts canonical routes (`/talks/$slug`), optionally with `#hash`
|
|
52
|
+
* and `?query` suffixes; both survive localization.
|
|
53
|
+
* - External URLs and bare `#hash` targets render a plain `<a>`.
|
|
54
|
+
* - Active state is language-invariant: a canonical `to` is active when the
|
|
55
|
+
* current location matches any localized variant of that route.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <Link to="/talks/$slug" params={{ slug: talk.slug }} hash="abstract" />
|
|
60
|
+
* <Link to="/platform" /> // -> /cs/platforma under cs
|
|
61
|
+
* <Link to="/#work-with-me" /> // cross-page hash, SPA navigation
|
|
62
|
+
* <Link to="https://ai.bleeding.dev" /> // external -> plain <a>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare const Link: <TTo extends string = string>(props: LinkProps<TTo>) => React.ReactElement;
|
|
66
|
+
export default Link;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical (language-agnostic) route map.
|
|
3
|
+
*
|
|
4
|
+
* Empty by default; populated via declaration merging by the generated
|
|
5
|
+
* `register.gen.d.ts` that `@modern-js/plugin-tanstack` emits:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* declare module '@modern-js/plugin-i18n/runtime' {
|
|
9
|
+
* interface UltramodernCanonicalRoutes {
|
|
10
|
+
* '/': Record<string, never>;
|
|
11
|
+
* '/talks': Record<string, never>;
|
|
12
|
+
* '/talks/$slug': { slug: string };
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Keys are canonical route patterns in TanStack notation (`$param`,
|
|
18
|
+
* `{-$param}`); values describe the route's path params.
|
|
19
|
+
*/
|
|
20
|
+
export interface UltramodernCanonicalRoutes {
|
|
21
|
+
}
|
|
22
|
+
export type CanonicalRoutePath = keyof UltramodernCanonicalRoutes & string;
|
|
23
|
+
type HasCanonicalRoutes = [keyof UltramodernCanonicalRoutes] extends [never] ? false : true;
|
|
24
|
+
/**
|
|
25
|
+
* Targets that bypass canonical-route validation: external URLs, same-page
|
|
26
|
+
* hash anchors, and canonical paths with a `?search` and/or `#hash` suffix
|
|
27
|
+
* (the pathname part of suffixed targets is still validated).
|
|
28
|
+
*/
|
|
29
|
+
type ExternalLinkTarget = `http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `//${string}`;
|
|
30
|
+
type SuffixedCanonicalTarget = `${CanonicalRoutePath}?${string}` | `${CanonicalRoutePath}#${string}`;
|
|
31
|
+
export type AllowedLinkTarget = CanonicalRoutePath | SuffixedCanonicalTarget | ExternalLinkTarget | `#${string}`;
|
|
32
|
+
/**
|
|
33
|
+
* Validates a literal `to` against the canonical route map. Computed strings
|
|
34
|
+
* (type `string`) always pass — the escape hatch for dynamic values. When no
|
|
35
|
+
* canonical map has been generated, everything passes.
|
|
36
|
+
*/
|
|
37
|
+
export type ValidateLinkTo<TTo extends string> = HasCanonicalRoutes extends false ? unknown : string extends TTo ? unknown : TTo extends AllowedLinkTarget ? unknown : {
|
|
38
|
+
to: {
|
|
39
|
+
error: 'Not a canonical route. Authors must write language-agnostic paths; see UltramodernCanonicalRoutes.';
|
|
40
|
+
received: TTo;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/** Strip `?search`/`#hash` suffixes from a link target type. */
|
|
44
|
+
export type LinkTargetPathname<TTo extends string> = TTo extends `${infer TPath}#${string}` ? TPath extends `${infer TPure}?${string}` ? TPure : TPath : TTo extends `${infer TPath}?${string}` ? TPath : TTo;
|
|
45
|
+
/**
|
|
46
|
+
* `params` prop contract for a canonical target: required when the route has
|
|
47
|
+
* required params, optional when all params are optional, forbidden when the
|
|
48
|
+
* route has none. Non-canonical (computed/external) targets accept a loose
|
|
49
|
+
* record.
|
|
50
|
+
*/
|
|
51
|
+
export type LinkParamsProp<TPath extends string> = TPath extends CanonicalRoutePath ? UltramodernCanonicalRoutes[TPath] extends Record<string, never> ? {
|
|
52
|
+
params?: undefined;
|
|
53
|
+
} : Record<string, never> extends UltramodernCanonicalRoutes[TPath] ? {
|
|
54
|
+
params?: UltramodernCanonicalRoutes[TPath];
|
|
55
|
+
} : {
|
|
56
|
+
params: UltramodernCanonicalRoutes[TPath];
|
|
57
|
+
} : {
|
|
58
|
+
params?: Record<string, string | number | undefined>;
|
|
59
|
+
};
|
|
60
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FC, ReactNode } from 'react';
|
|
2
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
2
3
|
import type { I18nInstance } from './i18n';
|
|
3
4
|
export interface ModernI18nContextValue {
|
|
4
5
|
language: string;
|
|
@@ -7,6 +8,7 @@ export interface ModernI18nContextValue {
|
|
|
7
8
|
languages?: string[];
|
|
8
9
|
localePathRedirect?: boolean;
|
|
9
10
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
11
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
10
12
|
updateLanguage?: (newLang: string) => void;
|
|
11
13
|
}
|
|
12
14
|
export interface ModernI18nProviderProps {
|
|
@@ -19,6 +21,7 @@ export interface UseModernI18nReturn {
|
|
|
19
21
|
changeLanguage: (newLang: string) => Promise<void>;
|
|
20
22
|
i18nInstance: I18nInstance;
|
|
21
23
|
supportedLanguages: string[];
|
|
24
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
22
25
|
isLanguageSupported: (lang: string) => boolean;
|
|
23
26
|
isResourcesReady: boolean;
|
|
24
27
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { TRuntimeContext } from '@modern-js/runtime';
|
|
2
2
|
import type React from 'react';
|
|
3
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
3
4
|
import type { I18nInstance } from './i18n';
|
|
4
5
|
interface RuntimeContextWithI18n extends TRuntimeContext {
|
|
5
6
|
i18nInstance?: I18nInstance;
|
|
6
7
|
}
|
|
7
|
-
export declare function createContextValue(lang: string, i18nInstance: I18nInstance | undefined, entryName: string | undefined, languages: string[], localePathRedirect: boolean, ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined, setLang: (lang: string) => void): {
|
|
8
|
+
export declare function createContextValue(lang: string, i18nInstance: I18nInstance | undefined, entryName: string | undefined, languages: string[], localePathRedirect: boolean, ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined, localisedUrls: LocalisedUrlsOption | undefined, setLang: (lang: string) => void): {
|
|
8
9
|
language: string;
|
|
9
10
|
i18nInstance: I18nInstance;
|
|
10
11
|
entryName: string | undefined;
|
|
11
12
|
languages: string[];
|
|
12
13
|
localePathRedirect: boolean;
|
|
13
14
|
ignoreRedirectRoutes: string[] | ((pathname: string) => boolean) | undefined;
|
|
15
|
+
localisedUrls: LocalisedUrlsOption | undefined;
|
|
14
16
|
updateLanguage: (lang: string) => void;
|
|
15
17
|
};
|
|
16
18
|
export declare function useSdkResourcesLoader(i18nInstance: I18nInstance | undefined, setForceUpdate: React.Dispatch<React.SetStateAction<number>>): void;
|
|
@@ -23,6 +25,6 @@ export declare function useSdkResourcesLoader(i18nInstance: I18nInstance | undef
|
|
|
23
25
|
* In SSR/SSG scenarios, server-side middleware handles redirects, so this hook is skipped.
|
|
24
26
|
* We use process.env.MODERN_TARGET to ensure this code is only included in browser bundles.
|
|
25
27
|
*/
|
|
26
|
-
export declare function useClientSideRedirect(i18nInstance: I18nInstance | undefined, localePathRedirect: boolean, languages: string[], fallbackLanguage: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)): void;
|
|
28
|
+
export declare function useClientSideRedirect(i18nInstance: I18nInstance | undefined, localePathRedirect: boolean, languages: string[], fallbackLanguage: string, ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean), localisedUrls?: LocalisedUrlsOption): void;
|
|
27
29
|
export declare function useLanguageSync(i18nInstance: I18nInstance | undefined, localePathRedirect: boolean, languages: string[], runtimeContextRef: React.MutableRefObject<RuntimeContextWithI18n>, prevLangRef: React.MutableRefObject<string>, setLang: (lang: string) => void): void;
|
|
28
30
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export declare const resolveDefaultLocalesDir: (cwd?: string) => string;
|
|
1
2
|
export declare const DEFAULT_I18NEXT_BACKEND_OPTIONS: {
|
|
2
|
-
loadPath: string;
|
|
3
|
-
addPath: string;
|
|
3
|
+
readonly loadPath: string;
|
|
4
|
+
readonly addPath: string;
|
|
4
5
|
};
|
|
5
6
|
export declare function convertBackendOptions<T extends {
|
|
6
7
|
loadPath?: string;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import type { BaseBackendOptions } from '../../shared/type';
|
|
2
|
-
type ReactI18nextModule = typeof import('react-i18next');
|
|
3
|
-
type InitReactI18next = ReactI18nextModule['initReactI18next'];
|
|
4
|
-
type I18nextProviderComponent = ReactI18nextModule['I18nextProvider'];
|
|
5
2
|
export interface I18nResourceStore {
|
|
6
3
|
data?: {
|
|
7
4
|
[language: string]: {
|
|
@@ -64,9 +61,12 @@ export interface BackendOptions extends Omit<BaseBackendOptions, 'enabled'> {
|
|
|
64
61
|
stringify?: (data: any) => string;
|
|
65
62
|
[key: string]: any;
|
|
66
63
|
}
|
|
64
|
+
export type ResourceValue = string | {
|
|
65
|
+
[key: string]: ResourceValue;
|
|
66
|
+
};
|
|
67
67
|
export interface Resources {
|
|
68
68
|
[lng: string]: {
|
|
69
|
-
[source: string]:
|
|
69
|
+
[source: string]: ResourceValue;
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
export type I18nInitOptions = {
|
|
@@ -91,6 +91,4 @@ export type I18nInitOptions = {
|
|
|
91
91
|
export declare function isI18nInstance(obj: any): obj is I18nInstance;
|
|
92
92
|
export declare function getI18nextInstanceForProvider(instance: I18nInstance | any): any;
|
|
93
93
|
export declare function getI18nInstance(userInstance?: I18nInstance | any): Promise<I18nInstance>;
|
|
94
|
-
export declare function getInitReactI18next(): Promise<InitReactI18next | null>;
|
|
95
|
-
export declare function getI18nextProvider(): Promise<I18nextProviderComponent | null>;
|
|
96
94
|
export {};
|
|
@@ -12,9 +12,14 @@ export interface I18nPluginOptions {
|
|
|
12
12
|
changeLanguage?: (lang: string) => void;
|
|
13
13
|
initOptions?: I18nInitOptions;
|
|
14
14
|
htmlLangAttr?: boolean;
|
|
15
|
+
reactI18next?: boolean;
|
|
15
16
|
[key: string]: any;
|
|
16
17
|
}
|
|
17
18
|
export declare const i18nPlugin: (options: I18nPluginOptions) => RuntimePlugin;
|
|
19
|
+
export type { AllowedLinkTarget, CanonicalRoutePath, UltramodernCanonicalRoutes, } from './canonicalRoutes';
|
|
18
20
|
export { useModernI18n } from './context';
|
|
19
|
-
export { I18nLink } from './I18nLink';
|
|
21
|
+
export { I18nLink, type I18nLinkProps } from './I18nLink';
|
|
22
|
+
export { Link, type LinkActiveOptions, type LinkBaseProps, type LinkParams, type LinkProps, } from './Link';
|
|
23
|
+
export { canonicalPath, type LocalizedPathsConfig, localizePath, type UseLocalizedLocationReturn, type UseLocalizedPathsReturn, useLocalizedLocation, useLocalizedPaths, } from './localizedPaths';
|
|
24
|
+
export { buildLocalizedUrl, splitUrlTarget } from './utils';
|
|
20
25
|
export default i18nPlugin;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
2
|
+
export interface LocalizedPathsConfig {
|
|
3
|
+
languages: string[];
|
|
4
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Localize a canonical, language-agnostic target for the given language:
|
|
8
|
+
* adds the language prefix and applies `localisedUrls` pattern mapping.
|
|
9
|
+
* `?search`/`#hash` suffixes are preserved verbatim.
|
|
10
|
+
*/
|
|
11
|
+
export declare const localizePath: (pathname: string, language: string, config: LocalizedPathsConfig) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Reverse of {@link localizePath}: strip the language prefix and map localized
|
|
14
|
+
* slugs back to the canonical pattern's path. `?search`/`#hash` suffixes are
|
|
15
|
+
* preserved verbatim.
|
|
16
|
+
*/
|
|
17
|
+
export declare const canonicalPath: (target: string, config: LocalizedPathsConfig) => string;
|
|
18
|
+
export interface UseLocalizedPathsReturn {
|
|
19
|
+
localizePath: (pathname: string, language: string) => string;
|
|
20
|
+
canonicalPath: (pathname: string) => string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Context-bound versions of {@link localizePath} and {@link canonicalPath} —
|
|
24
|
+
* the plugin configuration (languages, localisedUrls) is read from the i18n
|
|
25
|
+
* provider, so apps never copy pattern-matching helpers again.
|
|
26
|
+
*/
|
|
27
|
+
export declare const useLocalizedPaths: () => UseLocalizedPathsReturn;
|
|
28
|
+
export interface UseLocalizedLocationReturn {
|
|
29
|
+
language: string;
|
|
30
|
+
/** Canonical (language-agnostic) path of the current location. */
|
|
31
|
+
canonical: string;
|
|
32
|
+
/** Per-language hrefs for the current location, search+hash preserved. */
|
|
33
|
+
alternates: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Per-language hrefs for the current location — for hreflang `<link>` tags and
|
|
37
|
+
* language switchers. SSR-safe: the location comes from the router adapter.
|
|
38
|
+
*/
|
|
39
|
+
export declare const useLocalizedLocation: () => UseLocalizedLocationReturn;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export type I18nRouterFramework = 'react-router' | 'tanstack' | string;
|
|
3
|
+
export interface I18nRouterLocation {
|
|
4
|
+
pathname: string;
|
|
5
|
+
search: string;
|
|
6
|
+
hash: string;
|
|
7
|
+
}
|
|
8
|
+
export interface I18nRouterNavigateOptions {
|
|
9
|
+
replace?: boolean;
|
|
10
|
+
state?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export type I18nRouterNavigate = (href: string, options?: I18nRouterNavigateOptions) => void | Promise<void>;
|
|
13
|
+
export type I18nRouterLink = React.ComponentType<{
|
|
14
|
+
to: string;
|
|
15
|
+
children?: React.ReactNode;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
export interface I18nRouterAdapter {
|
|
19
|
+
framework?: I18nRouterFramework;
|
|
20
|
+
hasRouter: boolean;
|
|
21
|
+
location: I18nRouterLocation | null;
|
|
22
|
+
navigate: I18nRouterNavigate | null;
|
|
23
|
+
Link: I18nRouterLink | null;
|
|
24
|
+
params: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export declare const useI18nRouterAdapter: () => I18nRouterAdapter;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TInternalRuntimeContext } from '@modern-js/runtime/context';
|
|
2
|
+
import type { LocalisedUrlsOption } from '../shared/localisedUrls';
|
|
2
3
|
export declare const getPathname: (context: TInternalRuntimeContext) => string;
|
|
3
4
|
export declare const getEntryPath: () => string;
|
|
4
5
|
/**
|
|
@@ -9,14 +10,23 @@ export declare const getEntryPath: () => string;
|
|
|
9
10
|
* @returns The detected language or fallback language
|
|
10
11
|
*/
|
|
11
12
|
export declare const getLanguageFromPath: (pathname: string, languages: string[], fallbackLanguage: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Split a link target into its pathname, search and hash parts without
|
|
15
|
+
* relying on `new URL` (SSR-hot path; targets are relative).
|
|
16
|
+
*/
|
|
17
|
+
export declare const splitUrlTarget: (target: string) => {
|
|
18
|
+
pathname: string;
|
|
19
|
+
search: string;
|
|
20
|
+
hash: string;
|
|
21
|
+
};
|
|
12
22
|
/**
|
|
13
23
|
* Helper function to build localized URL
|
|
14
|
-
* @param
|
|
24
|
+
* @param target - The language-agnostic target; may include `?search` and `#hash`
|
|
15
25
|
* @param language - The target language
|
|
16
26
|
* @param languages - Array of supported languages
|
|
17
|
-
* @returns The localized URL path
|
|
27
|
+
* @returns The localized URL path with search and hash re-appended verbatim
|
|
18
28
|
*/
|
|
19
|
-
export declare const buildLocalizedUrl: (
|
|
29
|
+
export declare const buildLocalizedUrl: (target: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
|
|
20
30
|
export declare const detectLanguageFromPath: (pathname: string, languages: string[], localePathRedirect: boolean) => {
|
|
21
31
|
detected: boolean;
|
|
22
32
|
language?: string;
|
|
@@ -25,9 +35,3 @@ export declare const detectLanguageFromPath: (pathname: string, languages: strin
|
|
|
25
35
|
* Check if the given pathname should ignore automatic locale redirect
|
|
26
36
|
*/
|
|
27
37
|
export declare const shouldIgnoreRedirect: (pathname: string, languages: string[], ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean)) => boolean;
|
|
28
|
-
export declare const useRouterHooks: () => {
|
|
29
|
-
navigate: any;
|
|
30
|
-
location: any;
|
|
31
|
-
params: any;
|
|
32
|
-
hasRouter: boolean;
|
|
33
|
-
};
|
|
@@ -4,5 +4,11 @@ export interface I18nPluginOptions {
|
|
|
4
4
|
localeDetection: LocaleDetectionOptions;
|
|
5
5
|
staticRoutePrefixes: string[];
|
|
6
6
|
}
|
|
7
|
+
type ApiPrefixInput = string | string[] | undefined;
|
|
8
|
+
export declare const collectApiPrefixes: (routes: Array<{
|
|
9
|
+
isApi?: boolean;
|
|
10
|
+
urlPath?: string;
|
|
11
|
+
}>, bffPrefix?: ApiPrefixInput) => string[];
|
|
12
|
+
export declare const matchesApiPrefix: (pathname: string, apiPrefixes: string[]) => boolean;
|
|
7
13
|
export declare const i18nServerPlugin: (options: I18nPluginOptions) => ServerPlugin;
|
|
8
14
|
export default i18nServerPlugin;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
|
|
2
|
+
export type LocalisedUrlPathMap = Record<string, string>;
|
|
3
|
+
export type LocalisedUrlsMap = Record<string, LocalisedUrlPathMap>;
|
|
4
|
+
export type LocalisedUrlsOption = boolean | LocalisedUrlsMap;
|
|
5
|
+
export interface ResolvedLocalisedUrlsConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
map: LocalisedUrlsMap;
|
|
8
|
+
}
|
|
9
|
+
export declare const normalisePathPattern: (path: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Normalise a concrete request pathname: slash cleanup only. Unlike
|
|
12
|
+
* {@link normalisePathPattern} it must not rewrite literal `[x]` segments to
|
|
13
|
+
* `:x` params — pathnames are values, not patterns.
|
|
14
|
+
*/
|
|
15
|
+
export declare const normalisePathname: (pathname: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Localised URLs are strictly opt-in: only an explicit, non-empty map enables
|
|
18
|
+
* route expansion and validation. `true`, `false`, an empty map and absence
|
|
19
|
+
* all resolve to disabled, so upstream-style configs (`localePathRedirect` +
|
|
20
|
+
* `languages` without a map) keep plain locale-prefix behavior instead of
|
|
21
|
+
* failing the build for every route missing from a map they never wrote.
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveLocalisedUrlsConfig: (option: LocalisedUrlsOption | undefined) => ResolvedLocalisedUrlsConfig;
|
|
24
|
+
export declare const validateLocalisedUrls: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => void;
|
|
25
|
+
export declare const applyLocalisedUrlsToRoutes: (routes: (NestedRouteForCli | PageRoute)[], languages: string[], localisedUrls: LocalisedUrlsMap) => (NestedRouteForCli | PageRoute)[];
|
|
26
|
+
export declare const matchPathPattern: (pathname: string, pattern: string) => Record<string, string> | null;
|
|
27
|
+
export declare const buildPathFromPattern: (pattern: string, params: Record<string, string>) => string;
|
|
28
|
+
export declare const resolveLocalisedPath: (pathname: string, targetLanguage: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Reverse-map a language-specific pathname (without language prefix) back to
|
|
31
|
+
* the canonical, language-agnostic path: localized slug patterns are matched
|
|
32
|
+
* against every language variant and rebuilt from the canonical map key.
|
|
33
|
+
*/
|
|
34
|
+
export declare const resolveCanonicalLocalisedPath: (pathname: string, languages: string[], localisedUrls: LocalisedUrlsMap) => string;
|
|
35
|
+
export declare const localiseTargetPathname: (pathname: string, language: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
|
|
36
|
+
export declare const canonicalTargetPathname: (pathname: string, languages: string[], localisedUrls?: LocalisedUrlsOption) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LanguageDetectorOptions, Resources } from '../runtime/i18n/instance';
|
|
2
|
+
import type { LocalisedUrlsOption } from './localisedUrls';
|
|
2
3
|
export interface BaseLocaleDetectionOptions {
|
|
3
4
|
localePathRedirect?: boolean;
|
|
4
5
|
i18nextDetector?: boolean;
|
|
@@ -6,6 +7,19 @@ export interface BaseLocaleDetectionOptions {
|
|
|
6
7
|
fallbackLanguage?: string;
|
|
7
8
|
detection?: LanguageDetectorOptions;
|
|
8
9
|
ignoreRedirectRoutes?: string[] | ((pathname: string) => boolean);
|
|
10
|
+
/**
|
|
11
|
+
* Enables localised pathnames in addition to the locale prefix.
|
|
12
|
+
*
|
|
13
|
+
* - non-empty object: map canonical route paths to every configured
|
|
14
|
+
* language; route generation then validates that every localisable route
|
|
15
|
+
* path has entries for all configured languages.
|
|
16
|
+
* - absent / `false` / `true` / empty object: keep only locale-prefix
|
|
17
|
+
* behavior (`/en/about`).
|
|
18
|
+
*
|
|
19
|
+
* Strictly opt-in: without a map, `localePathRedirect` + `languages` behave
|
|
20
|
+
* exactly like upstream Modern.js.
|
|
21
|
+
*/
|
|
22
|
+
localisedUrls?: LocalisedUrlsOption;
|
|
9
23
|
}
|
|
10
24
|
export interface LocaleDetectionOptions extends BaseLocaleDetectionOptions {
|
|
11
25
|
localeDetectionByEntry?: Record<string, BaseLocaleDetectionOptions>;
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"modern",
|
|
18
18
|
"modern.js"
|
|
19
19
|
],
|
|
20
|
-
"version": "3.2.0-ultramodern.
|
|
20
|
+
"version": "3.2.0-ultramodern.121",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
@@ -81,25 +81,25 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
|
-
"@swc/helpers": "^0.5.
|
|
84
|
+
"@swc/helpers": "^0.5.23",
|
|
85
85
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
86
86
|
"i18next-chained-backend": "^5.0.4",
|
|
87
|
-
"i18next-fs-backend": "^2.6.
|
|
87
|
+
"i18next-fs-backend": "^2.6.6",
|
|
88
88
|
"i18next-http-backend": "^4.0.0",
|
|
89
|
-
"i18next-http-middleware": "^3.9.
|
|
90
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.
|
|
91
|
-
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.
|
|
92
|
-
"@modern-js/runtime
|
|
93
|
-
"@modern-js/
|
|
94
|
-
"@modern-js/
|
|
95
|
-
"@modern-js/
|
|
89
|
+
"i18next-http-middleware": "^3.9.7",
|
|
90
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.2.0-ultramodern.121",
|
|
91
|
+
"@modern-js/server-core": "npm:@bleedingdev/modern-js-server-core@3.2.0-ultramodern.121",
|
|
92
|
+
"@modern-js/server-runtime": "npm:@bleedingdev/modern-js-server-runtime@3.2.0-ultramodern.121",
|
|
93
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.2.0-ultramodern.121",
|
|
94
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.2.0-ultramodern.121",
|
|
95
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.2.0-ultramodern.121"
|
|
96
96
|
},
|
|
97
97
|
"peerDependencies": {
|
|
98
|
-
"@modern-js/runtime": "3.2.0-ultramodern.
|
|
98
|
+
"@modern-js/runtime": "3.2.0-ultramodern.121",
|
|
99
99
|
"i18next": ">=25.7.4",
|
|
100
|
-
"react": "^19.2.
|
|
101
|
-
"react-dom": "^19.2.
|
|
102
|
-
"react-i18next": "
|
|
100
|
+
"react": "^19.2.7",
|
|
101
|
+
"react-dom": "^19.2.7",
|
|
102
|
+
"react-i18next": "^17.0.0"
|
|
103
103
|
},
|
|
104
104
|
"peerDependenciesMeta": {
|
|
105
105
|
"i18next": {
|
|
@@ -110,18 +110,18 @@
|
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@rslib/core": "0.
|
|
113
|
+
"@rslib/core": "0.22.0",
|
|
114
114
|
"@types/jest": "^30.0.0",
|
|
115
|
-
"@types/node": "^25.
|
|
116
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
117
|
-
"i18next": "26.
|
|
115
|
+
"@types/node": "^25.9.3",
|
|
116
|
+
"@typescript/native-preview": "7.0.0-dev.20260610.1",
|
|
117
|
+
"i18next": "26.3.1",
|
|
118
118
|
"jest": "^30.4.2",
|
|
119
|
-
"react": "^19.2.
|
|
120
|
-
"react-dom": "^19.2.
|
|
119
|
+
"react": "^19.2.7",
|
|
120
|
+
"react-dom": "^19.2.7",
|
|
121
121
|
"react-i18next": "17.0.8",
|
|
122
|
-
"ts-jest": "^29.4.
|
|
123
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.
|
|
124
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.
|
|
122
|
+
"ts-jest": "^29.4.11",
|
|
123
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.2.0-ultramodern.121",
|
|
124
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.2.0-ultramodern.121"
|
|
125
125
|
},
|
|
126
126
|
"sideEffects": false,
|
|
127
127
|
"publishConfig": {
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
},
|
|
131
131
|
"scripts": {
|
|
132
132
|
"dev": "rslib build --watch",
|
|
133
|
-
"build": "rslib build",
|
|
133
|
+
"build": "rslib build && pnpm -w tsgo:dts \"$PWD\"",
|
|
134
134
|
"test": "rstest --passWithNoTests"
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { ProjectConfig } from '@rstest/core';
|
|
4
|
+
import { withTestPreset } from '@scripts/rstest-config';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const commonConfig: ProjectConfig = {
|
|
9
|
+
setupFiles: [resolve(__dirname, '../../../scripts/rstest-config/setup.ts')],
|
|
10
|
+
globals: true,
|
|
11
|
+
tools: {
|
|
12
|
+
swc: {
|
|
13
|
+
jsc: {
|
|
14
|
+
transform: {
|
|
15
|
+
react: {
|
|
16
|
+
runtime: 'automatic',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
projects: [
|
|
26
|
+
withTestPreset({
|
|
27
|
+
name: 'plugin-i18n-node',
|
|
28
|
+
testEnvironment: 'node',
|
|
29
|
+
include: [
|
|
30
|
+
'tests/i18nUtils.test.ts',
|
|
31
|
+
'tests/localisedUrls.test.ts',
|
|
32
|
+
'tests/linkTypes.test.ts',
|
|
33
|
+
'tests/backendDefaults.test.ts',
|
|
34
|
+
],
|
|
35
|
+
extends: commonConfig,
|
|
36
|
+
}),
|
|
37
|
+
withTestPreset({
|
|
38
|
+
name: 'plugin-i18n-client',
|
|
39
|
+
testEnvironment: 'happy-dom',
|
|
40
|
+
include: ['tests/routerAdapter.test.tsx', 'tests/link.test.tsx'],
|
|
41
|
+
extends: commonConfig,
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
};
|
package/src/cli/index.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
2
|
import { getPublicDirRoutePrefixes } from '@modern-js/server-core';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Entrypoint,
|
|
5
|
+
NestedRouteForCli,
|
|
6
|
+
PageRoute,
|
|
7
|
+
} from '@modern-js/types';
|
|
4
8
|
import fs from 'fs';
|
|
5
9
|
import path from 'path';
|
|
10
|
+
import {
|
|
11
|
+
applyLocalisedUrlsToRoutes,
|
|
12
|
+
resolveLocalisedUrlsConfig,
|
|
13
|
+
} from '../shared/localisedUrls';
|
|
6
14
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
7
15
|
import { getBackendOptions, getLocaleDetectionOptions } from '../shared/utils';
|
|
16
|
+
import '../runtime/types';
|
|
8
17
|
|
|
9
18
|
export type TransformRuntimeConfigFn = (
|
|
10
19
|
extendedConfig: Record<string, any>,
|
|
@@ -203,6 +212,40 @@ export const i18nPlugin = (
|
|
|
203
212
|
};
|
|
204
213
|
});
|
|
205
214
|
|
|
215
|
+
api.modifyFileSystemRoutes(({ entrypoint, routes }) => {
|
|
216
|
+
if (!localeDetection) {
|
|
217
|
+
return { entrypoint, routes };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const localeDetectionOptions = getLocaleDetectionOptions(
|
|
221
|
+
entrypoint.entryName,
|
|
222
|
+
localeDetection,
|
|
223
|
+
);
|
|
224
|
+
const {
|
|
225
|
+
localePathRedirect,
|
|
226
|
+
languages = [],
|
|
227
|
+
localisedUrls,
|
|
228
|
+
} = localeDetectionOptions;
|
|
229
|
+
|
|
230
|
+
if (!localePathRedirect || languages.length === 0) {
|
|
231
|
+
return { entrypoint, routes };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
235
|
+
if (!localisedUrlsConfig.enabled) {
|
|
236
|
+
return { entrypoint, routes };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
entrypoint,
|
|
241
|
+
routes: applyLocalisedUrlsToRoutes(
|
|
242
|
+
routes as (NestedRouteForCli | PageRoute)[],
|
|
243
|
+
languages,
|
|
244
|
+
localisedUrlsConfig.map,
|
|
245
|
+
),
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
206
249
|
api._internalServerPlugins(({ plugins }) => {
|
|
207
250
|
const { serverRoutes, metaName } = api.getAppContext();
|
|
208
251
|
const normalizedConfig = api.getNormalizedConfig();
|