@rangojs/router 0.0.0-experimental.83 → 0.0.0-experimental.8332dbe4
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 +112 -17
- package/dist/vite/index.js +1197 -454
- package/package.json +4 -2
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +30 -2
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +25 -0
- package/skills/layout/SKILL.md +2 -0
- package/skills/links/SKILL.md +234 -16
- package/skills/loader/SKILL.md +70 -3
- package/skills/middleware/SKILL.md +2 -0
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/migrate-react-router/SKILL.md +4 -0
- package/skills/parallel/SKILL.md +9 -0
- package/skills/rango/SKILL.md +2 -0
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +9 -1
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +44 -4
- package/src/browser/navigation-bridge.ts +113 -6
- package/src/browser/navigation-store.ts +25 -1
- package/src/browser/partial-update.ts +44 -10
- package/src/browser/prefetch/cache.ts +16 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/NavigationProvider.tsx +64 -16
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/use-params.ts +8 -5
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +8 -1
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/rsc-router.tsx +34 -6
- package/src/browser/types.ts +19 -0
- package/src/build/route-trie.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +5 -1
- package/src/href-client.ts +4 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/outlet-context.ts +1 -1
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +62 -39
- package/src/route-definition/dsl-helpers.ts +16 -3
- package/src/route-definition/helpers-types.ts +6 -1
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/router/handler-context.ts +21 -41
- package/src/router/lazy-includes.ts +1 -1
- package/src/router/loader-resolution.ts +3 -0
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +1 -0
- package/src/router/match-result.ts +21 -2
- package/src/router/middleware-types.ts +14 -25
- package/src/router/middleware.ts +54 -7
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/revalidation.ts +15 -1
- package/src/router/segment-resolution/fresh.ts +8 -0
- package/src/router/segment-resolution/revalidation.ts +128 -100
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +1 -2
- package/src/rsc/handler.ts +8 -4
- package/src/rsc/progressive-enhancement.ts +2 -0
- package/src/rsc/response-route-handler.ts +11 -10
- package/src/rsc/rsc-rendering.ts +3 -0
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +6 -0
- package/src/segment-system.tsx +60 -9
- package/src/server/request-context.ts +10 -42
- package/src/ssr/index.tsx +5 -1
- package/src/types/handler-context.ts +12 -39
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/segments.ts +17 -0
- package/src/urls/response-types.ts +2 -10
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +31 -3
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +48 -1
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/cjs-to-esm.ts +5 -0
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +16 -4
- package/src/vite/plugins/expose-action-id.ts +52 -28
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +516 -486
- package/src/vite/plugins/performance-tracks.ts +17 -9
- package/src/vite/plugins/use-cache-transform.ts +56 -43
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/rango.ts +49 -14
- package/src/vite/router-discovery.ts +498 -52
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.8332dbe4",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -135,14 +135,16 @@
|
|
|
135
135
|
"scripts": {
|
|
136
136
|
"build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
137
|
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit",
|
|
138
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit",
|
|
139
139
|
"test": "playwright test",
|
|
140
140
|
"test:ui": "playwright test --ui",
|
|
141
141
|
"test:unit": "vitest run",
|
|
142
142
|
"test:unit:watch": "vitest"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
+
"@types/debug": "^4.1.12",
|
|
145
146
|
"@vitejs/plugin-rsc": "^0.5.23",
|
|
147
|
+
"debug": "^4.4.1",
|
|
146
148
|
"magic-string": "^0.30.17",
|
|
147
149
|
"picomatch": "^4.0.3",
|
|
148
150
|
"rsc-html-stream": "^0.0.7"
|
|
@@ -141,9 +141,11 @@ path("/dashboard", (ctx) => {
|
|
|
141
141
|
breadcrumb({ label: "Dashboard", href: "/dashboard" });
|
|
142
142
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
143
143
|
});
|
|
144
|
+
```
|
|
144
145
|
|
|
146
|
+
```tsx
|
|
145
147
|
// Client component
|
|
146
|
-
|
|
148
|
+
"use client";
|
|
147
149
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
148
150
|
|
|
149
151
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
@@ -59,6 +59,8 @@ Now `ProductPage` carries its loader, loading state, and response-header middlew
|
|
|
59
59
|
| `intercept()` | `middleware`, `revalidate`, `loader`, `loading`, `errorBoundary`, `notFoundBoundary`, `layout`, `route`, `when`, `transition` |
|
|
60
60
|
| Response routes (`path.json()`, `path.text()`, …) | `middleware`, `cache` |
|
|
61
61
|
|
|
62
|
+
For per-item semantics see the dedicated skills: [middleware](../middleware/SKILL.md), [loader](../loader/SKILL.md), [parallel](../parallel/SKILL.md), [intercept](../intercept/SKILL.md), [layout](../layout/SKILL.md), [view-transitions](../view-transitions/SKILL.md).
|
|
63
|
+
|
|
62
64
|
If `handler.use()` returns a disallowed item for a mount site, registration throws:
|
|
63
65
|
|
|
64
66
|
```
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -298,9 +298,11 @@ path("/dashboard", (ctx) => {
|
|
|
298
298
|
push({ label: "Dashboard", href: "/dashboard" });
|
|
299
299
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
300
300
|
});
|
|
301
|
+
```
|
|
301
302
|
|
|
303
|
+
```tsx
|
|
302
304
|
// Client component — typeof infers the full Handle<T> type
|
|
303
|
-
|
|
305
|
+
"use client";
|
|
304
306
|
import { useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
305
307
|
|
|
306
308
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
@@ -321,6 +323,11 @@ RSC serialization strips the `collect` function via `toJSON()`. On the client,
|
|
|
321
323
|
|
|
322
324
|
## Action Hooks
|
|
323
325
|
|
|
326
|
+
For the full server-action guide (defining actions, `useActionState`,
|
|
327
|
+
`useOptimistic`, validation, revalidation, error handling, file uploads),
|
|
328
|
+
see `/server-actions`. `useAction()` below is a Rango-specific hook for
|
|
329
|
+
tracking actions called outside a `<form action={...}>` flow.
|
|
330
|
+
|
|
324
331
|
### useAction()
|
|
325
332
|
|
|
326
333
|
Track state of server action invocations:
|
|
@@ -687,7 +694,27 @@ function MountInfo() {
|
|
|
687
694
|
}
|
|
688
695
|
```
|
|
689
696
|
|
|
690
|
-
|
|
697
|
+
### useReverse(routes)
|
|
698
|
+
|
|
699
|
+
Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse(".name", params?)`. Auto-fills params from `useParams()`; explicit params override.
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
"use client";
|
|
703
|
+
import { Link, useReverse } from "@rangojs/router/client";
|
|
704
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
705
|
+
|
|
706
|
+
function BlogNav() {
|
|
707
|
+
const reverse = useReverse(blogRoutes);
|
|
708
|
+
return (
|
|
709
|
+
<nav>
|
|
710
|
+
<Link to={reverse(".index")}>Blog</Link>
|
|
711
|
+
<Link to={reverse(".post", { postId: "hello" })}>Post</Link>
|
|
712
|
+
</nav>
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
See `/links` for the full URL generation guide. `ctx.reverse()` is server-only; on the client, prefer `useReverse(routes)` for in-module names and pass URLs as props for cross-module ones.
|
|
691
718
|
|
|
692
719
|
## Hook Summary
|
|
693
720
|
|
|
@@ -698,6 +725,7 @@ See `/links` for full URL generation guide including server-side `ctx.reverse`.
|
|
|
698
725
|
| `useSearchParams()` | URL search params | `ReadonlyURLSearchParams` |
|
|
699
726
|
| `useHref()` | Mount-aware href | `(path) => string` |
|
|
700
727
|
| `useMount()` | Current include() mount path | `string` |
|
|
728
|
+
| `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
|
|
701
729
|
| `useNavigation()` | Reactive navigation state | state, location, isStreaming |
|
|
702
730
|
| `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
|
|
703
731
|
| `useSegments()` | URL path & segment IDs | path, segmentIds, location |
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n
|
|
3
|
+
description: Locale-aware routing with `include("/:locale?", ...)`, locale resolution chains, and react-intl integration
|
|
4
|
+
argument-hint: "[topic]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Internationalization (i18n) and Locale Routing
|
|
8
|
+
|
|
9
|
+
Rango doesn't ship an i18n module. The router gives you the URL primitives
|
|
10
|
+
(optional include prefixes, constraints, typed reverse) and you compose
|
|
11
|
+
them with whatever message library you use — `react-intl`, `lingui`,
|
|
12
|
+
`@formatjs/intl`, or hand-rolled.
|
|
13
|
+
|
|
14
|
+
This skill covers:
|
|
15
|
+
|
|
16
|
+
- Mounting routes under an optional locale prefix (`/`, `/en`, `/gb`)
|
|
17
|
+
- Constraining the prefix to a known locale set
|
|
18
|
+
- Resolving the active locale (URL → cookie → `Accept-Language` → default)
|
|
19
|
+
- Generating localized URLs via `reverse()` round-trip
|
|
20
|
+
- Wiring `react-intl` into an RSC route tree
|
|
21
|
+
|
|
22
|
+
## URL Shape: Optional Locale Prefix
|
|
23
|
+
|
|
24
|
+
Mount your localized routes under an optional include prefix so the
|
|
25
|
+
default locale lives at the bare URL and other locales get a prefix:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// urls.tsx
|
|
29
|
+
import { urls } from "@rangojs/router";
|
|
30
|
+
import { menuRoutes } from "./menu";
|
|
31
|
+
|
|
32
|
+
export const urlpatterns = urls(({ include }) => [
|
|
33
|
+
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
34
|
+
]);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
URLs that match:
|
|
38
|
+
|
|
39
|
+
| URL | Matched route | `ctx.params.locale` |
|
|
40
|
+
| -------------- | --------------- | ------------------- |
|
|
41
|
+
| `/` | `menu.index` | `undefined` |
|
|
42
|
+
| `/en` | `menu.index` | `"en"` |
|
|
43
|
+
| `/c/breads` | `menu.category` | `undefined` |
|
|
44
|
+
| `/en/c/breads` | `menu.category` | `"en"` |
|
|
45
|
+
|
|
46
|
+
> **Constrain to known locales** when you want unknown locales to fall
|
|
47
|
+
> through to other routes (or 404) instead of being treated as a slug:
|
|
48
|
+
>
|
|
49
|
+
> ```typescript
|
|
50
|
+
> include("/:locale(en|gb|fr)?", menuRoutes, { name: "menu" });
|
|
51
|
+
> ```
|
|
52
|
+
>
|
|
53
|
+
> `/de` now 404s (constraint rejects `de`), and `/c/breads` continues to
|
|
54
|
+
> match `menu.category` with `locale: undefined`. Without the constraint,
|
|
55
|
+
> `/de` would match `menu.index` with `locale: "de"`.
|
|
56
|
+
|
|
57
|
+
## Reading the Locale in Handlers
|
|
58
|
+
|
|
59
|
+
Absent optionals are `undefined` (not `""`), so `??` coalesces correctly:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Handler } from "@rangojs/router";
|
|
63
|
+
|
|
64
|
+
export const MenuIndex: Handler<"menu.index"> = (ctx) => {
|
|
65
|
+
// ctx.params.locale is `string | undefined`
|
|
66
|
+
const locale = resolveLocale(ctx);
|
|
67
|
+
return <Welcome locale={locale} />;
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `resolveLocale` helper below implements a typical fallback chain.
|
|
72
|
+
|
|
73
|
+
## Locale Resolution
|
|
74
|
+
|
|
75
|
+
URL is the strongest signal but you usually want a fallback chain:
|
|
76
|
+
|
|
77
|
+
1. **URL prefix** — if the user navigates to `/gb/...`, honor it
|
|
78
|
+
2. **Cookie** — sticky preference set by a previous language switcher
|
|
79
|
+
3. **`Accept-Language`** — browser hint
|
|
80
|
+
4. **Default** — your app default
|
|
81
|
+
|
|
82
|
+
Put it in a small helper that every locale-aware handler calls:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// lib/locale.ts
|
|
86
|
+
import { cookies, headers } from "@rangojs/router";
|
|
87
|
+
|
|
88
|
+
export const SUPPORTED_LOCALES = ["en", "gb", "fr"] as const;
|
|
89
|
+
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
90
|
+
const DEFAULT_LOCALE: Locale = "en";
|
|
91
|
+
|
|
92
|
+
const isSupported = (v: string): v is Locale =>
|
|
93
|
+
(SUPPORTED_LOCALES as readonly string[]).includes(v);
|
|
94
|
+
|
|
95
|
+
export function resolveLocale(ctx: {
|
|
96
|
+
params: Record<string, string | undefined>;
|
|
97
|
+
}): Locale {
|
|
98
|
+
const fromUrl = ctx.params.locale;
|
|
99
|
+
if (fromUrl && isSupported(fromUrl)) return fromUrl;
|
|
100
|
+
|
|
101
|
+
const fromCookie = cookies().get("locale")?.value;
|
|
102
|
+
if (fromCookie && isSupported(fromCookie)) return fromCookie;
|
|
103
|
+
|
|
104
|
+
const accept = headers().get("accept-language") ?? "";
|
|
105
|
+
for (const tag of accept.split(",")) {
|
|
106
|
+
const code = tag.split(";")[0].trim().split("-")[0];
|
|
107
|
+
if (isSupported(code)) return code as Locale;
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_LOCALE;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
If you want to redirect to the canonical URL when the resolved locale
|
|
114
|
+
doesn't match the URL (e.g., user has `gb` cookie but visits `/`), do
|
|
115
|
+
that in a global middleware so it covers actions too:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { redirect } from "@rangojs/router";
|
|
119
|
+
|
|
120
|
+
router.use("/*", async (ctx, next) => {
|
|
121
|
+
const fromUrl = ctx.params.locale;
|
|
122
|
+
const resolved = resolveLocale(ctx);
|
|
123
|
+
if (resolved !== DEFAULT_LOCALE && !fromUrl) {
|
|
124
|
+
return redirect(`/${resolved}${ctx.url.pathname}`);
|
|
125
|
+
}
|
|
126
|
+
await next();
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Generating Localized URLs
|
|
131
|
+
|
|
132
|
+
`reverse()` treats `undefined` and `""` for an optional param as "absent"
|
|
133
|
+
and collapses the segment cleanly. The round-trip is symmetric with the
|
|
134
|
+
matcher:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
ctx.reverse("menu.index", { locale: "" }); // → "/"
|
|
138
|
+
ctx.reverse("menu.index", { locale: undefined }); // → "/"
|
|
139
|
+
ctx.reverse("menu.index", { locale: "en" }); // → "/en"
|
|
140
|
+
ctx.reverse("menu.category", { locale: "en", slug: "breads" }); // → "/en/c/breads"
|
|
141
|
+
ctx.reverse("menu.category", { slug: "breads" }); // → "/c/breads"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If the active locale is the app default and your URL strategy hides it
|
|
145
|
+
(`"en"` → `/`, others → `/<locale>`), normalize before calling reverse:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const normalized = locale === DEFAULT_LOCALE ? undefined : locale;
|
|
149
|
+
const href = ctx.reverse("menu.category", { locale: normalized, slug });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## react-intl Integration
|
|
153
|
+
|
|
154
|
+
`react-intl` needs a `<IntlProvider>` wrapping the tree, with `locale`
|
|
155
|
+
and `messages` props. The cleanest split: load messages on the server
|
|
156
|
+
(handler or layout), pass them through to a client provider component.
|
|
157
|
+
|
|
158
|
+
### Messages loader
|
|
159
|
+
|
|
160
|
+
Load message bundles per locale. Keep them server-side so they stream
|
|
161
|
+
through the RSC payload and don't bloat the client bundle:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// lib/messages.ts
|
|
165
|
+
import type { Locale } from "./locale";
|
|
166
|
+
|
|
167
|
+
const loaders: Record<Locale, () => Promise<Record<string, string>>> = {
|
|
168
|
+
en: () => import("../messages/en.json").then((m) => m.default),
|
|
169
|
+
gb: () => import("../messages/gb.json").then((m) => m.default),
|
|
170
|
+
fr: () => import("../messages/fr.json").then((m) => m.default),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export async function loadMessages(locale: Locale) {
|
|
174
|
+
return loaders[locale]();
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Server layout: hand off to the client provider
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
// layouts/intl-layout.tsx (server component)
|
|
182
|
+
import type { ReactNode } from "react";
|
|
183
|
+
import { resolveLocale } from "../lib/locale";
|
|
184
|
+
import { loadMessages } from "../lib/messages";
|
|
185
|
+
import { IntlClientProvider } from "../components/intl-client-provider";
|
|
186
|
+
|
|
187
|
+
export async function IntlLayout({
|
|
188
|
+
ctx,
|
|
189
|
+
children,
|
|
190
|
+
}: {
|
|
191
|
+
ctx: any;
|
|
192
|
+
children: ReactNode;
|
|
193
|
+
}) {
|
|
194
|
+
const locale = resolveLocale(ctx);
|
|
195
|
+
const messages = await loadMessages(locale);
|
|
196
|
+
return (
|
|
197
|
+
<IntlClientProvider locale={locale} messages={messages}>
|
|
198
|
+
{children}
|
|
199
|
+
</IntlClientProvider>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Client provider
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// components/intl-client-provider.tsx
|
|
208
|
+
"use client";
|
|
209
|
+
|
|
210
|
+
import { IntlProvider } from "react-intl";
|
|
211
|
+
import type { ReactNode } from "react";
|
|
212
|
+
|
|
213
|
+
export function IntlClientProvider({
|
|
214
|
+
locale,
|
|
215
|
+
messages,
|
|
216
|
+
children,
|
|
217
|
+
}: {
|
|
218
|
+
locale: string;
|
|
219
|
+
messages: Record<string, string>;
|
|
220
|
+
children: ReactNode;
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<IntlProvider
|
|
224
|
+
locale={locale}
|
|
225
|
+
defaultLocale="en"
|
|
226
|
+
messages={messages}
|
|
227
|
+
onError={(err) => {
|
|
228
|
+
if (err.code === "MISSING_TRANSLATION") return; // common, log only
|
|
229
|
+
console.error(err);
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</IntlProvider>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Mounting
|
|
239
|
+
|
|
240
|
+
Wrap your localized routes with the layout:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { urls } from "@rangojs/router";
|
|
244
|
+
import { IntlLayout } from "./layouts/intl-layout";
|
|
245
|
+
import { menuRoutes } from "./menu";
|
|
246
|
+
|
|
247
|
+
export const urlpatterns = urls(({ layout, include }) => [
|
|
248
|
+
layout(IntlLayout, () => [
|
|
249
|
+
include("/:locale?", menuRoutes, { name: "menu" }),
|
|
250
|
+
]),
|
|
251
|
+
]);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`<FormattedMessage>`, `useIntl()`, etc. work in any client component
|
|
255
|
+
under the layout. Server components can use `formatjs`'s `createIntl()`
|
|
256
|
+
directly with the same `messages` map for static text.
|
|
257
|
+
|
|
258
|
+
## Common Pitfalls
|
|
259
|
+
|
|
260
|
+
| Pitfall | Fix |
|
|
261
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
262
|
+
| `ctx.params.locale === ""` returns `false` | Absent optionals are `undefined`, not `""`. Use `=== undefined` or `??`. |
|
|
263
|
+
| `ctx.params.locale ?? "en"` returns `""` | Pre-fix behavior. After the include-prefix fix this works correctly. |
|
|
264
|
+
| Bare `/` 404s when mounted via `include("/:locale?", routes)` | Requires the all-optional pattern fix in `compilePattern` (shipped). |
|
|
265
|
+
| Unknown locale (e.g. `/de`) matches as `locale: "de"` | Add a constraint: `:locale(en\|gb\|fr)?`. Unknown values now 404. |
|
|
266
|
+
| Reverse produces `//c/breads` for absent locale | `reverse()` collapses `undefined`/`""` segments — should not happen. File a bug. |
|
|
267
|
+
| Locale switcher loses search params | Read `ctx.url.search` and pass to `reverse(..., undefined, parsedSearch)`. |
|
|
268
|
+
| Action middleware can't read `ctx.params.locale` | Route middleware doesn't wrap action execution. Use global `router.use()` for actions. |
|
|
269
|
+
|
|
270
|
+
## Cross-references
|
|
271
|
+
|
|
272
|
+
- `/route` — optional URL param syntax and runtime contract
|
|
273
|
+
- `/typesafety` — `RouteParams<"name">` typing for optionals
|
|
274
|
+
- `/middleware` — global vs route middleware scope (matters for actions)
|
|
275
|
+
- `/server-actions` — actions and the global-vs-route middleware boundary
|
|
276
|
+
- `/links` — `ctx.reverse()` and locale-aware URL generation
|
|
@@ -197,6 +197,31 @@ function ModalWrapper({ children }) {
|
|
|
197
197
|
}
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
## Interaction with View Transitions
|
|
201
|
+
|
|
202
|
+
A layout that owns the `@modal` slot can also configure `transition()` for page
|
|
203
|
+
fades — opening a modal does **not** fire the layout's view transition. Rango
|
|
204
|
+
narrows the layout's `<ViewTransition>` wrap to the layout's default outlet
|
|
205
|
+
content, so `<ParallelOutlet />` (the slot where the modal mounts) is a sibling
|
|
206
|
+
of the wrap, not inside its subtree. Form actions submitted from inside an open
|
|
207
|
+
modal also commit without firing the underlying layout's transition, and the
|
|
208
|
+
modal subtree identity is preserved across revalidation (no remount,
|
|
209
|
+
`useActionState` survives). Closing the modal restores the page without a
|
|
210
|
+
stray transition.
|
|
211
|
+
|
|
212
|
+
For a modal-only morph (e.g. when intercepted URLs change while the modal
|
|
213
|
+
stays open), use an element-level React `<ViewTransition>` inside the modal
|
|
214
|
+
component — `transition()` accepted on `intercept()` via the DSL is not
|
|
215
|
+
applied to slot rendering today.
|
|
216
|
+
|
|
217
|
+
Caveat: route-level `transition()` wraps the route component itself, so a
|
|
218
|
+
`<ParallelOutlet />` rendered directly inside that route component would still
|
|
219
|
+
be inside the route's VT subtree. Mount the slot in a layout instead when you
|
|
220
|
+
combine intercept modals with route-level transitions.
|
|
221
|
+
|
|
222
|
+
See [skills/view-transitions](../view-transitions/SKILL.md) for the full
|
|
223
|
+
contract and direction-aware examples.
|
|
224
|
+
|
|
200
225
|
## Interaction with Prerender
|
|
201
226
|
|
|
202
227
|
When the target route of an intercept uses `Prerender`, the intercept handler is
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -118,6 +118,8 @@ function ShopLayout() {
|
|
|
118
118
|
}
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
A layout's `transition()` config wraps the content that flows through `<Outlet />` — not the layout chrome itself, and not sibling `<ParallelOutlet />` slots. Stacking transitions across nested layouts collapses around the deepest default outlet content. See [skills/view-transitions](../view-transitions/SKILL.md) for the full wrap rules and intercept-modal interaction.
|
|
122
|
+
|
|
121
123
|
## Named Outlets
|
|
122
124
|
|
|
123
125
|
For parallel routes, use named outlets:
|