@mandujs/core 0.53.3 → 0.54.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/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 18.μ — message registry + typed `t()` helper.
|
|
3
|
-
*
|
|
4
|
-
* `defineMessages()` creates a locale-keyed message bundle with its
|
|
5
|
-
* keys inferred at compile time (via `as const`). `createTranslator()`
|
|
6
|
-
* returns a `t(key, vars)` function bound to a specific active
|
|
7
|
-
* locale — misses fall back to `fallbackLocale` then raw `key`.
|
|
8
|
-
*
|
|
9
|
-
* Placeholder syntax is Next.js / react-intl-lite compatible:
|
|
10
|
-
* `"Hello, {{name}}!"` resolves `{ name: "Mandu" }` → `"Hello, Mandu!"`.
|
|
11
|
-
* Missing vars are left as `{{name}}` so template errors are visible
|
|
12
|
-
* in the rendered page instead of silently producing empty strings.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { LocaleCode, MessageBundle, Translator } from "./types";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Brand `defineMessages()` output so downstream helpers can verify
|
|
19
|
-
* the caller actually went through the declared API. Runtime-only
|
|
20
|
-
* field; TS infers it without the caller writing it.
|
|
21
|
-
*/
|
|
22
|
-
export interface MessageRegistry<TKeys extends string = string> {
|
|
23
|
-
readonly __manduMessages: true;
|
|
24
|
-
readonly bundles: MessageBundle<TKeys>;
|
|
25
|
-
readonly locales: readonly LocaleCode[];
|
|
26
|
-
/**
|
|
27
|
-
* Look up a single message. Returns `undefined` when neither
|
|
28
|
-
* the requested locale nor the registry's configured fallbacks
|
|
29
|
-
* carry the key. The returned value is the raw template, NOT
|
|
30
|
-
* interpolated — use `createTranslator()` for the full pipeline.
|
|
31
|
-
*/
|
|
32
|
-
lookup(locale: LocaleCode, key: TKeys, fallback?: LocaleCode): string | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Create a message registry. The generic `TBundle` is inferred from
|
|
37
|
-
* the argument when passed `as const`, giving `t()` compile-time
|
|
38
|
-
* key checks.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```ts
|
|
42
|
-
* const messages = defineMessages({
|
|
43
|
-
* en: { welcome: "Welcome, {{name}}!" },
|
|
44
|
-
* ko: { welcome: "환영합니다, {{name}}님!" },
|
|
45
|
-
* } as const);
|
|
46
|
-
* ```
|
|
47
|
-
*/
|
|
48
|
-
export function defineMessages<
|
|
49
|
-
TBundle extends Record<string, Record<string, string>>,
|
|
50
|
-
>(bundles: TBundle): MessageRegistry<
|
|
51
|
-
Extract<keyof TBundle[keyof TBundle], string>
|
|
52
|
-
> {
|
|
53
|
-
if (!bundles || typeof bundles !== "object") {
|
|
54
|
-
throw new Error("[mandu/i18n] defineMessages() requires an object");
|
|
55
|
-
}
|
|
56
|
-
const locales = Object.keys(bundles);
|
|
57
|
-
if (locales.length === 0) {
|
|
58
|
-
throw new Error("[mandu/i18n] defineMessages() requires at least one locale");
|
|
59
|
-
}
|
|
60
|
-
for (const [locale, bundle] of Object.entries(bundles)) {
|
|
61
|
-
if (!bundle || typeof bundle !== "object") {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`[mandu/i18n] defineMessages(): bundle for "${locale}" must be an object`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
for (const [key, value] of Object.entries(bundle)) {
|
|
67
|
-
if (typeof value !== "string") {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`[mandu/i18n] defineMessages(): "${locale}.${key}" must be a string`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
type TKey = Extract<keyof TBundle[keyof TBundle], string>;
|
|
76
|
-
|
|
77
|
-
const registry: MessageRegistry<TKey> = {
|
|
78
|
-
__manduMessages: true,
|
|
79
|
-
bundles: bundles as unknown as MessageBundle<TKey>,
|
|
80
|
-
locales,
|
|
81
|
-
lookup(locale: LocaleCode, key: TKey, fallback?: LocaleCode): string | undefined {
|
|
82
|
-
const primary = (bundles as Record<string, Record<string, string>>)[locale]?.[key];
|
|
83
|
-
if (typeof primary === "string") return primary;
|
|
84
|
-
if (fallback && fallback !== locale) {
|
|
85
|
-
const alt = (bundles as Record<string, Record<string, string>>)[fallback]?.[key];
|
|
86
|
-
if (typeof alt === "string") return alt;
|
|
87
|
-
}
|
|
88
|
-
return undefined;
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
return registry;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Replace `{{var}}` placeholders in `template` using `vars`. Missing
|
|
96
|
-
* vars are preserved as `{{var}}` so templating bugs are visible in
|
|
97
|
-
* the rendered output (not silently stripped). `vars[...]` values
|
|
98
|
-
* that are numbers are coerced via `String(value)`.
|
|
99
|
-
*
|
|
100
|
-
* Whitespace inside braces is tolerated: `{{ name }}` and `{{name}}`
|
|
101
|
-
* both resolve to `vars.name`.
|
|
102
|
-
*/
|
|
103
|
-
export function interpolate(
|
|
104
|
-
template: string,
|
|
105
|
-
vars: Record<string, string | number> | undefined
|
|
106
|
-
): string {
|
|
107
|
-
if (!vars) return template;
|
|
108
|
-
return template.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (match, key) => {
|
|
109
|
-
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
110
|
-
const value = vars[key as keyof typeof vars];
|
|
111
|
-
if (value === undefined || value === null) return match;
|
|
112
|
-
return String(value);
|
|
113
|
-
}
|
|
114
|
-
return match;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Build a typed `t()` function bound to a specific active locale.
|
|
120
|
-
* Misses walk `activeLocale → fallbackLocale → defaultLocale → key`
|
|
121
|
-
* (skipping duplicates). The final fallback to raw `key` makes
|
|
122
|
-
* missing-translation bugs highly visible during QA without
|
|
123
|
-
* crashing the page.
|
|
124
|
-
*
|
|
125
|
-
* @example
|
|
126
|
-
* ```ts
|
|
127
|
-
* const messages = defineMessages({
|
|
128
|
-
* en: { greet: "Hi, {{name}}" },
|
|
129
|
-
* ko: { greet: "안녕, {{name}}" },
|
|
130
|
-
* } as const);
|
|
131
|
-
*
|
|
132
|
-
* const t = createTranslator(messages, { activeLocale: "ko", defaultLocale: "en" });
|
|
133
|
-
* t("greet", { name: "만두" }); // → "안녕, 만두"
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
export function createTranslator<TKeys extends string>(
|
|
137
|
-
registry: MessageRegistry<TKeys>,
|
|
138
|
-
opts: {
|
|
139
|
-
activeLocale: LocaleCode;
|
|
140
|
-
defaultLocale: LocaleCode;
|
|
141
|
-
fallbackLocale?: LocaleCode;
|
|
142
|
-
}
|
|
143
|
-
): Translator<TKeys> {
|
|
144
|
-
const { activeLocale, defaultLocale, fallbackLocale } = opts;
|
|
145
|
-
return function t(key, vars) {
|
|
146
|
-
const candidates: LocaleCode[] = [activeLocale];
|
|
147
|
-
if (fallbackLocale && !candidates.includes(fallbackLocale)) candidates.push(fallbackLocale);
|
|
148
|
-
if (!candidates.includes(defaultLocale)) candidates.push(defaultLocale);
|
|
149
|
-
|
|
150
|
-
for (const locale of candidates) {
|
|
151
|
-
const value = registry.lookup(locale, key as TKeys);
|
|
152
|
-
if (typeof value === "string") {
|
|
153
|
-
return interpolate(value, vars);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// Final fallback — raw key preserves "unresolved" signal instead
|
|
157
|
-
// of a blank string. `vars` is intentionally ignored here because
|
|
158
|
-
// the key itself isn't a template.
|
|
159
|
-
return key as unknown as string;
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Type guard for {@link MessageRegistry}. Useful when the runtime
|
|
165
|
-
* receives `unknown` from user config (e.g. dynamic imports).
|
|
166
|
-
*/
|
|
167
|
-
export function isMessageRegistry(value: unknown): value is MessageRegistry {
|
|
168
|
-
return (
|
|
169
|
-
typeof value === "object" &&
|
|
170
|
-
value !== null &&
|
|
171
|
-
(value as { __manduMessages?: unknown }).__manduMessages === true
|
|
172
|
-
);
|
|
173
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.μ — message registry + typed `t()` helper.
|
|
3
|
+
*
|
|
4
|
+
* `defineMessages()` creates a locale-keyed message bundle with its
|
|
5
|
+
* keys inferred at compile time (via `as const`). `createTranslator()`
|
|
6
|
+
* returns a `t(key, vars)` function bound to a specific active
|
|
7
|
+
* locale — misses fall back to `fallbackLocale` then raw `key`.
|
|
8
|
+
*
|
|
9
|
+
* Placeholder syntax is Next.js / react-intl-lite compatible:
|
|
10
|
+
* `"Hello, {{name}}!"` resolves `{ name: "Mandu" }` → `"Hello, Mandu!"`.
|
|
11
|
+
* Missing vars are left as `{{name}}` so template errors are visible
|
|
12
|
+
* in the rendered page instead of silently producing empty strings.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { LocaleCode, MessageBundle, Translator } from "./types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Brand `defineMessages()` output so downstream helpers can verify
|
|
19
|
+
* the caller actually went through the declared API. Runtime-only
|
|
20
|
+
* field; TS infers it without the caller writing it.
|
|
21
|
+
*/
|
|
22
|
+
export interface MessageRegistry<TKeys extends string = string> {
|
|
23
|
+
readonly __manduMessages: true;
|
|
24
|
+
readonly bundles: MessageBundle<TKeys>;
|
|
25
|
+
readonly locales: readonly LocaleCode[];
|
|
26
|
+
/**
|
|
27
|
+
* Look up a single message. Returns `undefined` when neither
|
|
28
|
+
* the requested locale nor the registry's configured fallbacks
|
|
29
|
+
* carry the key. The returned value is the raw template, NOT
|
|
30
|
+
* interpolated — use `createTranslator()` for the full pipeline.
|
|
31
|
+
*/
|
|
32
|
+
lookup(locale: LocaleCode, key: TKeys, fallback?: LocaleCode): string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a message registry. The generic `TBundle` is inferred from
|
|
37
|
+
* the argument when passed `as const`, giving `t()` compile-time
|
|
38
|
+
* key checks.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const messages = defineMessages({
|
|
43
|
+
* en: { welcome: "Welcome, {{name}}!" },
|
|
44
|
+
* ko: { welcome: "환영합니다, {{name}}님!" },
|
|
45
|
+
* } as const);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function defineMessages<
|
|
49
|
+
TBundle extends Record<string, Record<string, string>>,
|
|
50
|
+
>(bundles: TBundle): MessageRegistry<
|
|
51
|
+
Extract<keyof TBundle[keyof TBundle], string>
|
|
52
|
+
> {
|
|
53
|
+
if (!bundles || typeof bundles !== "object") {
|
|
54
|
+
throw new Error("[mandu/i18n] defineMessages() requires an object");
|
|
55
|
+
}
|
|
56
|
+
const locales = Object.keys(bundles);
|
|
57
|
+
if (locales.length === 0) {
|
|
58
|
+
throw new Error("[mandu/i18n] defineMessages() requires at least one locale");
|
|
59
|
+
}
|
|
60
|
+
for (const [locale, bundle] of Object.entries(bundles)) {
|
|
61
|
+
if (!bundle || typeof bundle !== "object") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`[mandu/i18n] defineMessages(): bundle for "${locale}" must be an object`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
for (const [key, value] of Object.entries(bundle)) {
|
|
67
|
+
if (typeof value !== "string") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`[mandu/i18n] defineMessages(): "${locale}.${key}" must be a string`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type TKey = Extract<keyof TBundle[keyof TBundle], string>;
|
|
76
|
+
|
|
77
|
+
const registry: MessageRegistry<TKey> = {
|
|
78
|
+
__manduMessages: true,
|
|
79
|
+
bundles: bundles as unknown as MessageBundle<TKey>,
|
|
80
|
+
locales,
|
|
81
|
+
lookup(locale: LocaleCode, key: TKey, fallback?: LocaleCode): string | undefined {
|
|
82
|
+
const primary = (bundles as Record<string, Record<string, string>>)[locale]?.[key];
|
|
83
|
+
if (typeof primary === "string") return primary;
|
|
84
|
+
if (fallback && fallback !== locale) {
|
|
85
|
+
const alt = (bundles as Record<string, Record<string, string>>)[fallback]?.[key];
|
|
86
|
+
if (typeof alt === "string") return alt;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return registry;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Replace `{{var}}` placeholders in `template` using `vars`. Missing
|
|
96
|
+
* vars are preserved as `{{var}}` so templating bugs are visible in
|
|
97
|
+
* the rendered output (not silently stripped). `vars[...]` values
|
|
98
|
+
* that are numbers are coerced via `String(value)`.
|
|
99
|
+
*
|
|
100
|
+
* Whitespace inside braces is tolerated: `{{ name }}` and `{{name}}`
|
|
101
|
+
* both resolve to `vars.name`.
|
|
102
|
+
*/
|
|
103
|
+
export function interpolate(
|
|
104
|
+
template: string,
|
|
105
|
+
vars: Record<string, string | number> | undefined
|
|
106
|
+
): string {
|
|
107
|
+
if (!vars) return template;
|
|
108
|
+
return template.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (match, key) => {
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
110
|
+
const value = vars[key as keyof typeof vars];
|
|
111
|
+
if (value === undefined || value === null) return match;
|
|
112
|
+
return String(value);
|
|
113
|
+
}
|
|
114
|
+
return match;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a typed `t()` function bound to a specific active locale.
|
|
120
|
+
* Misses walk `activeLocale → fallbackLocale → defaultLocale → key`
|
|
121
|
+
* (skipping duplicates). The final fallback to raw `key` makes
|
|
122
|
+
* missing-translation bugs highly visible during QA without
|
|
123
|
+
* crashing the page.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const messages = defineMessages({
|
|
128
|
+
* en: { greet: "Hi, {{name}}" },
|
|
129
|
+
* ko: { greet: "안녕, {{name}}" },
|
|
130
|
+
* } as const);
|
|
131
|
+
*
|
|
132
|
+
* const t = createTranslator(messages, { activeLocale: "ko", defaultLocale: "en" });
|
|
133
|
+
* t("greet", { name: "만두" }); // → "안녕, 만두"
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function createTranslator<TKeys extends string>(
|
|
137
|
+
registry: MessageRegistry<TKeys>,
|
|
138
|
+
opts: {
|
|
139
|
+
activeLocale: LocaleCode;
|
|
140
|
+
defaultLocale: LocaleCode;
|
|
141
|
+
fallbackLocale?: LocaleCode;
|
|
142
|
+
}
|
|
143
|
+
): Translator<TKeys> {
|
|
144
|
+
const { activeLocale, defaultLocale, fallbackLocale } = opts;
|
|
145
|
+
return function t(key, vars) {
|
|
146
|
+
const candidates: LocaleCode[] = [activeLocale];
|
|
147
|
+
if (fallbackLocale && !candidates.includes(fallbackLocale)) candidates.push(fallbackLocale);
|
|
148
|
+
if (!candidates.includes(defaultLocale)) candidates.push(defaultLocale);
|
|
149
|
+
|
|
150
|
+
for (const locale of candidates) {
|
|
151
|
+
const value = registry.lookup(locale, key as TKeys);
|
|
152
|
+
if (typeof value === "string") {
|
|
153
|
+
return interpolate(value, vars);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Final fallback — raw key preserves "unresolved" signal instead
|
|
157
|
+
// of a blank string. `vars` is intentionally ignored here because
|
|
158
|
+
// the key itself isn't a template.
|
|
159
|
+
return key as unknown as string;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Type guard for {@link MessageRegistry}. Useful when the runtime
|
|
165
|
+
* receives `unknown` from user config (e.g. dynamic imports).
|
|
166
|
+
*/
|
|
167
|
+
export function isMessageRegistry(value: unknown): value is MessageRegistry {
|
|
168
|
+
return (
|
|
169
|
+
typeof value === "object" &&
|
|
170
|
+
value !== null &&
|
|
171
|
+
(value as { __manduMessages?: unknown }).__manduMessages === true
|
|
172
|
+
);
|
|
173
|
+
}
|
package/src/i18n/types.ts
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 18.μ — i18n types.
|
|
3
|
-
*
|
|
4
|
-
* Public type surface for Mandu's first-class i18n. Separated from
|
|
5
|
-
* runtime so `import type` callers pay zero bundle cost.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A locale code. The string is opaque to the framework — we never
|
|
10
|
-
* parse it beyond "equality vs the configured allow-list", so
|
|
11
|
-
* `"en"`, `"en-US"`, `"zh-Hant"`, `"ko-KR"`, `"pt-BR"` are all valid
|
|
12
|
-
* and treated as distinct locales. Match case-sensitively to the
|
|
13
|
-
* entries of `I18nConfig.locales`.
|
|
14
|
-
*/
|
|
15
|
-
export type LocaleCode = string;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* How the active locale is resolved from an incoming request.
|
|
19
|
-
*
|
|
20
|
-
* - `"path-prefix"` — Next.js default. `/en/docs` → `en`, `/ko/docs`
|
|
21
|
-
* → `ko`. Routes without a locale prefix fall through to
|
|
22
|
-
* `defaultLocale`. Path-prefix synthesis happens at manifest
|
|
23
|
-
* build time (see `router/fs-scanner.ts`).
|
|
24
|
-
* - `"domain"` — locale-per-subdomain. `en.example.com` → `en`.
|
|
25
|
-
* The mapping is supplied via {@link I18nConfig.domains}.
|
|
26
|
-
* - `"header"` — pure `Accept-Language` negotiation. Useful for
|
|
27
|
-
* APIs where URLs should stay locale-less.
|
|
28
|
-
* - `"cookie"` — explicit user choice is stored in a cookie
|
|
29
|
-
* (default name: `mandu_locale`). Server-side persistence
|
|
30
|
-
* across navigations without URL mutation.
|
|
31
|
-
*/
|
|
32
|
-
export type I18nStrategy = "path-prefix" | "domain" | "header" | "cookie";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Resolved locale state, attached to `ctx.locale` by the runtime
|
|
36
|
-
* dispatcher. Carries the canonical `code`, the `strategy` that
|
|
37
|
-
* produced it (for `Vary:` bookkeeping), and the raw `source` so
|
|
38
|
-
* debuggers can see whether the URL won over the cookie.
|
|
39
|
-
*/
|
|
40
|
-
export interface ResolvedLocale {
|
|
41
|
-
/** The resolved locale code (always one of `I18nConfig.locales`). */
|
|
42
|
-
code: LocaleCode;
|
|
43
|
-
/** Whether this request used the default locale (no explicit signal). */
|
|
44
|
-
isDefault: boolean;
|
|
45
|
-
/** Which strategy ultimately produced the locale. */
|
|
46
|
-
strategy: I18nStrategy | "default" | "fallback";
|
|
47
|
-
/** Raw input that produced the match, for debugging. */
|
|
48
|
-
source?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* A map of translation strings keyed by locale. Typically declared
|
|
53
|
-
* `as const` so keys are inferred literally and the `t()` helper can
|
|
54
|
-
* reject typos at compile time.
|
|
55
|
-
*/
|
|
56
|
-
export type MessageBundle<TKeys extends string = string> = {
|
|
57
|
-
[locale: string]: Record<TKeys, string>;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Configuration for `defineI18n()`. Kept shallow so it round-trips
|
|
62
|
-
* through JSON config files without loss.
|
|
63
|
-
*/
|
|
64
|
-
export interface I18nConfig {
|
|
65
|
-
/**
|
|
66
|
-
* Non-empty allow-list of supported locales. First entry is NOT
|
|
67
|
-
* automatically the default — `defaultLocale` is explicit.
|
|
68
|
-
*/
|
|
69
|
-
locales: readonly LocaleCode[];
|
|
70
|
-
/** Fallback when no resolver returns a known locale. MUST be in `locales`. */
|
|
71
|
-
defaultLocale: LocaleCode;
|
|
72
|
-
/**
|
|
73
|
-
* Optional fallback chain. When `t(key)` misses in the active
|
|
74
|
-
* locale, we look up `fallback` before `defaultLocale` before
|
|
75
|
-
* returning the raw key. Typical use: `{ defaultLocale: 'en',
|
|
76
|
-
* fallback: 'en-US' }` so `zh-Hant` falls through `en-US` to `en`.
|
|
77
|
-
*/
|
|
78
|
-
fallback?: LocaleCode;
|
|
79
|
-
/** Locale detection strategy. See {@link I18nStrategy}. */
|
|
80
|
-
strategy: I18nStrategy;
|
|
81
|
-
/**
|
|
82
|
-
* Cookie name for `strategy: 'cookie'` OR a cookie override for
|
|
83
|
-
* any other strategy (useful for "user explicitly picked a
|
|
84
|
-
* locale" persistence). Default: `"mandu_locale"`.
|
|
85
|
-
*/
|
|
86
|
-
cookieName?: string;
|
|
87
|
-
/**
|
|
88
|
-
* Domain → locale map for `strategy: 'domain'`. Required when
|
|
89
|
-
* `strategy === "domain"`; ignored otherwise.
|
|
90
|
-
*/
|
|
91
|
-
domains?: Record<string, LocaleCode>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Result of `defineI18n()`. A frozen config object that downstream
|
|
96
|
-
* resolver + registry use. The brand field prevents accidental
|
|
97
|
-
* coercion from plain literals.
|
|
98
|
-
*/
|
|
99
|
-
export interface I18nDefinition extends I18nConfig {
|
|
100
|
-
readonly __manduI18n: true;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Strongly-typed translator. Given a registry whose entries
|
|
105
|
-
* declared `as const`, `t()` rejects typos in `key`, and
|
|
106
|
-
* `vars` accepts any `{{name}}` placeholders present in the
|
|
107
|
-
* template.
|
|
108
|
-
*/
|
|
109
|
-
export type Translator<TKeys extends string = string> = (
|
|
110
|
-
key: TKeys,
|
|
111
|
-
vars?: Record<string, string | number>
|
|
112
|
-
) => string;
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.μ — i18n types.
|
|
3
|
+
*
|
|
4
|
+
* Public type surface for Mandu's first-class i18n. Separated from
|
|
5
|
+
* runtime so `import type` callers pay zero bundle cost.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A locale code. The string is opaque to the framework — we never
|
|
10
|
+
* parse it beyond "equality vs the configured allow-list", so
|
|
11
|
+
* `"en"`, `"en-US"`, `"zh-Hant"`, `"ko-KR"`, `"pt-BR"` are all valid
|
|
12
|
+
* and treated as distinct locales. Match case-sensitively to the
|
|
13
|
+
* entries of `I18nConfig.locales`.
|
|
14
|
+
*/
|
|
15
|
+
export type LocaleCode = string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How the active locale is resolved from an incoming request.
|
|
19
|
+
*
|
|
20
|
+
* - `"path-prefix"` — Next.js default. `/en/docs` → `en`, `/ko/docs`
|
|
21
|
+
* → `ko`. Routes without a locale prefix fall through to
|
|
22
|
+
* `defaultLocale`. Path-prefix synthesis happens at manifest
|
|
23
|
+
* build time (see `router/fs-scanner.ts`).
|
|
24
|
+
* - `"domain"` — locale-per-subdomain. `en.example.com` → `en`.
|
|
25
|
+
* The mapping is supplied via {@link I18nConfig.domains}.
|
|
26
|
+
* - `"header"` — pure `Accept-Language` negotiation. Useful for
|
|
27
|
+
* APIs where URLs should stay locale-less.
|
|
28
|
+
* - `"cookie"` — explicit user choice is stored in a cookie
|
|
29
|
+
* (default name: `mandu_locale`). Server-side persistence
|
|
30
|
+
* across navigations without URL mutation.
|
|
31
|
+
*/
|
|
32
|
+
export type I18nStrategy = "path-prefix" | "domain" | "header" | "cookie";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolved locale state, attached to `ctx.locale` by the runtime
|
|
36
|
+
* dispatcher. Carries the canonical `code`, the `strategy` that
|
|
37
|
+
* produced it (for `Vary:` bookkeeping), and the raw `source` so
|
|
38
|
+
* debuggers can see whether the URL won over the cookie.
|
|
39
|
+
*/
|
|
40
|
+
export interface ResolvedLocale {
|
|
41
|
+
/** The resolved locale code (always one of `I18nConfig.locales`). */
|
|
42
|
+
code: LocaleCode;
|
|
43
|
+
/** Whether this request used the default locale (no explicit signal). */
|
|
44
|
+
isDefault: boolean;
|
|
45
|
+
/** Which strategy ultimately produced the locale. */
|
|
46
|
+
strategy: I18nStrategy | "default" | "fallback";
|
|
47
|
+
/** Raw input that produced the match, for debugging. */
|
|
48
|
+
source?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A map of translation strings keyed by locale. Typically declared
|
|
53
|
+
* `as const` so keys are inferred literally and the `t()` helper can
|
|
54
|
+
* reject typos at compile time.
|
|
55
|
+
*/
|
|
56
|
+
export type MessageBundle<TKeys extends string = string> = {
|
|
57
|
+
[locale: string]: Record<TKeys, string>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Configuration for `defineI18n()`. Kept shallow so it round-trips
|
|
62
|
+
* through JSON config files without loss.
|
|
63
|
+
*/
|
|
64
|
+
export interface I18nConfig {
|
|
65
|
+
/**
|
|
66
|
+
* Non-empty allow-list of supported locales. First entry is NOT
|
|
67
|
+
* automatically the default — `defaultLocale` is explicit.
|
|
68
|
+
*/
|
|
69
|
+
locales: readonly LocaleCode[];
|
|
70
|
+
/** Fallback when no resolver returns a known locale. MUST be in `locales`. */
|
|
71
|
+
defaultLocale: LocaleCode;
|
|
72
|
+
/**
|
|
73
|
+
* Optional fallback chain. When `t(key)` misses in the active
|
|
74
|
+
* locale, we look up `fallback` before `defaultLocale` before
|
|
75
|
+
* returning the raw key. Typical use: `{ defaultLocale: 'en',
|
|
76
|
+
* fallback: 'en-US' }` so `zh-Hant` falls through `en-US` to `en`.
|
|
77
|
+
*/
|
|
78
|
+
fallback?: LocaleCode;
|
|
79
|
+
/** Locale detection strategy. See {@link I18nStrategy}. */
|
|
80
|
+
strategy: I18nStrategy;
|
|
81
|
+
/**
|
|
82
|
+
* Cookie name for `strategy: 'cookie'` OR a cookie override for
|
|
83
|
+
* any other strategy (useful for "user explicitly picked a
|
|
84
|
+
* locale" persistence). Default: `"mandu_locale"`.
|
|
85
|
+
*/
|
|
86
|
+
cookieName?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Domain → locale map for `strategy: 'domain'`. Required when
|
|
89
|
+
* `strategy === "domain"`; ignored otherwise.
|
|
90
|
+
*/
|
|
91
|
+
domains?: Record<string, LocaleCode>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Result of `defineI18n()`. A frozen config object that downstream
|
|
96
|
+
* resolver + registry use. The brand field prevents accidental
|
|
97
|
+
* coercion from plain literals.
|
|
98
|
+
*/
|
|
99
|
+
export interface I18nDefinition extends I18nConfig {
|
|
100
|
+
readonly __manduI18n: true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Strongly-typed translator. Given a registry whose entries
|
|
105
|
+
* declared `as const`, `t()` rejects typos in `key`, and
|
|
106
|
+
* `vars` accepts any `{{name}}` placeholders present in the
|
|
107
|
+
* template.
|
|
108
|
+
*/
|
|
109
|
+
export type Translator<TKeys extends string = string> = (
|
|
110
|
+
key: TKeys,
|
|
111
|
+
vars?: Record<string, string | number>
|
|
112
|
+
) => string;
|