@zerotal/arch 1.7.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
package/docs/i18n.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Internationalization
|
|
3
|
+
description: Resolve each visitor's locale automatically, then translate keys with interpolation, pluralization, and fallback.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Internationalization
|
|
7
|
+
|
|
8
|
+
Request-scoped localization: resolve each visitor's locale automatically, then
|
|
9
|
+
translate keys with interpolation, pluralization, and fallback — without threading
|
|
10
|
+
the locale through every function call.
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# in your project root
|
|
16
|
+
bun add @zerotal/i18n
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Register the provider
|
|
20
|
+
|
|
21
|
+
Add `I18nProvider` to the providers array in `bootstrap/providers.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// bootstrap/providers.ts
|
|
25
|
+
import { I18nProvider } from "@zerotal/i18n";
|
|
26
|
+
|
|
27
|
+
const providers = [
|
|
28
|
+
// …your other providers
|
|
29
|
+
I18nProvider,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export default providers;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Registering the provider switches on the following:
|
|
36
|
+
|
|
37
|
+
- `onRegister` — binds the translation service as the lazy `i18n` singleton (a
|
|
38
|
+
`Translator` loaded from your catalogs).
|
|
39
|
+
- `onBooting` — configures and registers `LocaleMiddleware` via `app.useOnce()`,
|
|
40
|
+
so every request resolves a locale and gains `ctx.locale` + `ctx.t()`.
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Create `config/i18n.ts`. The `I18nConfig()` helper fills in sensible defaults, so
|
|
45
|
+
you only set what differs:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// config/i18n.ts
|
|
49
|
+
import { I18nConfig } from "@zerotal/i18n";
|
|
50
|
+
import { env } from "zerotal";
|
|
51
|
+
|
|
52
|
+
export default I18nConfig({
|
|
53
|
+
defaultLocale: env("APP_LOCALE", "en"),
|
|
54
|
+
fallbackLocale: env("APP_FALLBACK_LOCALE", "en"),
|
|
55
|
+
supportedLocales: ["en"],
|
|
56
|
+
resolvers: ["query", "cookie", "accept-header"], // tried in order
|
|
57
|
+
queryKey: "lang", // ?lang=fr
|
|
58
|
+
cookieKey: "locale", // locale=fr cookie
|
|
59
|
+
loadPath: "resources/lang", // <locale>.json files (optional)
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Field | Required | Default | Description |
|
|
64
|
+
| ------------------ | -------- | ------------------------------ | ------------------------------------------------------------ |
|
|
65
|
+
| `defaultLocale` | no | `"en"` | Locale used when no resolver matches. |
|
|
66
|
+
| `fallbackLocale` | no | `defaultLocale` | Locale consulted when a key is missing in the active locale. |
|
|
67
|
+
| `supportedLocales` | no | `[defaultLocale]` | Locales resolvers may return; others fall back to default. |
|
|
68
|
+
| `resolvers` | no | `query, cookie, accept-header` | Request resolvers, tried in order (see below). |
|
|
69
|
+
| `queryKey` | no | `"lang"` | Query-string key for the `query` resolver. |
|
|
70
|
+
| `cookieKey` | no | `"locale"` | Cookie name for the `cookie` resolver. |
|
|
71
|
+
| `loadPath` | no | — | Directory of `<locale>.json` catalogs, loaded at boot. |
|
|
72
|
+
| `catalogs` | no | — | In-memory catalogs, merged over anything from `loadPath`. |
|
|
73
|
+
|
|
74
|
+
> **Tip** — `I18nConfig()` is optional. You can instead write a plain object with
|
|
75
|
+
> `satisfies I18nConfigShape` for the same type-checking — but then every field is
|
|
76
|
+
> required, since the helper is what supplies the defaults above.
|
|
77
|
+
|
|
78
|
+
Catalog files live under `loadPath`, one JSON file per locale:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
resources/lang/
|
|
82
|
+
├── en.json
|
|
83
|
+
├── fr.json
|
|
84
|
+
└── es.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Basic usage
|
|
88
|
+
|
|
89
|
+
`LocaleMiddleware` resolves the locale for every request and exposes `ctx.locale`
|
|
90
|
+
and `ctx.t()` on the request context:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// in a controller
|
|
94
|
+
async show(ctx: HttpContext) {
|
|
95
|
+
ctx.t("welcome.greeting", { name: "Alice" }); // active locale
|
|
96
|
+
ctx.t("welcome.greeting", { name: "Alice" }, "fr"); // explicit locale
|
|
97
|
+
return ctx.json({ locale: ctx.locale });
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Outside a controller — in a service, job, or view — use the `Lang` facade or the
|
|
102
|
+
global `t()` helper. Both read the active request locale from `I18nContext`, so no
|
|
103
|
+
locale needs to be passed around:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// anywhere in the request's async tree
|
|
107
|
+
import { Lang, t } from "@zerotal/i18n";
|
|
108
|
+
|
|
109
|
+
Lang.translate("auth.login.title");
|
|
110
|
+
t("dashboard.welcome");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Keys, interpolation, pluralization
|
|
114
|
+
|
|
115
|
+
Catalogs may use nested objects or flat dotted keys — both resolve:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
// resources/lang/en.json
|
|
119
|
+
{
|
|
120
|
+
"welcome": { "greeting": "Hello, {name}!" },
|
|
121
|
+
"validation.required": "The :field field is required.",
|
|
122
|
+
"apples": "no apples | one apple | {count} apples"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **Interpolation** — `{name}` and `:name` are both replaced from the
|
|
127
|
+
replacements object.
|
|
128
|
+
- **Pluralization** — pipe-separated segments are chosen by `count`: two segments
|
|
129
|
+
are `singular | plural` (`count === 1` → first); three or more are
|
|
130
|
+
`zero | one | many` (`0` → first, `1` → second, otherwise last).
|
|
131
|
+
- **Fallback** — a key missing in the active locale is looked up in
|
|
132
|
+
`fallbackLocale`; if still missing, the key itself is returned, so gaps are
|
|
133
|
+
visible and never throw.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
t("apples", { count: 0 }); // "no apples"
|
|
137
|
+
t("apples", { count: 5 }); // "5 apples"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Locale resolution
|
|
141
|
+
|
|
142
|
+
`resolveLocale(request, config)` tries each configured resolver in order and only
|
|
143
|
+
returns a value listed in `supportedLocales` (otherwise `defaultLocale`):
|
|
144
|
+
|
|
145
|
+
| Resolver | Source |
|
|
146
|
+
| --------------- | --------------------------------------------------------- |
|
|
147
|
+
| `query` | `?lang=fr` (key configurable via `queryKey`) |
|
|
148
|
+
| `cookie` | `locale=fr` cookie (name configurable via `cookieKey`) |
|
|
149
|
+
| `accept-header` | `Accept-Language`, by quality; `fr-FR` falls back to `fr` |
|
|
150
|
+
|
|
151
|
+
**Which resolvers should I list?** Order them most-explicit first:
|
|
152
|
+
|
|
153
|
+
- **`query`** — useful for a one-off preview link (`?lang=fr`) or language
|
|
154
|
+
switcher, but it doesn't persist. Put it first so it can override the others.
|
|
155
|
+
- **`cookie`** — the choice that _sticks_. List it when you let users pick a
|
|
156
|
+
language and persist it (see [Overriding the locale](#overriding-the-locale)).
|
|
157
|
+
- **`accept-header`** — the visitor's browser preference; a sensible default when
|
|
158
|
+
no explicit choice has been made. List it last as the fallback.
|
|
159
|
+
|
|
160
|
+
## Overriding the locale
|
|
161
|
+
|
|
162
|
+
The locale is resolved once, when `LocaleMiddleware` runs, and is then fixed for
|
|
163
|
+
the rest of the request — so `ctx.locale` and `ctx.t()` always reflect what the
|
|
164
|
+
resolvers chose. There is no `setLocale()`. To change the language, do one of two
|
|
165
|
+
things.
|
|
166
|
+
|
|
167
|
+
**Persist a user's choice** by writing the `locale` cookie; the `cookie` resolver
|
|
168
|
+
picks it up on every subsequent request:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// in a controller — save the visitor's language choice
|
|
172
|
+
async setLocale(ctx: HttpContext) {
|
|
173
|
+
const { locale } = await ctx.body<{ locale: string }>();
|
|
174
|
+
|
|
175
|
+
const headers = new Headers({ Location: ctx.header("Referer") ?? "/" });
|
|
176
|
+
headers.append(
|
|
177
|
+
"Set-Cookie",
|
|
178
|
+
`locale=${locale}; Path=/; Max-Age=${365 * 86400}; SameSite=Lax`,
|
|
179
|
+
);
|
|
180
|
+
ctx.response = new Response(null, { status: 303, headers });
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Override within the current request** — e.g. to honour a locale stored on the
|
|
185
|
+
user — by running code inside `I18nContext.run()`. The `Lang` facade and `t()`
|
|
186
|
+
helper use the supplied locale for the duration of the callback:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// in a controller
|
|
190
|
+
import { I18nContext, t } from "@zerotal/i18n";
|
|
191
|
+
|
|
192
|
+
async show(ctx: HttpContext) {
|
|
193
|
+
const user = ctx.user as { name?: string; locale?: string } | undefined;
|
|
194
|
+
const locale = user?.locale ?? ctx.locale;
|
|
195
|
+
|
|
196
|
+
const greeting = I18nContext.run(locale, () =>
|
|
197
|
+
t("welcome.greeting", { name: user?.name }),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return ctx.view(DashboardPage({ greeting }));
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
> **Note** — `I18nContext.run()` only affects the `Lang` facade and `t()` inside
|
|
205
|
+
> its callback. `ctx.t()` and `ctx.locale` were bound by the middleware and stay on
|
|
206
|
+
> the request's resolved locale. For a persistent change, set the cookie above.
|
|
207
|
+
|
|
208
|
+
## Catalog structure
|
|
209
|
+
|
|
210
|
+
Catalogs can be flat or deeply nested — both styles resolve with the same dot-path:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
// resources/lang/en.json
|
|
214
|
+
{
|
|
215
|
+
"auth": {
|
|
216
|
+
"login": {
|
|
217
|
+
"title": "Sign in",
|
|
218
|
+
"submit": "Sign in to your account",
|
|
219
|
+
"forgot": "Forgot your password?"
|
|
220
|
+
},
|
|
221
|
+
"logout": "Sign out"
|
|
222
|
+
},
|
|
223
|
+
"validation": {
|
|
224
|
+
"required": "The :field field is required.",
|
|
225
|
+
"email": "The :field must be a valid email address.",
|
|
226
|
+
"min": "The :field must be at least :min characters."
|
|
227
|
+
},
|
|
228
|
+
"posts": {
|
|
229
|
+
"count": "no posts | one post | :count posts",
|
|
230
|
+
"created": "Post created on :date"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
ctx.t("auth.login.title"); // "Sign in"
|
|
237
|
+
ctx.t("auth.logout"); // "Sign out"
|
|
238
|
+
ctx.t("validation.required", { field: "name" }); // "The name field is required."
|
|
239
|
+
ctx.t("posts.count", { count: 0 }); // "no posts"
|
|
240
|
+
ctx.t("posts.count", { count: 1 }); // "one post"
|
|
241
|
+
ctx.t("posts.count", { count: 42 }); // "42 posts"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Translating validation messages
|
|
245
|
+
|
|
246
|
+
A common pattern is to keep your localized form-error strings under a `validation`
|
|
247
|
+
namespace in each catalog, then translate them with `t()` where you build the
|
|
248
|
+
error response:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
// resources/lang/fr.json
|
|
252
|
+
{
|
|
253
|
+
"validation": {
|
|
254
|
+
"required": "Le champ :field est obligatoire.",
|
|
255
|
+
"email": "Le champ :field doit être une adresse e-mail valide.",
|
|
256
|
+
"min": "Le champ :field doit contenir au moins :min caractères.",
|
|
257
|
+
"unique": "Cette valeur est déjà prise."
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// in a controller — translate a validation message yourself
|
|
264
|
+
t("validation.required", { field: "email" }); // active locale
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> **Note** — The `validation` namespace is just a convention for organizing keys;
|
|
268
|
+
> the i18n package translates any key you pass to `t()`. See
|
|
269
|
+
> [Validator](/docs/validator) for how the validator itself reports errors.
|
|
270
|
+
|
|
271
|
+
## Multiple supported locales
|
|
272
|
+
|
|
273
|
+
List every locale your app ships in `supportedLocales`. The resolver only returns a
|
|
274
|
+
locale from this list — unsupported values fall back to `defaultLocale`:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// config/i18n.ts
|
|
278
|
+
export default I18nConfig({
|
|
279
|
+
defaultLocale: "en",
|
|
280
|
+
fallbackLocale: "en",
|
|
281
|
+
supportedLocales: ["en", "fr", "es", "zu", "af"],
|
|
282
|
+
resolvers: ["cookie", "accept-header"],
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Add a catalog file for each:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
resources/lang/
|
|
290
|
+
en.json
|
|
291
|
+
fr.json
|
|
292
|
+
es.json
|
|
293
|
+
zu.json ← isiZulu
|
|
294
|
+
af.json ← Afrikaans
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Using t in JSX views
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// app/views/PostCard.tsx
|
|
301
|
+
import { t } from "@zerotal/i18n";
|
|
302
|
+
|
|
303
|
+
export function PostCard({ post }: { post: Post }) {
|
|
304
|
+
return (
|
|
305
|
+
<div>
|
|
306
|
+
<h2>{post.title}</h2>
|
|
307
|
+
<p>{t("posts.count", { count: post.commentCount })}</p>
|
|
308
|
+
<a href={`/posts/${post.slug}`}>{t("posts.readMore")}</a>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`t()` reads the active locale from `I18nContext` (async local storage) — no props
|
|
315
|
+
threading needed.
|
|
316
|
+
|
|
317
|
+
## Errors
|
|
318
|
+
|
|
319
|
+
`I18nError` (`E_I18N`) is the base; `CatalogLoadError` (`E_I18N_CATALOG_LOAD`) is
|
|
320
|
+
thrown when a catalog file exists but contains malformed JSON. Both extend
|
|
321
|
+
`ZerotalError`.
|
|
322
|
+
|
|
323
|
+
## Testing
|
|
324
|
+
|
|
325
|
+
Set your suite up once as described in [Testing](/docs/testing). Translation
|
|
326
|
+
tests are cheap, and the two worth writing are the ones that catch a missing
|
|
327
|
+
string before a user does.
|
|
328
|
+
|
|
329
|
+
**Assert the resolved string, not the key.** A test that checks `t("cart.empty")
|
|
330
|
+
=== "cart.empty"` passes when the catalogue is missing entirely:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// tests/i18n/catalogues.test.ts
|
|
334
|
+
import { test, expect } from "bun:test";
|
|
335
|
+
import { t } from "@zerotal/i18n";
|
|
336
|
+
|
|
337
|
+
test("renders the French cart message", () => {
|
|
338
|
+
expect(t("cart.empty", {}, "fr")).toBe("Votre panier est vide");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("substitutes replacements", () => {
|
|
342
|
+
expect(t("cart.count", { n: 3 }, "fr")).toContain("3");
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Test that every locale has every key.** This is the test that earns its place —
|
|
347
|
+
it fails the moment someone adds an English string and forgets the translation,
|
|
348
|
+
which is otherwise found in production by a French speaker:
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// tests/i18n/parity.test.ts
|
|
352
|
+
import { test, expect } from "bun:test";
|
|
353
|
+
import { loadCatalogs } from "@zerotal/i18n";
|
|
354
|
+
|
|
355
|
+
test("every locale defines the same keys as English", async () => {
|
|
356
|
+
const catalogs = await loadCatalogs("./resources/lang");
|
|
357
|
+
const english = Object.keys(catalogs.en).sort();
|
|
358
|
+
|
|
359
|
+
for (const [locale, messages] of Object.entries(catalogs)) {
|
|
360
|
+
expect(Object.keys(messages).sort(), `locale: ${locale}`).toEqual(english);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Locale resolution is a separate concern** from translation, and fails
|
|
366
|
+
separately — a correct catalogue served under the wrong locale looks like a
|
|
367
|
+
missing translation:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// tests/http/locale.test.ts
|
|
371
|
+
const res = await app.get("/", { "Accept-Language": "fr-CA,fr;q=0.9" });
|
|
372
|
+
|
|
373
|
+
res.assertSee("Votre panier est vide");
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
> **Note** — `resolveLocale` and `parseAcceptLanguage` are exported and pure, so
|
|
377
|
+
> a header you are unsure about can be checked directly rather than through a
|
|
378
|
+
> request.
|
|
379
|
+
|
|
380
|
+
## References
|
|
381
|
+
|
|
382
|
+
**Request context** — added by `LocaleMiddleware`:
|
|
383
|
+
|
|
384
|
+
| Member | Signature | Description |
|
|
385
|
+
| ------------ | ---------------------------------------------------------------------- | ------------------------------------- |
|
|
386
|
+
| `ctx.locale` | `string` | The locale resolved for this request. |
|
|
387
|
+
| `ctx.t()` | `t(key: string, replacements?: Replacements, locale?: string): string` | Translate using the request locale. |
|
|
388
|
+
|
|
389
|
+
**`Lang` facade** — the `i18n` binding (a `Translator`):
|
|
390
|
+
|
|
391
|
+
| Method | Signature | Description |
|
|
392
|
+
| ----------------- | --------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
393
|
+
| `Lang.translate` | `(key: string, replacements?: Replacements, locale?: string): string` | Translate a key; a missing key returns the key itself. |
|
|
394
|
+
| `Lang.has` | `(key: string, locale?: string): boolean` | Whether the key exists in the active or fallback locale. |
|
|
395
|
+
| `Lang.locales` | `string[]` | Loaded locales. |
|
|
396
|
+
| `Lang.addCatalog` | `(locale: string, messages: Messages): void` | Merge messages into a locale (tooling / tests). |
|
|
397
|
+
|
|
398
|
+
**Helpers & context** — importable from `@zerotal/i18n`:
|
|
399
|
+
|
|
400
|
+
| Export | Signature | Description |
|
|
401
|
+
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------ |
|
|
402
|
+
| `t` | `(key: string, replacements?: Replacements, locale?: string): string` | Global translate; reads the active locale. |
|
|
403
|
+
| `I18nContext.run` | `<T>(locale: string, cb: () => T): T` | Run `cb` with `locale` active. |
|
|
404
|
+
| `I18nContext.current` | `(): string \| undefined` | Active locale, or `undefined` outside a request. |
|
|
405
|
+
| `resolveLocale` | `(request: Request, config: I18nConfigShape): string` | Resolve a request's locale per config. |
|
|
406
|
+
|
|
407
|
+
See [Configuration](#configuration) for the `config/i18n.ts` fields.
|
|
408
|
+
|
|
409
|
+
## Next steps
|
|
410
|
+
|
|
411
|
+
- [Validator](/docs/validator) — pair form validation with a translated `validation` namespace.
|
|
412
|
+
- [Middleware](/docs/middleware) — how `LocaleMiddleware` resolves the request locale.
|
|
413
|
+
- [Cookies](/docs/cookies) — persist a visitor's locale choice.
|
|
414
|
+
- [View](/docs/view) — use `t()` inside server-rendered JSX.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Documentation
|
|
3
|
+
description: Start here — installation, the ideas behind Zerotal, and a map of every guide in the documentation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Zerotal Documentation
|
|
7
|
+
|
|
8
|
+
Zerotal is a Bun-native, full-stack TypeScript framework. You write plain classes,
|
|
9
|
+
drop them in convention-named folders, and the framework wires them together at
|
|
10
|
+
boot — no build step, no manual registration.
|
|
11
|
+
|
|
12
|
+
## New here?
|
|
13
|
+
|
|
14
|
+
Three pages, in order, and you'll be productive:
|
|
15
|
+
|
|
16
|
+
1. **[Getting Started](/docs/getting-started)** — scaffold a project, run it, add a
|
|
17
|
+
route, a model, and a [service provider](/docs/providers). Fifteen minutes.
|
|
18
|
+
2. **[About Zerotal](/docs/about)** — the whole framework on one screen: the four
|
|
19
|
+
ideas it rests on, and a runnable example of every major feature.
|
|
20
|
+
3. **[Conventions](/docs/conventions)** — what gets auto-discovered, and the naming
|
|
21
|
+
rules that make it happen. This is the page that stops the surprises.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# in your project's parent directory
|
|
25
|
+
bun create zerotal my-app
|
|
26
|
+
cd my-app && bun dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Find your way around
|
|
30
|
+
|
|
31
|
+
| If you want to… | Read |
|
|
32
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| Handle a request | [Routing](/docs/routing) · [Controllers](/docs/controllers) · [Middleware](/docs/middleware) · [Requests Context](/docs/context) |
|
|
34
|
+
| Talk to a database | [Database](/docs/database) · [ORM](/docs/orm) · [Migrations](/docs/migrations) · [Query Builder](/docs/query-builder) |
|
|
35
|
+
| Build a UI | [Views](/docs/view) · [Flow](/docs/flow) · [Inertia](/docs/inertia) · [Assets](/docs/assets) |
|
|
36
|
+
| Sign users in | [Authentication](/docs/authentication) · [Authorization](/docs/authorization) · [Roles & 2FA](/docs/roles-and-2fa) · [Social Login](/docs/social) |
|
|
37
|
+
| Do work in the background | [Queue](/docs/queue) · [Scheduler](/docs/scheduler) · [Broadcasting](/docs/broadcasting) |
|
|
38
|
+
| Understand how the framework boots | [The Application](/docs/application) · [Lifecycle](/docs/lifecycle) · [Container](/docs/container) · [Providers](/docs/providers) |
|
|
39
|
+
| Test what you built | [Testing](/docs/testing) · [HTTP Tests](/docs/testing/http) · [Database Tests](/docs/testing/database) |
|
|
40
|
+
| See what's running in production | [Logger](/docs/logger) · [Monitor](/docs/monitor) · [Telemetry](/docs/telemetry) · [Health](/docs/health) |
|
|
41
|
+
| Debug what it just did | [DevTools](/docs/devtools) · [Errors](/docs/errors) |
|
|
42
|
+
| Work with a coding agent | [Agent Surface](/docs/arch) |
|
|
43
|
+
| Ship it | [Deployment](/docs/deployment) · [Commands](/docs/commands) |
|
|
44
|
+
|
|
45
|
+
## Choosing a frontend
|
|
46
|
+
|
|
47
|
+
Zerotal doesn't pick for you, and the choice is per project rather than per
|
|
48
|
+
framework:
|
|
49
|
+
|
|
50
|
+
- **[Views](/docs/view)** — server-rendered JSX. No client runtime at all. Right for
|
|
51
|
+
content sites, forms, and anything that doesn't need live updates.
|
|
52
|
+
- **[Flow](/docs/flow)** — reactive components rendered on the server, updated
|
|
53
|
+
over a WebSocket. You write TypeScript classes, not client state. Right when you
|
|
54
|
+
want interactivity without a separate frontend codebase.
|
|
55
|
+
- **[Inertia](/docs/inertia)** — a React or Vue SPA that talks to your controllers
|
|
56
|
+
directly, with no API layer to maintain. Right when you already know React or Vue
|
|
57
|
+
and want the full client-side experience.
|
|
58
|
+
|
|
59
|
+
## Reference
|
|
60
|
+
|
|
61
|
+
- **[API Reference](/docs/api)** — generated from source, every exported symbol.
|
|
62
|
+
- **[Commands](/docs/commands)** — every `bun zt` command.
|
|
63
|
+
- **[Release Notes](/docs/changelog)** · **[Upgrade Guide](/docs/upgrade)**
|
|
64
|
+
- **[Package Development](/docs/package-development)** — build your own `@zerotal`-style package.
|
|
65
|
+
- **[Contribution Guide](/docs/contributing)** · **[Inspirations](/docs/inspirations)**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Inertia CLI & Build
|
|
3
|
+
description: The page registry, the bundler pipeline, and building for production.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CLI & build
|
|
7
|
+
|
|
8
|
+
`@zerotal/inertia` ships two commands — one to scaffold page components, one to
|
|
9
|
+
bundle them — plus the page registry that ties component names to files.
|
|
10
|
+
|
|
11
|
+
## Generating a page
|
|
12
|
+
|
|
13
|
+
Scaffold a new page component under your pages directory (`resources/js/pages/` by default):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# in your project root
|
|
17
|
+
bun zt make:page Dashboard
|
|
18
|
+
bun zt make:page Users/Index # nested — creates resources/js/pages/Users/Index.tsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The generated stub wires up `usePage()` for typed access to shared props and a couple
|
|
22
|
+
of starter links. The framework (React `.tsx` or Vue `.vue`) is auto-detected from
|
|
23
|
+
the Inertia adapter you installed; force it with `--framework vue` or `--framework react`.
|
|
24
|
+
After writing the file, `make:page` regenerates the [page registry](#page-registry)
|
|
25
|
+
automatically.
|
|
26
|
+
|
|
27
|
+
### Persistent layouts — --layout
|
|
28
|
+
|
|
29
|
+
Pass `--layout` to wrap the page in an Inertia persistent layout (the layout mounts
|
|
30
|
+
once and survives client-side navigations; only the page content re-renders):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# in your project root
|
|
34
|
+
bun zt make:page Settings --layout MainLayout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This emits a page that assigns a `.layout` function and imports the layout from a
|
|
38
|
+
`layouts/` directory relative to the page. Adjust the import path if your layouts live
|
|
39
|
+
elsewhere — the generator prints a reminder.
|
|
40
|
+
|
|
41
|
+
| Argument / flag | Description |
|
|
42
|
+
| --------------- | ------------------------------------------------------------------ |
|
|
43
|
+
| `name` | Page name, e.g. `Dashboard` or `Users/Index` (required). |
|
|
44
|
+
| `--layout` | Wrap the page in a persistent layout component. |
|
|
45
|
+
| `--framework` | Force `vue` or `react` (auto-detected from the installed adapter). |
|
|
46
|
+
|
|
47
|
+
## Building assets
|
|
48
|
+
|
|
49
|
+
Bundle the frontend with `Bun.build` and regenerate the page registry:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# in your project root
|
|
53
|
+
bun zt inertia:build # development build (external source maps)
|
|
54
|
+
bun zt inertia:build -p # production build (minified, no source maps)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The build:
|
|
58
|
+
|
|
59
|
+
- entrypoint `resources/js/app.tsx` → output `public/assets/`,
|
|
60
|
+
- targets the browser with **code splitting on** (each page becomes its own chunk
|
|
61
|
+
from the registry's dynamic imports, so navigation only loads what it needs),
|
|
62
|
+
- auto-detects and applies CSS (and Vue) plugins present in your project,
|
|
63
|
+
- prints a table of output files and sizes.
|
|
64
|
+
|
|
65
|
+
| Flag | Effect |
|
|
66
|
+
| -------------------- | --------------------------------------------------- |
|
|
67
|
+
| `-p`, `--production` | Minify and drop source maps for a production build. |
|
|
68
|
+
|
|
69
|
+
Pair `inertia:build -p` with a hashed [asset version](/docs/inertia/middleware#asset-versioning)
|
|
70
|
+
in your deploy so clients reload onto the new bundle.
|
|
71
|
+
|
|
72
|
+
## Page registry
|
|
73
|
+
|
|
74
|
+
The page registry is a generated module (`resources/js/pages.generated.ts`) mapping each
|
|
75
|
+
component name (e.g. `"Users/Index"`) to a dynamic `import()` of its file. It's what lets
|
|
76
|
+
the client resolve `inertia("Users/Index")` to the right chunk, and what enables per-page
|
|
77
|
+
code splitting.
|
|
78
|
+
|
|
79
|
+
You rarely touch it directly — both `make:page` and `inertia:build` regenerate it.
|
|
80
|
+
To regenerate programmatically:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// in a build script
|
|
84
|
+
import { generatePageRegistry } from "@zerotal/inertia";
|
|
85
|
+
|
|
86
|
+
await generatePageRegistry(process.cwd());
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run it whenever you add or remove page files outside the generators (e.g. in a custom
|
|
90
|
+
build script) so the registry stays in sync with your pages directory.
|
|
91
|
+
|
|
92
|
+
## Next steps
|
|
93
|
+
|
|
94
|
+
- [Inertia overview](/docs/inertia) — the guide's front page and the rest of the sections.
|
|
95
|
+
- [Reference](/docs/inertia/references) — the full API surface in one table.
|