@bleedingdev/modern-js-plugin-i18n 3.2.0-ultramodern.119 → 3.2.0-ultramodern.120
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/runtime/I18nLink.js +7 -17
- package/dist/cjs/runtime/Link.js +252 -0
- package/dist/cjs/runtime/canonicalRoutes.js +18 -0
- package/dist/cjs/runtime/index.js +23 -0
- package/dist/cjs/runtime/localizedPaths.js +105 -0
- package/dist/cjs/runtime/utils.js +22 -5
- package/dist/cjs/shared/localisedUrls.js +32 -2
- package/dist/esm/runtime/I18nLink.mjs +6 -16
- package/dist/esm/runtime/Link.mjs +209 -0
- package/dist/esm/runtime/canonicalRoutes.mjs +0 -0
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/localizedPaths.mjs +58 -0
- package/dist/esm/runtime/utils.mjs +18 -4
- package/dist/esm/shared/localisedUrls.mjs +24 -3
- package/dist/esm-node/runtime/I18nLink.mjs +6 -16
- package/dist/esm-node/runtime/Link.mjs +210 -0
- package/dist/esm-node/runtime/canonicalRoutes.mjs +1 -0
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/localizedPaths.mjs +59 -0
- package/dist/esm-node/runtime/utils.mjs +18 -4
- package/dist/esm-node/shared/localisedUrls.mjs +24 -3
- package/dist/types/runtime/I18nLink.d.ts +4 -13
- package/dist/types/runtime/Link.d.ts +56 -0
- package/dist/types/runtime/canonicalRoutes.d.ts +60 -0
- package/dist/types/runtime/index.d.ts +5 -1
- package/dist/types/runtime/localizedPaths.d.ts +39 -0
- package/dist/types/runtime/utils.d.ts +12 -3
- package/dist/types/shared/localisedUrls.d.ts +8 -0
- package/package.json +13 -13
- package/rstest.config.mts +2 -2
- package/src/runtime/I18nLink.tsx +13 -46
- package/src/runtime/Link.tsx +414 -0
- package/src/runtime/canonicalRoutes.ts +93 -0
- package/src/runtime/index.tsx +24 -2
- package/src/runtime/localizedPaths.ts +118 -0
- package/src/runtime/utils.ts +24 -5
- package/src/shared/localisedUrls.ts +63 -3
- package/tests/link.test.tsx +475 -0
- package/tests/linkTypes.test.ts +28 -0
- package/tests/type-fixture/linkTypes.fixture.tsx +51 -0
- package/tests/type-fixture/tsconfig.json +15 -0
package/README.md
CHANGED
|
@@ -2,20 +2,236 @@
|
|
|
2
2
|
<a href="https://modernjs.dev" target="blank"><img src="https://lf3-static.bytednsdoc.com/obj/eden-cn/ylaelkeh7nuhfnuhf/modernjs-cover.png" width="300" alt="Modern.js Logo" /></a>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center"
|
|
5
|
+
<h1 align="center">@modern-js/plugin-i18n</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
Internationalization plugin for Modern.js, providing multi-language support with locale-aware routing and navigation.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Locale detection**: Supports detection from URL path, Cookie, LocalStorage, request headers, browser settings, and other sources.
|
|
14
|
+
- **Resource loading**: Supports HTTP static files, file system loading for SSR, custom SDK functions, and chained backend progressive loading.
|
|
15
|
+
- **Routing integration**: Automatically adds locale path prefixes and provides the `Link` component for language-agnostic navigation.
|
|
16
|
+
- **SSR support**: Detects the language on the server and injects it into the page to avoid language flicker.
|
|
17
|
+
- **TypeScript support**: Provides complete type definitions and type-safe route navigation.
|
|
18
|
+
|
|
11
19
|
## Getting Started
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
For full documentation, see the [Modern.js Internationalization Guide](https://modernjs.dev/en/guides/advanced-features/international/).
|
|
22
|
+
|
|
23
|
+
## Link Component
|
|
24
|
+
|
|
25
|
+
The `Link` component is the standard way to create navigation links in i18n-enabled applications. It automatically localizes language-agnostic canonical paths and provides type-safe navigation with params validation.
|
|
26
|
+
|
|
27
|
+
### Basic Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Link } from '@modern-js/plugin-i18n/runtime';
|
|
31
|
+
|
|
32
|
+
function Navigation() {
|
|
33
|
+
return (
|
|
34
|
+
<nav>
|
|
35
|
+
<Link to="/">Home</Link>
|
|
36
|
+
<Link to="/about">About</Link>
|
|
37
|
+
<Link to="/talks/$slug" params={{ slug: 'my-talk' }} />
|
|
38
|
+
<Link to="/#work-with-me" />
|
|
39
|
+
</nav>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Key Features
|
|
45
|
+
|
|
46
|
+
**Canonical path localization**: `to` accepts language-agnostic canonical paths. The Link automatically adds the locale prefix and applies `localisedUrls` slug mappings:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// When current language is 'cs' and localisedUrls maps '/platform' to '/platforma':
|
|
50
|
+
<Link to="/platform" />
|
|
51
|
+
// renders as: <a href="/cs/platforma">...</a>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Hash and query preservation**: Hash fragments and query strings in `to` are preserved during localization:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<Link to="/#work-with-me" /> // Cross-page anchor → /cs#work-with-me
|
|
58
|
+
<Link to="/talks?sort=date" /> // Search params preserved
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Typed routes and params**: When used with TanStack Router codegen, `to` accepts typed canonical route paths with validated params:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Route /talks/$slug exists → TypeScript validates this:
|
|
65
|
+
<Link to="/talks/$slug" params={{ slug: 'my-talk' }} />
|
|
66
|
+
|
|
67
|
+
// TypeScript error: route does not exist
|
|
68
|
+
<Link to="/talkz" />
|
|
69
|
+
|
|
70
|
+
// TypeScript error: missing required param
|
|
71
|
+
<Link to="/talks/$slug" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Language-invariant active state**: The link is marked active when the location matches any localized variant of the canonical route:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// Matches /en/talks/my-talk, /cs/prednaska/muj-pohovor, etc.
|
|
78
|
+
<Link to="/talks/$slug" params={{ slug: 'my-talk' }} activeProps={{ className: 'active' }} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Active state props and classes**:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Link
|
|
85
|
+
to="/about"
|
|
86
|
+
activeProps={{ className: 'active-link' }}
|
|
87
|
+
activeOptions={{ exact: false }} // Nested routes also match
|
|
88
|
+
/>
|
|
89
|
+
// When active, renders: <a href="..." className="active-link" data-status="active" aria-current="page">
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**External URLs and bare anchors render as plain `<a>` tags**:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<Link to="https://example.com" /> // → <a href="https://example.com">
|
|
96
|
+
<Link to="#contact" /> // → <a href="#contact">
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Props
|
|
100
|
+
|
|
101
|
+
- `to` (string): Canonical (language-agnostic) target path, optionally with `#hash` and `?query` suffixes.
|
|
102
|
+
- `params` (object, optional): Route param values for dynamic segments (e.g., `$slug`).
|
|
103
|
+
- `hash` (string, optional): Hash fragment (overrides any `#hash` in `to`).
|
|
104
|
+
- `search` (string | object, optional): Query string or object (overrides any `?query` in `to`).
|
|
105
|
+
- `hashScrollIntoView` (boolean | ScrollIntoViewOptions, optional): TanStack Router scroll behavior.
|
|
106
|
+
- `activeProps` (object, optional): Props applied when the link is active.
|
|
107
|
+
- `activeOptions` (object, optional): `{ exact?: boolean }` for active state matching.
|
|
108
|
+
- All standard `<a>` HTML attributes (className, style, onClick, etc.).
|
|
109
|
+
|
|
110
|
+
## Localization Utilities
|
|
111
|
+
|
|
112
|
+
### `localizePath(pathname, language, config)`
|
|
113
|
+
|
|
114
|
+
Localize a canonical pathname for a given language, applying language prefix and `localisedUrls` mapping:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { localizePath } from '@modern-js/plugin-i18n/runtime';
|
|
118
|
+
|
|
119
|
+
localizePath('/about', 'cs', {
|
|
120
|
+
languages: ['en', 'cs'],
|
|
121
|
+
localisedUrls: { '/about': { en: '/about', cs: '/o-nas' } }
|
|
122
|
+
})
|
|
123
|
+
// → '/cs/o-nas'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `canonicalPath(target, config)`
|
|
127
|
+
|
|
128
|
+
Reverse of `localizePath`: strip language prefix and reverse `localisedUrls` mapping:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { canonicalPath } from '@modern-js/plugin-i18n/runtime';
|
|
132
|
+
|
|
133
|
+
canonicalPath('/cs/o-nas', {
|
|
134
|
+
languages: ['en', 'cs'],
|
|
135
|
+
localisedUrls: { '/about': { en: '/about', cs: '/o-nas' } }
|
|
136
|
+
})
|
|
137
|
+
// → '/about'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `useLocalizedPaths()`
|
|
141
|
+
|
|
142
|
+
Hook for context-bound `localizePath` and `canonicalPath` (reads plugin config automatically):
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { useLocalizedPaths } from '@modern-js/plugin-i18n/runtime';
|
|
146
|
+
|
|
147
|
+
function MyComponent() {
|
|
148
|
+
const { localizePath, canonicalPath } = useLocalizedPaths();
|
|
149
|
+
|
|
150
|
+
const localized = localizePath('/about', 'cs');
|
|
151
|
+
const canonical = canonicalPath('/cs/o-nas');
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### `useLocalizedLocation()`
|
|
156
|
+
|
|
157
|
+
Hook for hreflang tags and language switchers. Returns the current location's canonical path and per-language hrefs:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
import { useLocalizedLocation } from '@modern-js/plugin-i18n/runtime';
|
|
161
|
+
|
|
162
|
+
function HrefLang() {
|
|
163
|
+
const { language, canonical, alternates } = useLocalizedLocation();
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<link rel="canonical" href={alternates['en']} />
|
|
168
|
+
{Object.entries(alternates).map(([lang, href]) => (
|
|
169
|
+
<link key={lang} rel="alternate" hrefLang={lang} href={href} />
|
|
170
|
+
))}
|
|
171
|
+
</>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function LanguageSwitcher() {
|
|
176
|
+
const { alternates } = useLocalizedLocation();
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<select onChange={(e) => window.location.href = alternates[e.target.value]}>
|
|
180
|
+
{Object.entries(alternates).map(([lang, href]) => (
|
|
181
|
+
<option key={lang} value={lang}>{lang}</option>
|
|
182
|
+
))}
|
|
183
|
+
</select>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Migration from Deprecated I18nLink
|
|
189
|
+
|
|
190
|
+
The deprecated `I18nLink` component is now an alias for `Link`. Update your code:
|
|
191
|
+
|
|
192
|
+
| Old Pattern | New Pattern | Notes |
|
|
193
|
+
|-------------|-------------|-------|
|
|
194
|
+
| `import { I18nLink }` | `import { Link }` | Drop the `I18n` prefix. |
|
|
195
|
+
| `<I18nLink to="/">` | `<Link to="/">` | Identical props and behavior. |
|
|
196
|
+
| Hand-rolled `localizePath` helper | `useLocalizedPaths()` hook | Reads config from context automatically. |
|
|
197
|
+
| Hand-rolled `matchPattern` for active state | `Link` with `activeProps`/`activeOptions` | Language-invariant matching. |
|
|
198
|
+
| Manual hreflang blocks | `useLocalizedLocation()` hook | Generates per-language hrefs automatically. |
|
|
199
|
+
|
|
200
|
+
**I18nLink deprecation notice:** `I18nLink` is deprecated and will be removed in a future version. A one-time development console warning is logged when used.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
// ❌ Old
|
|
204
|
+
import { I18nLink } from '@modern-js/plugin-i18n/runtime';
|
|
205
|
+
<I18nLink to="/about">About</I18nLink>
|
|
206
|
+
|
|
207
|
+
// ✅ New
|
|
208
|
+
import { Link } from '@modern-js/plugin-i18n/runtime';
|
|
209
|
+
<Link to="/about">About</Link>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Exported Types
|
|
213
|
+
|
|
214
|
+
For TypeScript projects, the following types are exported from `@modern-js/plugin-i18n/runtime`:
|
|
215
|
+
|
|
216
|
+
- `LinkProps<TTo>`: Props type for the `Link` component with typed `to` and `params`.
|
|
217
|
+
- `LinkBaseProps`: Base props shared across router frameworks.
|
|
218
|
+
- `LinkParams`: Record of route param values.
|
|
219
|
+
- `LinkActiveOptions`: Active state matching options.
|
|
220
|
+
- `UltramodernCanonicalRoutes`: Canonical route map (emitted by TanStack Router codegen).
|
|
221
|
+
- `CanonicalRoutePath`: Typed canonical route path.
|
|
222
|
+
- `AllowedLinkTarget`: Union of all valid canonical routes (when codegen is active).
|
|
223
|
+
|
|
224
|
+
## Exported Utilities
|
|
225
|
+
|
|
226
|
+
- `buildLocalizedUrl(pathname, language, languages, localisedUrls)`: Localize a pathname (used internally by `Link`).
|
|
227
|
+
- `splitUrlTarget(urlString)`: Parse a URL into pathname, search, and hash.
|
|
14
228
|
|
|
15
229
|
## Documentation
|
|
16
230
|
|
|
17
|
-
|
|
18
|
-
|
|
231
|
+
For complete documentation, including locale detection, resource loading, SSR, and custom route configuration, see:
|
|
232
|
+
|
|
233
|
+
- [English Documentation](https://modernjs.dev/en/guides/advanced-features/international/)
|
|
234
|
+
- [中文文档](https://modernjs.dev/guides/advanced-features/international/)
|
|
19
235
|
|
|
20
236
|
## Contributing
|
|
21
237
|
|
|
@@ -24,9 +240,3 @@ Please read the [Contributing Guide](https://github.com/web-infra-dev/modern.js/
|
|
|
24
240
|
## License
|
|
25
241
|
|
|
26
242
|
Modern.js is [MIT licensed](https://github.com/web-infra-dev/modern.js/blob/main/LICENSE).
|
|
27
|
-
|
|
28
|
-
## Credist
|
|
29
|
-
|
|
30
|
-
Thanks to:
|
|
31
|
-
|
|
32
|
-
- [@loadable/webpack-plugin](https://github.com/gregberge/loadable-components) to create a webpack plugin prepare for loadable usage in ssr.
|
|
@@ -32,25 +32,15 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
32
32
|
default: ()=>runtime_I18nLink
|
|
33
33
|
});
|
|
34
34
|
const jsx_runtime_namespaceObject = require("react/jsx-runtime");
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const external_utils_js_namespaceObject = require("./utils.js");
|
|
35
|
+
const external_Link_js_namespaceObject = require("./Link.js");
|
|
36
|
+
let warnedDeprecation = false;
|
|
38
37
|
const I18nLink = ({ to, children, ...props })=>{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const localizedTo = (0, external_utils_js_namespaceObject.buildLocalizedUrl)(to, currentLang, supportedLanguages, localisedUrls);
|
|
43
|
-
if ('development' === process.env.NODE_ENV && hasRouter && !params.lang) console.warn("I18nLink is being used outside of a :lang dynamic route context. This may cause unexpected behavior. Please ensure I18nLink is used within a route that has a :lang parameter.");
|
|
44
|
-
if (!hasRouter || !Link) {
|
|
45
|
-
const { prefetch: _prefetch, preload: _preload, ...anchorProps } = props;
|
|
46
|
-
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("a", {
|
|
47
|
-
href: localizedTo,
|
|
48
|
-
...anchorProps,
|
|
49
|
-
children: children
|
|
50
|
-
});
|
|
38
|
+
if ('development' === process.env.NODE_ENV && !warnedDeprecation) {
|
|
39
|
+
warnedDeprecation = true;
|
|
40
|
+
console.warn("[plugin-i18n] I18nLink is deprecated. Import { Link } from '@modern-js/plugin-i18n/runtime' instead — it accepts the same language-agnostic `to` values.");
|
|
51
41
|
}
|
|
52
|
-
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(Link, {
|
|
53
|
-
to:
|
|
42
|
+
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_Link_js_namespaceObject.Link, {
|
|
43
|
+
to: to,
|
|
54
44
|
...props,
|
|
55
45
|
children: children
|
|
56
46
|
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
+
var define = (defs, kind)=>{
|
|
6
|
+
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
[kind]: defs[key]
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
define(getters, "get");
|
|
12
|
+
define(values, "value");
|
|
13
|
+
};
|
|
14
|
+
})();
|
|
15
|
+
(()=>{
|
|
16
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
+
})();
|
|
18
|
+
(()=>{
|
|
19
|
+
__webpack_require__.r = (exports1)=>{
|
|
20
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
+
value: 'Module'
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
+
value: true
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
})();
|
|
28
|
+
var __webpack_exports__ = {};
|
|
29
|
+
__webpack_require__.r(__webpack_exports__);
|
|
30
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
31
|
+
Link: ()=>Link,
|
|
32
|
+
default: ()=>runtime_Link,
|
|
33
|
+
interpolateRouteParams: ()=>interpolateRouteParams
|
|
34
|
+
});
|
|
35
|
+
const jsx_runtime_namespaceObject = require("react/jsx-runtime");
|
|
36
|
+
const external_react_namespaceObject = require("react");
|
|
37
|
+
const external_context_js_namespaceObject = require("./context.js");
|
|
38
|
+
const external_localizedPaths_js_namespaceObject = require("./localizedPaths.js");
|
|
39
|
+
const external_routerAdapter_js_namespaceObject = require("./routerAdapter.js");
|
|
40
|
+
const external_utils_js_namespaceObject = require("./utils.js");
|
|
41
|
+
const EXTERNAL_TARGET_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
42
|
+
const warnedTargets = new Set();
|
|
43
|
+
const warnOnce = (key, message)=>{
|
|
44
|
+
if ('development' !== process.env.NODE_ENV || warnedTargets.has(key)) return;
|
|
45
|
+
warnedTargets.add(key);
|
|
46
|
+
console.warn(message);
|
|
47
|
+
};
|
|
48
|
+
const interpolateRouteParams = (pathname, params)=>{
|
|
49
|
+
if (!/[$:*{]/.test(pathname)) return pathname;
|
|
50
|
+
const resolveParam = (name)=>{
|
|
51
|
+
const value = params?.[name];
|
|
52
|
+
return void 0 === value ? void 0 : String(value);
|
|
53
|
+
};
|
|
54
|
+
const segments = pathname.split('/').map((segment)=>{
|
|
55
|
+
if (!segment) return segment;
|
|
56
|
+
if (segment.startsWith('{-$') && segment.endsWith('}')) {
|
|
57
|
+
const value = resolveParam(segment.slice(3, -1));
|
|
58
|
+
return void 0 === value ? null : encodeURIComponent(value);
|
|
59
|
+
}
|
|
60
|
+
if ('$' === segment || '*' === segment) {
|
|
61
|
+
const value = resolveParam('_splat') ?? resolveParam('*');
|
|
62
|
+
return void 0 === value ? null : value.split('/').map(encodeURIComponent).join('/');
|
|
63
|
+
}
|
|
64
|
+
if (segment.startsWith('$')) {
|
|
65
|
+
const value = resolveParam(segment.slice(1));
|
|
66
|
+
if (void 0 === value) {
|
|
67
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${segment.slice(1)}".`);
|
|
68
|
+
return segment;
|
|
69
|
+
}
|
|
70
|
+
return encodeURIComponent(value);
|
|
71
|
+
}
|
|
72
|
+
if (segment.startsWith(':')) {
|
|
73
|
+
const optional = segment.endsWith('?');
|
|
74
|
+
const name = segment.slice(1, optional ? -1 : void 0);
|
|
75
|
+
const value = resolveParam(name);
|
|
76
|
+
if (void 0 === value) {
|
|
77
|
+
if (optional) return null;
|
|
78
|
+
warnOnce(`missing-param:${pathname}:${segment}`, `[plugin-i18n] <Link to="${pathname}"> is missing required param "${name}".`);
|
|
79
|
+
return segment;
|
|
80
|
+
}
|
|
81
|
+
return encodeURIComponent(value);
|
|
82
|
+
}
|
|
83
|
+
return segment;
|
|
84
|
+
}).filter((segment)=>null !== segment);
|
|
85
|
+
return segments.join('/') || '/';
|
|
86
|
+
};
|
|
87
|
+
const normalizeSearch = (search, searchFromTo)=>{
|
|
88
|
+
if (search && 'object' == typeof search) {
|
|
89
|
+
const entries = Object.entries(search).filter(([, value])=>null != value);
|
|
90
|
+
const searchObject = Object.fromEntries(entries.map(([key, value])=>[
|
|
91
|
+
key,
|
|
92
|
+
String(value)
|
|
93
|
+
]));
|
|
94
|
+
const params = new URLSearchParams(searchObject);
|
|
95
|
+
const serialized = params.toString();
|
|
96
|
+
return {
|
|
97
|
+
searchString: serialized ? `?${serialized}` : '',
|
|
98
|
+
searchObject
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const raw = 'string' == typeof search && search ? search : searchFromTo;
|
|
102
|
+
if (!raw) return {
|
|
103
|
+
searchString: '',
|
|
104
|
+
searchObject: void 0
|
|
105
|
+
};
|
|
106
|
+
const searchString = raw.startsWith('?') ? raw : `?${raw}`;
|
|
107
|
+
const searchObject = {};
|
|
108
|
+
new URLSearchParams(searchString).forEach((value, key)=>{
|
|
109
|
+
searchObject[key] = value;
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
searchString,
|
|
113
|
+
searchObject
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const splitActiveProps = (active, activeProps)=>{
|
|
117
|
+
if (!active || !activeProps) return {};
|
|
118
|
+
return activeProps;
|
|
119
|
+
};
|
|
120
|
+
const mergeClassNames = (...values)=>{
|
|
121
|
+
const classNames = values.filter((value)=>'string' == typeof value && value.length > 0);
|
|
122
|
+
return classNames.length > 0 ? classNames.join(' ') : void 0;
|
|
123
|
+
};
|
|
124
|
+
const Link = (props)=>{
|
|
125
|
+
const { to, params, children, hash: hashProp, search: searchProp, hashScrollIntoView, activeOptions, activeProps, ...rest } = props;
|
|
126
|
+
const adapter = (0, external_routerAdapter_js_namespaceObject.useI18nRouterAdapter)();
|
|
127
|
+
const { language, supportedLanguages, localisedUrls } = (0, external_context_js_namespaceObject.useModernI18n)();
|
|
128
|
+
const config = {
|
|
129
|
+
languages: supportedLanguages,
|
|
130
|
+
localisedUrls
|
|
131
|
+
};
|
|
132
|
+
const isExternal = EXTERNAL_TARGET_RE.test(to);
|
|
133
|
+
const isBareHash = to.startsWith('#');
|
|
134
|
+
const target = (0, external_react_namespaceObject.useMemo)(()=>{
|
|
135
|
+
if (isExternal || isBareHash) return null;
|
|
136
|
+
const { pathname, search: searchFromTo, hash: hashFromTo } = (0, external_utils_js_namespaceObject.splitUrlTarget)(to);
|
|
137
|
+
const interpolated = interpolateRouteParams(pathname || '/', params);
|
|
138
|
+
const firstSegment = interpolated.split('/').filter(Boolean)[0];
|
|
139
|
+
if (firstSegment && supportedLanguages.includes(firstSegment)) warnOnce(`lang-prefix:${to}`, `[plugin-i18n] <Link to="${to}"> starts with a language prefix. Write language-agnostic canonical paths; the Link localizes them automatically.`);
|
|
140
|
+
const localizedPathname = (0, external_utils_js_namespaceObject.buildLocalizedUrl)(interpolated, language, supportedLanguages, localisedUrls);
|
|
141
|
+
const hash = hashProp ?? (hashFromTo ? hashFromTo.slice(1) : '');
|
|
142
|
+
const { searchString, searchObject } = normalizeSearch(searchProp, searchFromTo);
|
|
143
|
+
return {
|
|
144
|
+
canonicalPathname: interpolated,
|
|
145
|
+
localizedPathname,
|
|
146
|
+
hash,
|
|
147
|
+
searchString,
|
|
148
|
+
searchObject,
|
|
149
|
+
href: `${localizedPathname}${searchString}${hash ? `#${hash}` : ''}`
|
|
150
|
+
};
|
|
151
|
+
}, [
|
|
152
|
+
to,
|
|
153
|
+
params,
|
|
154
|
+
hashProp,
|
|
155
|
+
searchProp,
|
|
156
|
+
isExternal,
|
|
157
|
+
isBareHash,
|
|
158
|
+
language,
|
|
159
|
+
supportedLanguages,
|
|
160
|
+
localisedUrls
|
|
161
|
+
]);
|
|
162
|
+
const isActive = (0, external_react_namespaceObject.useMemo)(()=>{
|
|
163
|
+
if (!target || !adapter.location) return false;
|
|
164
|
+
const current = (0, external_localizedPaths_js_namespaceObject.canonicalPath)(adapter.location.pathname, config);
|
|
165
|
+
const targetCanonical = (0, external_localizedPaths_js_namespaceObject.canonicalPath)(target.canonicalPathname, config);
|
|
166
|
+
const exact = activeOptions?.exact ?? '/' === targetCanonical;
|
|
167
|
+
if (current === targetCanonical) return true;
|
|
168
|
+
if (exact) return false;
|
|
169
|
+
return current.startsWith('/' === targetCanonical ? '/' : `${targetCanonical}/`);
|
|
170
|
+
}, [
|
|
171
|
+
target,
|
|
172
|
+
adapter.location,
|
|
173
|
+
activeOptions?.exact,
|
|
174
|
+
supportedLanguages,
|
|
175
|
+
localisedUrls
|
|
176
|
+
]);
|
|
177
|
+
const resolvedActiveProps = splitActiveProps(isActive, activeProps);
|
|
178
|
+
const activeAttributes = isActive ? {
|
|
179
|
+
'data-status': 'active',
|
|
180
|
+
'aria-current': rest['aria-current'] ?? resolvedActiveProps['aria-current'] ?? 'page'
|
|
181
|
+
} : {};
|
|
182
|
+
if (!target) {
|
|
183
|
+
const { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
|
|
184
|
+
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("a", {
|
|
185
|
+
href: to,
|
|
186
|
+
...anchorProps,
|
|
187
|
+
children: children
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const { Link: RouterLink, hasRouter, framework } = adapter;
|
|
191
|
+
if (!hasRouter || !RouterLink) {
|
|
192
|
+
const { prefetch: _prefetch, preload: _preload, replace: _replace, ...anchorProps } = rest;
|
|
193
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
194
|
+
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("a", {
|
|
195
|
+
href: target.href,
|
|
196
|
+
...anchorProps,
|
|
197
|
+
...activeRest,
|
|
198
|
+
...activeAttributes,
|
|
199
|
+
className: mergeClassNames(rest.className, activeClassName),
|
|
200
|
+
style: {
|
|
201
|
+
...rest.style,
|
|
202
|
+
...activeStyle
|
|
203
|
+
},
|
|
204
|
+
children: children
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const { className: activeClassName, style: activeStyle, ...activeRest } = resolvedActiveProps;
|
|
208
|
+
const mergedClassName = mergeClassNames(rest.className, activeClassName);
|
|
209
|
+
const mergedStyle = {
|
|
210
|
+
...rest.style,
|
|
211
|
+
...activeStyle
|
|
212
|
+
};
|
|
213
|
+
if ('tanstack' === framework) return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(RouterLink, {
|
|
214
|
+
to: target.localizedPathname,
|
|
215
|
+
...target.searchObject ? {
|
|
216
|
+
search: target.searchObject
|
|
217
|
+
} : {},
|
|
218
|
+
...target.hash ? {
|
|
219
|
+
hash: target.hash
|
|
220
|
+
} : {},
|
|
221
|
+
...void 0 === hashScrollIntoView ? {} : {
|
|
222
|
+
hashScrollIntoView
|
|
223
|
+
},
|
|
224
|
+
...rest,
|
|
225
|
+
...activeRest,
|
|
226
|
+
...activeAttributes,
|
|
227
|
+
className: mergedClassName,
|
|
228
|
+
style: mergedStyle,
|
|
229
|
+
children: children
|
|
230
|
+
});
|
|
231
|
+
return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(RouterLink, {
|
|
232
|
+
to: target.href,
|
|
233
|
+
...rest,
|
|
234
|
+
...activeRest,
|
|
235
|
+
...activeAttributes,
|
|
236
|
+
className: mergedClassName,
|
|
237
|
+
style: mergedStyle,
|
|
238
|
+
children: children
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
const runtime_Link = Link;
|
|
242
|
+
exports.Link = __webpack_exports__.Link;
|
|
243
|
+
exports["default"] = __webpack_exports__["default"];
|
|
244
|
+
exports.interpolateRouteParams = __webpack_exports__.interpolateRouteParams;
|
|
245
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
246
|
+
"Link",
|
|
247
|
+
"default",
|
|
248
|
+
"interpolateRouteParams"
|
|
249
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
250
|
+
Object.defineProperty(exports, '__esModule', {
|
|
251
|
+
value: true
|
|
252
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.r = (exports1)=>{
|
|
5
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
6
|
+
value: 'Module'
|
|
7
|
+
});
|
|
8
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
9
|
+
value: true
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
})();
|
|
13
|
+
var __webpack_exports__ = {};
|
|
14
|
+
__webpack_require__.r(__webpack_exports__);
|
|
15
|
+
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
16
|
+
Object.defineProperty(exports, '__esModule', {
|
|
17
|
+
value: true
|
|
18
|
+
});
|
|
@@ -29,8 +29,15 @@ var __webpack_exports__ = {};
|
|
|
29
29
|
__webpack_require__.r(__webpack_exports__);
|
|
30
30
|
__webpack_require__.d(__webpack_exports__, {
|
|
31
31
|
I18nLink: ()=>external_I18nLink_js_namespaceObject.I18nLink,
|
|
32
|
+
Link: ()=>external_Link_js_namespaceObject.Link,
|
|
33
|
+
buildLocalizedUrl: ()=>external_utils_js_namespaceObject.buildLocalizedUrl,
|
|
34
|
+
canonicalPath: ()=>external_localizedPaths_js_namespaceObject.canonicalPath,
|
|
32
35
|
default: ()=>runtime,
|
|
33
36
|
i18nPlugin: ()=>i18nPlugin,
|
|
37
|
+
localizePath: ()=>external_localizedPaths_js_namespaceObject.localizePath,
|
|
38
|
+
splitUrlTarget: ()=>external_utils_js_namespaceObject.splitUrlTarget,
|
|
39
|
+
useLocalizedLocation: ()=>external_localizedPaths_js_namespaceObject.useLocalizedLocation,
|
|
40
|
+
useLocalizedPaths: ()=>external_localizedPaths_js_namespaceObject.useLocalizedPaths,
|
|
34
41
|
useModernI18n: ()=>external_context_js_namespaceObject.useModernI18n
|
|
35
42
|
});
|
|
36
43
|
const jsx_runtime_namespaceObject = require("react/jsx-runtime");
|
|
@@ -50,6 +57,8 @@ const utils_js_namespaceObject = require("./i18n/utils.js");
|
|
|
50
57
|
const external_utils_js_namespaceObject = require("./utils.js");
|
|
51
58
|
require("./types.js");
|
|
52
59
|
const external_I18nLink_js_namespaceObject = require("./I18nLink.js");
|
|
60
|
+
const external_Link_js_namespaceObject = require("./Link.js");
|
|
61
|
+
const external_localizedPaths_js_namespaceObject = require("./localizedPaths.js");
|
|
53
62
|
const i18nPlugin = (options)=>({
|
|
54
63
|
name: '@modern-js/plugin-i18n',
|
|
55
64
|
setup: (api)=>{
|
|
@@ -172,13 +181,27 @@ const i18nPlugin = (options)=>({
|
|
|
172
181
|
});
|
|
173
182
|
const runtime = i18nPlugin;
|
|
174
183
|
exports.I18nLink = __webpack_exports__.I18nLink;
|
|
184
|
+
exports.Link = __webpack_exports__.Link;
|
|
185
|
+
exports.buildLocalizedUrl = __webpack_exports__.buildLocalizedUrl;
|
|
186
|
+
exports.canonicalPath = __webpack_exports__.canonicalPath;
|
|
175
187
|
exports["default"] = __webpack_exports__["default"];
|
|
176
188
|
exports.i18nPlugin = __webpack_exports__.i18nPlugin;
|
|
189
|
+
exports.localizePath = __webpack_exports__.localizePath;
|
|
190
|
+
exports.splitUrlTarget = __webpack_exports__.splitUrlTarget;
|
|
191
|
+
exports.useLocalizedLocation = __webpack_exports__.useLocalizedLocation;
|
|
192
|
+
exports.useLocalizedPaths = __webpack_exports__.useLocalizedPaths;
|
|
177
193
|
exports.useModernI18n = __webpack_exports__.useModernI18n;
|
|
178
194
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
179
195
|
"I18nLink",
|
|
196
|
+
"Link",
|
|
197
|
+
"buildLocalizedUrl",
|
|
198
|
+
"canonicalPath",
|
|
180
199
|
"default",
|
|
181
200
|
"i18nPlugin",
|
|
201
|
+
"localizePath",
|
|
202
|
+
"splitUrlTarget",
|
|
203
|
+
"useLocalizedLocation",
|
|
204
|
+
"useLocalizedPaths",
|
|
182
205
|
"useModernI18n"
|
|
183
206
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
184
207
|
Object.defineProperty(exports, '__esModule', {
|