@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.99 → 3.4.0-ultramodern.1
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 +17 -64
- package/dist/cjs/cli/locales.js +132 -0
- package/dist/cjs/runtime/I18nLink.js +17 -20
- package/dist/cjs/runtime/Link.js +264 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/context.js +9 -5
- package/dist/cjs/runtime/hooks.js +9 -5
- package/dist/cjs/runtime/i18n/backend/config.js +9 -5
- package/dist/cjs/runtime/i18n/backend/defaults.js +20 -11
- package/dist/cjs/runtime/i18n/backend/defaults.node.js +79 -10
- 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 +9 -5
- 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 -13
- package/dist/cjs/runtime/i18n/react-i18next.js +12 -8
- package/dist/cjs/runtime/i18n/utils.js +9 -5
- package/dist/cjs/runtime/index.js +32 -5
- package/dist/cjs/runtime/localizedPaths.js +102 -0
- package/dist/cjs/runtime/routerAdapter.js +11 -7
- package/dist/cjs/runtime/utils.js +31 -17
- package/dist/cjs/server/index.js +10 -14
- package/dist/cjs/shared/deepMerge.js +12 -8
- package/dist/cjs/shared/detection.js +9 -5
- package/dist/cjs/shared/localisedUrls.js +148 -34
- package/dist/cjs/shared/utils.js +15 -11
- package/dist/esm/cli/index.mjs +8 -48
- package/dist/esm/cli/locales.mjs +80 -0
- package/dist/esm/runtime/I18nLink.mjs +7 -14
- package/dist/esm/runtime/Link.mjs +221 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm/runtime/i18n/backend/defaults.node.mjs +56 -5
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/localizedPaths.mjs +55 -0
- package/dist/esm/runtime/routerAdapter.mjs +3 -3
- package/dist/esm/runtime/utils.mjs +19 -12
- package/dist/esm/server/index.mjs +2 -10
- package/dist/esm/shared/localisedUrls.mjs +115 -23
- package/dist/esm-node/cli/index.mjs +8 -48
- package/dist/esm-node/cli/locales.mjs +81 -0
- package/dist/esm-node/runtime/I18nLink.mjs +7 -14
- package/dist/esm-node/runtime/Link.mjs +222 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/i18n/backend/defaults.mjs +6 -2
- package/dist/esm-node/runtime/i18n/backend/defaults.node.mjs +56 -5
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/localizedPaths.mjs +56 -0
- package/dist/esm-node/runtime/routerAdapter.mjs +3 -3
- package/dist/esm-node/runtime/utils.mjs +19 -12
- package/dist/esm-node/server/index.mjs +2 -10
- package/dist/esm-node/shared/localisedUrls.mjs +115 -23
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/locales.d.ts +17 -0
- package/dist/types/runtime/I18nLink.d.ts +4 -13
- package/dist/types/runtime/Link.d.ts +66 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/i18n/backend/defaults.d.ts +10 -7
- package/dist/types/runtime/i18n/backend/defaults.node.d.ts +13 -4
- package/dist/types/runtime/index.d.ts +5 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/types.d.ts +1 -1
- package/dist/types/runtime/utils.d.ts +13 -4
- package/dist/types/shared/localisedUrls.d.ts +23 -0
- package/dist/types/shared/type.d.ts +27 -5
- package/package.json +28 -25
- package/rstest.config.mts +7 -2
- package/src/cli/index.ts +25 -98
- package/src/cli/locales.ts +186 -0
- package/src/runtime/I18nLink.tsx +13 -44
- package/src/runtime/Link.tsx +430 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/i18n/backend/defaults.node.ts +112 -7
- package/src/runtime/i18n/backend/defaults.ts +20 -18
- package/src/runtime/index.tsx +24 -2
- package/src/runtime/localizedPaths.ts +107 -0
- package/src/runtime/routerAdapter.tsx +4 -5
- package/src/runtime/types.ts +1 -1
- package/src/runtime/utils.ts +33 -26
- package/src/server/index.ts +7 -17
- package/src/shared/localisedUrls.ts +256 -26
- package/src/shared/type.ts +27 -5
- package/tests/backendDefaults.test.ts +51 -0
- package/tests/i18nUtils.test.ts +10 -3
- package/tests/link.test.tsx +525 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/localisedUrls.test.ts +224 -0
- package/tests/routerAdapter.test.tsx +86 -12
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
import "node:module";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path_0 from "path";
|
|
4
|
+
const CONVENTIONAL_LOCALES_DIRS = [
|
|
5
|
+
'./locales',
|
|
6
|
+
'./config/public/locales'
|
|
7
|
+
];
|
|
8
|
+
const isDirectory = (dirPath)=>{
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(dirPath).isDirectory();
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const resolveDefaultLocalesDir = (cwd = process.cwd())=>{
|
|
16
|
+
for (const dir of CONVENTIONAL_LOCALES_DIRS)if (isDirectory(path_0.resolve(cwd, dir))) return dir;
|
|
17
|
+
return CONVENTIONAL_LOCALES_DIRS[0];
|
|
18
|
+
};
|
|
2
19
|
const DEFAULT_I18NEXT_BACKEND_OPTIONS = {
|
|
3
|
-
loadPath
|
|
4
|
-
|
|
20
|
+
get loadPath () {
|
|
21
|
+
return `${resolveDefaultLocalesDir()}/{{lng}}/{{ns}}.json`;
|
|
22
|
+
},
|
|
23
|
+
get addPath () {
|
|
24
|
+
return `${resolveDefaultLocalesDir()}/{{lng}}/{{ns}}.json`;
|
|
25
|
+
}
|
|
5
26
|
};
|
|
6
27
|
function convertPath(path) {
|
|
7
28
|
if (!path) return path;
|
|
8
29
|
if (path.startsWith('/')) return `.${path}`;
|
|
9
30
|
return path;
|
|
10
31
|
}
|
|
32
|
+
function shouldUseServerPath(currentPath, detectedPath) {
|
|
33
|
+
return !detectedPath || currentPath === detectedPath;
|
|
34
|
+
}
|
|
35
|
+
function getResourceBasePath(resourcePath) {
|
|
36
|
+
const markerIndex = resourcePath.indexOf('{{lng}}');
|
|
37
|
+
if (markerIndex < 0) return resourcePath;
|
|
38
|
+
return resourcePath.slice(0, markerIndex).replace(/[\\/]+$/, '');
|
|
39
|
+
}
|
|
40
|
+
function pathExists(resourcePath) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.existsSync(getResourceBasePath(resourcePath));
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function getServerPath(pathCandidates, fallbackPath) {
|
|
48
|
+
const candidates = Array.from(new Set([
|
|
49
|
+
...pathCandidates || [],
|
|
50
|
+
fallbackPath
|
|
51
|
+
].filter(Boolean)));
|
|
52
|
+
return candidates.find(pathExists) || candidates[0];
|
|
53
|
+
}
|
|
11
54
|
function convertBackendOptions(options) {
|
|
12
55
|
if (!options) return options;
|
|
13
56
|
const converted = {
|
|
14
57
|
...options
|
|
15
58
|
};
|
|
16
|
-
if (converted.loadPath) converted.loadPath =
|
|
17
|
-
if (converted.
|
|
59
|
+
if ((converted.serverLoadPath || converted.serverLoadPaths) && shouldUseServerPath(converted.loadPath, converted._detectedLoadPath)) converted.loadPath = getServerPath(converted.serverLoadPaths, converted.serverLoadPath);
|
|
60
|
+
else if (converted.loadPath) converted.loadPath = convertPath(converted.loadPath);
|
|
61
|
+
if ((converted.serverAddPath || converted.serverAddPaths) && shouldUseServerPath(converted.addPath, converted._detectedAddPath)) converted.addPath = getServerPath(converted.serverAddPaths, converted.serverAddPath);
|
|
62
|
+
else if (converted.addPath) converted.addPath = convertPath(converted.addPath);
|
|
63
|
+
delete converted.serverLoadPath;
|
|
64
|
+
delete converted.serverAddPath;
|
|
65
|
+
delete converted.serverLoadPaths;
|
|
66
|
+
delete converted.serverAddPaths;
|
|
67
|
+
delete converted._detectedLoadPath;
|
|
68
|
+
delete converted._detectedAddPath;
|
|
18
69
|
return converted;
|
|
19
70
|
}
|
|
20
|
-
export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions };
|
|
71
|
+
export { DEFAULT_I18NEXT_BACKEND_OPTIONS, convertBackendOptions, resolveDefaultLocalesDir };
|
|
@@ -13,7 +13,7 @@ import { detectLanguageWithPriority, exportServerLngToWindow, mergeDetectionOpti
|
|
|
13
13
|
import { useI18nextLanguageDetector } from "./i18n/detection/middleware.mjs";
|
|
14
14
|
import { getI18nextInstanceForProvider } from "./i18n/instance.mjs";
|
|
15
15
|
import { changeI18nLanguage, ensureLanguageMatch, initializeI18nInstance, setupClonedInstance } from "./i18n/utils.mjs";
|
|
16
|
-
import { getPathname } from "./utils.mjs";
|
|
16
|
+
import { buildLocalizedUrl, getPathname, splitUrlTarget } from "./utils.mjs";
|
|
17
17
|
import "./types.mjs";
|
|
18
18
|
const i18nPlugin = (options)=>({
|
|
19
19
|
name: '@modern-js/plugin-i18n',
|
|
@@ -137,5 +137,7 @@ const i18nPlugin = (options)=>({
|
|
|
137
137
|
});
|
|
138
138
|
const runtime = i18nPlugin;
|
|
139
139
|
export { I18nLink } from "./I18nLink.mjs";
|
|
140
|
+
export { Link } from "./Link.mjs";
|
|
141
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths } from "./localizedPaths.mjs";
|
|
140
142
|
export default runtime;
|
|
141
|
-
export { i18nPlugin, useModernI18n };
|
|
143
|
+
export { buildLocalizedUrl, i18nPlugin, splitUrlTarget, useModernI18n };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { canonicalTargetPathname } from "../shared/localisedUrls.mjs";
|
|
4
|
+
import { useModernI18n } from "./context.mjs";
|
|
5
|
+
import { useI18nRouterAdapter } from "./routerAdapter.mjs";
|
|
6
|
+
import { buildLocalizedUrl, splitUrlTarget } from "./utils.mjs";
|
|
7
|
+
const localizePath = (pathname, language, config)=>buildLocalizedUrl(pathname, language, config.languages, config.localisedUrls);
|
|
8
|
+
const canonicalPath = (target, config)=>{
|
|
9
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
10
|
+
const resolvedPath = canonicalTargetPathname(pathname, config.languages, config.localisedUrls);
|
|
11
|
+
return `${resolvedPath}${search}${hash}`;
|
|
12
|
+
};
|
|
13
|
+
const useLocalizedPaths = ()=>{
|
|
14
|
+
const { supportedLanguages, localisedUrls } = useModernI18n();
|
|
15
|
+
return useMemo(()=>{
|
|
16
|
+
const config = {
|
|
17
|
+
languages: supportedLanguages,
|
|
18
|
+
localisedUrls
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
localizePath: (pathname, language)=>localizePath(pathname, language, config),
|
|
22
|
+
canonicalPath: (pathname)=>canonicalPath(pathname, config)
|
|
23
|
+
};
|
|
24
|
+
}, [
|
|
25
|
+
supportedLanguages,
|
|
26
|
+
localisedUrls
|
|
27
|
+
]);
|
|
28
|
+
};
|
|
29
|
+
const useLocalizedLocation = ()=>{
|
|
30
|
+
const { language, supportedLanguages, localisedUrls } = useModernI18n();
|
|
31
|
+
const { location } = useI18nRouterAdapter();
|
|
32
|
+
const pathname = location?.pathname ?? '/';
|
|
33
|
+
const search = location?.search ?? '';
|
|
34
|
+
const hash = location?.hash ?? '';
|
|
35
|
+
return useMemo(()=>{
|
|
36
|
+
const config = {
|
|
37
|
+
languages: supportedLanguages,
|
|
38
|
+
localisedUrls
|
|
39
|
+
};
|
|
40
|
+
const alternates = {};
|
|
41
|
+
for (const supportedLanguage of supportedLanguages)alternates[supportedLanguage] = `${localizePath(pathname, supportedLanguage, config)}${search}${hash}`;
|
|
42
|
+
return {
|
|
43
|
+
language,
|
|
44
|
+
canonical: canonicalPath(pathname, config),
|
|
45
|
+
alternates
|
|
46
|
+
};
|
|
47
|
+
}, [
|
|
48
|
+
language,
|
|
49
|
+
supportedLanguages,
|
|
50
|
+
localisedUrls,
|
|
51
|
+
pathname,
|
|
52
|
+
search,
|
|
53
|
+
hash
|
|
54
|
+
]);
|
|
55
|
+
};
|
|
56
|
+
export { canonicalPath, localizePath, useLocalizedLocation, useLocalizedPaths };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { RuntimeContext, isBrowser } from "@modern-js/runtime";
|
|
3
|
-
import { InternalRuntimeContext } from "@modern-js/runtime/context";
|
|
3
|
+
import { InternalRuntimeContext, getRouterRuntimeState } from "@modern-js/runtime/context";
|
|
4
4
|
import { Link as router_Link, useInRouterContext, useLocation, useNavigate, useParams } from "@modern-js/runtime/router";
|
|
5
5
|
import { useCallback, useContext, useEffect, useState } from "react";
|
|
6
6
|
const normalizeUrlPart = (value, prefix)=>{
|
|
@@ -26,7 +26,7 @@ const getWindowLocation = ()=>{
|
|
|
26
26
|
};
|
|
27
27
|
};
|
|
28
28
|
const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
29
|
-
const framework = internalContext
|
|
29
|
+
const framework = getRouterRuntimeState(internalContext)?.framework || getRouterRuntimeState(runtimeContext)?.framework;
|
|
30
30
|
if (framework) return framework;
|
|
31
31
|
if (internalContext.router?.useRouter || runtimeContext.router?.useRouter) return 'tanstack';
|
|
32
32
|
if (internalContext.router?.useLocation || internalContext.router?.useHref || runtimeContext.router?.useLocation || runtimeContext.router?.useHref) return 'react-router';
|
|
@@ -34,7 +34,7 @@ const getRouterFramework = (runtimeContext, internalContext, inReactRouter)=>{
|
|
|
34
34
|
};
|
|
35
35
|
const getRouterInstance = (internalContext, contextRouter)=>{
|
|
36
36
|
if (contextRouter) return contextRouter;
|
|
37
|
-
const router = internalContext
|
|
37
|
+
const router = getRouterRuntimeState(internalContext)?.instance;
|
|
38
38
|
if (!router || 'object' != typeof router) return null;
|
|
39
39
|
return router;
|
|
40
40
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { isBrowser } from "@modern-js/runtime";
|
|
3
3
|
import { getGlobalBasename } from "@modern-js/runtime/context";
|
|
4
|
-
import {
|
|
4
|
+
import { localiseTargetPathname } from "../shared/localisedUrls.mjs";
|
|
5
5
|
const getPathname = (context)=>{
|
|
6
6
|
if (isBrowser()) return window.location.pathname;
|
|
7
7
|
return context.ssrContext?.request?.pathname || '/';
|
|
@@ -17,16 +17,23 @@ const getLanguageFromPath = (pathname, languages, fallbackLanguage)=>{
|
|
|
17
17
|
if (languages.includes(firstSegment)) return firstSegment;
|
|
18
18
|
return fallbackLanguage;
|
|
19
19
|
};
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const splitUrlTarget = (target)=>{
|
|
21
|
+
const hashIndex = target.indexOf('#');
|
|
22
|
+
const hash = hashIndex >= 0 ? target.slice(hashIndex) : '';
|
|
23
|
+
const beforeHash = hashIndex >= 0 ? target.slice(0, hashIndex) : target;
|
|
24
|
+
const searchIndex = beforeHash.indexOf('?');
|
|
25
|
+
const search = searchIndex >= 0 ? beforeHash.slice(searchIndex) : '';
|
|
26
|
+
const pathname = searchIndex >= 0 ? beforeHash.slice(0, searchIndex) : beforeHash;
|
|
27
|
+
return {
|
|
28
|
+
pathname,
|
|
29
|
+
search,
|
|
30
|
+
hash
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const buildLocalizedUrl = (target, language, languages, localisedUrls)=>{
|
|
34
|
+
const { pathname, search, hash } = splitUrlTarget(target);
|
|
35
|
+
const localizedPathname = localiseTargetPathname(pathname, language, languages, localisedUrls);
|
|
36
|
+
return `${localizedPathname}${search}${hash}`;
|
|
30
37
|
};
|
|
31
38
|
const detectLanguageFromPath = (pathname, languages, localePathRedirect)=>{
|
|
32
39
|
if (!localePathRedirect) return {
|
|
@@ -54,4 +61,4 @@ const shouldIgnoreRedirect = (pathname, languages, ignoreRedirectRoutes)=>{
|
|
|
54
61
|
if ('function' == typeof ignoreRedirectRoutes) return ignoreRedirectRoutes(normalizedPath);
|
|
55
62
|
return ignoreRedirectRoutes.some((pattern)=>normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`));
|
|
56
63
|
};
|
|
57
|
-
export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect };
|
|
64
|
+
export { buildLocalizedUrl, detectLanguageFromPath, getEntryPath, getLanguageFromPath, getPathname, shouldIgnoreRedirect, splitUrlTarget };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { DEFAULT_I18NEXT_DETECTION_OPTIONS, mergeDetectionOptions } from "../runtime/i18n/detection/config.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { localiseTargetPathname, resolveLocalisedUrlsConfig } from "../shared/localisedUrls.mjs";
|
|
4
4
|
import { getLocaleDetectionOptions } from "../shared/utils.mjs";
|
|
5
5
|
import * as __rspack_external__modern_js_server_core_hono_a76ca254 from "@modern-js/server-core/hono";
|
|
6
6
|
const { languageDetector: languageDetector } = __rspack_external__modern_js_server_core_hono_a76ca254;
|
|
@@ -128,15 +128,7 @@ const buildLocalizedUrl = (req, urlPath, language, languages, localisedUrls)=>{
|
|
|
128
128
|
const pathname = url.pathname;
|
|
129
129
|
const basePath = urlPath.replace('/*', '');
|
|
130
130
|
const remainingPath = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
|
|
131
|
-
const
|
|
132
|
-
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
133
|
-
const pathWithoutLanguage = segments.length > 0 && languages.includes(segments[0]) ? `/${segments.slice(1).join('/')}` : remainingPath;
|
|
134
|
-
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
135
|
-
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
136
|
-
const newPathname = `/${[
|
|
137
|
-
language,
|
|
138
|
-
...resolvedSegments
|
|
139
|
-
].join('/')}`;
|
|
131
|
+
const newPathname = localiseTargetPathname(remainingPath, language, languages, localisedUrls);
|
|
140
132
|
const suffix = `${url.search}${url.hash}`;
|
|
141
133
|
const localizedUrl = '/' === basePath ? newPathname + suffix : basePath + newPathname + suffix;
|
|
142
134
|
return localizedUrl;
|
|
@@ -4,12 +4,13 @@ const LOCALE_PARAM_NAMES = new Set([
|
|
|
4
4
|
'locale',
|
|
5
5
|
'language'
|
|
6
6
|
]);
|
|
7
|
-
const
|
|
7
|
+
const normaliseSlashes = (path)=>{
|
|
8
8
|
const withoutDuplicateSlashes = path.replace(/\/+/g, '/');
|
|
9
9
|
const withLeadingSlash = withoutDuplicateSlashes.startsWith('/') ? withoutDuplicateSlashes : `/${withoutDuplicateSlashes}`;
|
|
10
|
-
|
|
11
|
-
return withoutTrailingSlash.replace(/\[(.+?)\]/g, ':$1');
|
|
10
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, '') : withLeadingSlash;
|
|
12
11
|
};
|
|
12
|
+
const normalisePathPattern = (path)=>normaliseSlashes(path).replace(/\[(.+?)\]/g, ':$1');
|
|
13
|
+
const normalisePathname = (pathname)=>normaliseSlashes(pathname);
|
|
13
14
|
const normaliseRoutePath = (path)=>{
|
|
14
15
|
const normalized = normalisePathPattern(path);
|
|
15
16
|
return '/' === normalized ? '' : normalized.slice(1);
|
|
@@ -35,16 +36,12 @@ const getLeadingLocaleParam = (path)=>{
|
|
|
35
36
|
return getLocaleParamSegment(segments[0] || '');
|
|
36
37
|
};
|
|
37
38
|
const resolveLocalisedUrlsConfig = (option)=>{
|
|
38
|
-
if (
|
|
39
|
-
enabled: false,
|
|
40
|
-
map: {}
|
|
41
|
-
};
|
|
42
|
-
if (option && 'object' == typeof option) return {
|
|
39
|
+
if (option && 'object' == typeof option && Object.keys(option).length > 0) return {
|
|
43
40
|
enabled: true,
|
|
44
41
|
map: option
|
|
45
42
|
};
|
|
46
43
|
return {
|
|
47
|
-
enabled:
|
|
44
|
+
enabled: false,
|
|
48
45
|
map: {}
|
|
49
46
|
};
|
|
50
47
|
};
|
|
@@ -101,7 +98,7 @@ const transformLocalisedRoute = (route, parentCanonicalPath, parentLocalisedPath
|
|
|
101
98
|
if (!localisedUrlEntry) return [
|
|
102
99
|
baseRoute
|
|
103
100
|
];
|
|
104
|
-
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index));
|
|
101
|
+
return getLocalisedRoutePaths(canonicalPath, parentLocalisedPaths, languages, localisedUrlEntry).map((localisedPath, index)=>cloneRouteWithLocalisedPath(baseRoute, localisedPath, index, canonicalPath));
|
|
105
102
|
};
|
|
106
103
|
const legalRouteIdPart = (value)=>value.replace(/[^a-zA-Z0-9_$-]+/g, '_').replace(/^_+|_+$/g, '') || 'index';
|
|
107
104
|
const suffixRouteIds = (route, suffix)=>{
|
|
@@ -116,13 +113,14 @@ const suffixRouteIds = (route, suffix)=>{
|
|
|
116
113
|
} : {}
|
|
117
114
|
};
|
|
118
115
|
};
|
|
119
|
-
const cloneRouteWithLocalisedPath = (route, path, index)=>{
|
|
116
|
+
const cloneRouteWithLocalisedPath = (route, path, index, canonicalPath)=>{
|
|
120
117
|
const leadingLocaleParam = getLeadingLocaleParam(route.path);
|
|
121
118
|
const localisedPath = leadingLocaleParam ? normaliseRoutePath(`${leadingLocaleParam}/${path}`) : path;
|
|
122
119
|
const routeWithPath = {
|
|
123
120
|
...route,
|
|
124
121
|
path: localisedPath
|
|
125
122
|
};
|
|
123
|
+
routeWithPath.modernCanonicalPath = canonicalPath;
|
|
126
124
|
return 0 === index ? routeWithPath : suffixRouteIds(routeWithPath, legalRouteIdPart(localisedPath));
|
|
127
125
|
};
|
|
128
126
|
const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
@@ -135,9 +133,13 @@ const applyLocalisedUrlsToRoutes = (routes, languages, localisedUrls)=>{
|
|
|
135
133
|
};
|
|
136
134
|
const escapeRegExp = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
137
135
|
const getParamName = (segment)=>segment.slice(1).replace(/\?$/, '');
|
|
136
|
+
const compiledPathPatternCache = new Map();
|
|
138
137
|
const compilePathPattern = (pattern)=>{
|
|
138
|
+
const normalizedPattern = normalisePathPattern(pattern);
|
|
139
|
+
const cached = compiledPathPatternCache.get(normalizedPattern);
|
|
140
|
+
if (cached) return cached;
|
|
139
141
|
const names = [];
|
|
140
|
-
const segments =
|
|
142
|
+
const segments = normalizedPattern.split('/').filter(Boolean);
|
|
141
143
|
const source = segments.map((segment)=>{
|
|
142
144
|
if (segment.startsWith(':')) {
|
|
143
145
|
names.push(getParamName(segment));
|
|
@@ -150,19 +152,55 @@ const compilePathPattern = (pattern)=>{
|
|
|
150
152
|
}
|
|
151
153
|
return `/${escapeRegExp(segment)}`;
|
|
152
154
|
}).join('');
|
|
153
|
-
|
|
155
|
+
const compiled = {
|
|
154
156
|
names,
|
|
155
157
|
regexp: new RegExp(`^${source || '/'}$`)
|
|
156
158
|
};
|
|
159
|
+
compiledPathPatternCache.set(normalizedPattern, compiled);
|
|
160
|
+
return compiled;
|
|
161
|
+
};
|
|
162
|
+
const getPatternSpecificity = (pattern)=>{
|
|
163
|
+
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
164
|
+
let staticSegments = 0;
|
|
165
|
+
let dynamicSegments = 0;
|
|
166
|
+
let splatSegments = 0;
|
|
167
|
+
for (const segment of segments)if ('*' === segment) splatSegments++;
|
|
168
|
+
else if (segment.startsWith(':')) dynamicSegments++;
|
|
169
|
+
else staticSegments++;
|
|
170
|
+
return {
|
|
171
|
+
staticSegments,
|
|
172
|
+
dynamicSegments,
|
|
173
|
+
splatSegments,
|
|
174
|
+
totalSegments: segments.length
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const comparePatternSpecificity = (left, right)=>{
|
|
178
|
+
const a = getPatternSpecificity(left);
|
|
179
|
+
const b = getPatternSpecificity(right);
|
|
180
|
+
return b.staticSegments - a.staticSegments || b.totalSegments - a.totalSegments || a.splatSegments - b.splatSegments || a.dynamicSegments - b.dynamicSegments;
|
|
181
|
+
};
|
|
182
|
+
const sortPatternsBySpecificity = (patterns)=>patterns.map((pattern, index)=>({
|
|
183
|
+
pattern,
|
|
184
|
+
index
|
|
185
|
+
})).sort((left, right)=>comparePatternSpecificity(left.pattern.pattern, right.pattern.pattern) || left.index - right.index).map(({ pattern })=>pattern);
|
|
186
|
+
const decodePathParam = (value)=>{
|
|
187
|
+
try {
|
|
188
|
+
return decodeURIComponent(value);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
157
192
|
};
|
|
158
193
|
const matchPathPattern = (pathname, pattern)=>{
|
|
159
194
|
const { names, regexp } = compilePathPattern(pattern);
|
|
160
|
-
const match = regexp.exec(
|
|
195
|
+
const match = regexp.exec(normalisePathname(pathname));
|
|
161
196
|
if (!match) return null;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
const params = {};
|
|
198
|
+
for(let index = 0; index < names.length; index++){
|
|
199
|
+
const decoded = decodePathParam(match[index + 1] || '');
|
|
200
|
+
if (null === decoded) return null;
|
|
201
|
+
params[names[index]] = decoded;
|
|
202
|
+
}
|
|
203
|
+
return params;
|
|
166
204
|
};
|
|
167
205
|
const buildPathFromPattern = (pattern, params)=>{
|
|
168
206
|
const segments = normalisePathPattern(pattern).split('/').filter(Boolean);
|
|
@@ -177,16 +215,70 @@ const buildPathFromPattern = (pattern, params)=>{
|
|
|
177
215
|
return `/${path}`;
|
|
178
216
|
};
|
|
179
217
|
const resolveLocalisedPath = (pathname, targetLanguage, languages, localisedUrls)=>{
|
|
180
|
-
const normalizedPathname =
|
|
181
|
-
|
|
218
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
219
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
220
|
+
pattern: canonicalPattern,
|
|
221
|
+
canonicalPattern,
|
|
222
|
+
localisedUrlEntry
|
|
223
|
+
})));
|
|
224
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
225
|
+
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
226
|
+
if (!targetPattern) continue;
|
|
227
|
+
const params = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
228
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
229
|
+
}
|
|
230
|
+
const localisedCandidates = sortPatternsBySpecificity(Object.values(localisedUrls).flatMap((localisedUrlEntry)=>{
|
|
182
231
|
const targetPattern = localisedUrlEntry[targetLanguage];
|
|
183
|
-
if (targetPattern)
|
|
232
|
+
if (!targetPattern) return [];
|
|
233
|
+
return languages.map((language)=>localisedUrlEntry[language]).filter((sourcePattern)=>Boolean(sourcePattern)).map((sourcePattern)=>({
|
|
234
|
+
pattern: sourcePattern,
|
|
235
|
+
sourcePattern,
|
|
236
|
+
targetPattern
|
|
237
|
+
}));
|
|
238
|
+
}));
|
|
239
|
+
for (const { sourcePattern, targetPattern } of localisedCandidates){
|
|
240
|
+
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
241
|
+
if (params) return buildPathFromPattern(targetPattern, params);
|
|
242
|
+
}
|
|
243
|
+
return normalizedPathname;
|
|
244
|
+
};
|
|
245
|
+
const resolveCanonicalLocalisedPath = (pathname, languages, localisedUrls)=>{
|
|
246
|
+
const normalizedPathname = normalisePathname(pathname);
|
|
247
|
+
const canonicalCandidates = sortPatternsBySpecificity(Object.entries(localisedUrls).map(([canonicalPattern, localisedUrlEntry])=>({
|
|
248
|
+
pattern: canonicalPattern,
|
|
249
|
+
canonicalPattern,
|
|
250
|
+
localisedUrlEntry
|
|
251
|
+
})));
|
|
252
|
+
for (const { canonicalPattern, localisedUrlEntry } of canonicalCandidates){
|
|
253
|
+
const canonicalParams = matchPathPattern(normalizedPathname, canonicalPattern);
|
|
254
|
+
if (canonicalParams) return buildPathFromPattern(canonicalPattern, canonicalParams);
|
|
255
|
+
for (const language of languages){
|
|
184
256
|
const sourcePattern = localisedUrlEntry[language];
|
|
185
257
|
if (!sourcePattern) continue;
|
|
186
258
|
const params = matchPathPattern(normalizedPathname, sourcePattern);
|
|
187
|
-
if (params) return buildPathFromPattern(
|
|
259
|
+
if (params) return buildPathFromPattern(canonicalPattern, params);
|
|
188
260
|
}
|
|
189
261
|
}
|
|
190
262
|
return normalizedPathname;
|
|
191
263
|
};
|
|
192
|
-
|
|
264
|
+
const stripLanguagePrefix = (pathname, languages)=>{
|
|
265
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
266
|
+
if (segments.length > 0 && languages.includes(segments[0])) return `/${segments.slice(1).join('/')}`;
|
|
267
|
+
return pathname || '/';
|
|
268
|
+
};
|
|
269
|
+
const localiseTargetPathname = (pathname, language, languages, localisedUrls)=>{
|
|
270
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
271
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
272
|
+
const resolvedPath = localisedUrlsConfig.enabled ? resolveLocalisedPath(pathWithoutLanguage, language, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
273
|
+
const resolvedSegments = resolvedPath.split('/').filter(Boolean);
|
|
274
|
+
return `/${[
|
|
275
|
+
language,
|
|
276
|
+
...resolvedSegments
|
|
277
|
+
].join('/')}`;
|
|
278
|
+
};
|
|
279
|
+
const canonicalTargetPathname = (pathname, languages, localisedUrls)=>{
|
|
280
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname, languages);
|
|
281
|
+
const localisedUrlsConfig = resolveLocalisedUrlsConfig(localisedUrls);
|
|
282
|
+
return localisedUrlsConfig.enabled ? resolveCanonicalLocalisedPath(pathWithoutLanguage, languages, localisedUrlsConfig.map) : pathWithoutLanguage;
|
|
283
|
+
};
|
|
284
|
+
export { applyLocalisedUrlsToRoutes, buildPathFromPattern, canonicalTargetPathname, localiseTargetPathname, matchPathPattern, normalisePathPattern, normalisePathname, resolveCanonicalLocalisedPath, resolveLocalisedPath, resolveLocalisedUrlsConfig, validateLocalisedUrls };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AppTools, CliPlugin } from '@modern-js/app-tools';
|
|
2
2
|
import type { Entrypoint } from '@modern-js/types';
|
|
3
3
|
import type { BackendOptions, LocaleDetectionOptions } from '../shared/type';
|
|
4
|
+
import '../runtime/types';
|
|
4
5
|
export type TransformRuntimeConfigFn = (extendedConfig: Record<string, any>, entrypoint: Entrypoint) => Record<string, any>;
|
|
5
6
|
export interface I18nPluginOptions {
|
|
6
7
|
localeDetection?: LocaleDetectionOptions;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BackendOptions } from '../shared/type';
|
|
2
|
+
interface NormalizedConfigForLocales {
|
|
3
|
+
server?: {
|
|
4
|
+
publicDir?: string | string[];
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export interface DetectedLocalesDirectory {
|
|
8
|
+
loadPath: string;
|
|
9
|
+
addPath: string;
|
|
10
|
+
serverLoadPath: string;
|
|
11
|
+
serverAddPath: string;
|
|
12
|
+
serverLoadPaths?: string[];
|
|
13
|
+
serverAddPaths?: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function detectLocalesDirectory(appDirectory: string, normalizedConfig?: NormalizedConfigForLocales): DetectedLocalesDirectory | undefined;
|
|
16
|
+
export declare function applyDetectedBackendPaths(backendOptions: BackendOptions, detectedLocales: DetectedLocalesDirectory | undefined): BackendOptions;
|
|
17
|
+
export {};
|
|
@@ -5,19 +5,10 @@ export interface I18nLinkProps {
|
|
|
5
5
|
[key: string]: any;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* ```tsx
|
|
13
|
-
* // When current language is 'en' and to="/about"
|
|
14
|
-
* // The actual link will be "/en/about"
|
|
15
|
-
* <I18nLink to="/about">About</I18nLink>
|
|
16
|
-
*
|
|
17
|
-
* // When current language is 'zh' and to="/"
|
|
18
|
-
* // The actual link will be "/zh"
|
|
19
|
-
* <I18nLink to="/">Home</I18nLink>
|
|
20
|
-
* ```
|
|
8
|
+
* @deprecated Use {@link Link} from `@modern-js/plugin-i18n/runtime` instead.
|
|
9
|
+
* `Link` accepts the same language-agnostic `to` values and additionally
|
|
10
|
+
* supports `#hash`/`?query` targets, typed canonical routes, `params`
|
|
11
|
+
* interpolation and language-invariant active state.
|
|
21
12
|
*/
|
|
22
13
|
export declare const I18nLink: React.FC<I18nLinkProps>;
|
|
23
14
|
export default I18nLink;
|
|
@@ -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 {};
|