okengine 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +148 -13
  2. package/package.json +4 -3
  3. package/site/content/docs/elements/ai.mdx +82 -1
  4. package/site/content/docs/elements/channel.mdx +77 -8
  5. package/site/content/docs/elements/flow.mdx +20 -17
  6. package/site/content/docs/plugins/email-otp.mdx +25 -19
  7. package/site/content/docs/plugins/magic-link.mdx +27 -21
  8. package/site/content/docs/reference/configuration.mdx +12 -4
  9. package/site/content/docs/reference/environment-variables.mdx +42 -13
  10. package/site/content/docs/reference/errors.mdx +14 -0
  11. package/site/content/docs/reference/fx.mdx +68 -16
  12. package/site/content/docs/reference/i18n.mdx +313 -0
  13. package/site/content/docs/reference/index.mdx +6 -1
  14. package/site/content/docs/reference/meta.json +1 -0
  15. package/site/content/docs/reference/plugins.mdx +1 -0
  16. package/src/auth/auth.test.ts +3 -0
  17. package/src/auth/bindings.ts +1 -1
  18. package/src/auth/method-context.ts +12 -2
  19. package/src/cli/openbao-restart.integration.test.ts +106 -97
  20. package/src/compiler/aot.test.ts +16 -13
  21. package/src/compiler/effects-infer.ts +46 -0
  22. package/src/console/server/ai.test.ts +34 -5
  23. package/src/docker/compose.ts +9 -0
  24. package/src/docker/docker.test.ts +39 -0
  25. package/src/docker/dockerfile.integration.test.ts +126 -119
  26. package/src/docker/index.ts +11 -1
  27. package/src/docker/recipes/index.ts +3 -1
  28. package/src/docker/recipes/ollama.ts +43 -0
  29. package/src/docker/stack-id.ts +2 -0
  30. package/src/docker/stack.integration.test.ts +118 -102
  31. package/src/drivers/ai-mock.ts +60 -0
  32. package/src/drivers/ai-ollama-tools.integration.test.ts +109 -0
  33. package/src/drivers/ai-ollama.integration.test.ts +181 -0
  34. package/src/drivers/ai-ollama.ts +327 -0
  35. package/src/drivers/ai-openai-compatible.ts +211 -21
  36. package/src/drivers/ai-providers.test.ts +179 -2
  37. package/src/drivers/ai-stream.test.ts +195 -0
  38. package/src/drivers/ai-types.ts +42 -1
  39. package/src/drivers/channel-fcm.ts +49 -53
  40. package/src/drivers/channel-msegat.ts +61 -0
  41. package/src/drivers/channel-sently-map.ts +57 -0
  42. package/src/drivers/channel-sently.test.ts +99 -0
  43. package/src/drivers/channel-smtp.ts +8 -2
  44. package/src/drivers/channel-sndr.ts +28 -0
  45. package/src/drivers/channel-taqnyat.ts +57 -0
  46. package/src/drivers/channel-types.ts +79 -2
  47. package/src/drivers/channel-unifonic.ts +26 -43
  48. package/src/drivers/channel-wa-cloud.ts +33 -47
  49. package/src/drivers/channel-webpush.ts +39 -239
  50. package/src/drivers/index.ts +25 -1
  51. package/src/drivers/ollama.ts +14 -0
  52. package/src/elements/ai/rate.test.ts +53 -0
  53. package/src/elements/ai/rate.ts +66 -0
  54. package/src/elements/ai/redacted-prompt.test.ts +90 -0
  55. package/src/elements/ai/runtime.ts +330 -100
  56. package/src/elements/ai/tools.test.ts +99 -0
  57. package/src/elements/ai.test.ts +26 -2
  58. package/src/elements/ai.ts +10 -1
  59. package/src/elements/channel/costs.test.ts +2 -2
  60. package/src/elements/channel/costs.ts +14 -2
  61. package/src/elements/channel/mime.ts +11 -0
  62. package/src/elements/channel/runtime.ts +94 -0
  63. package/src/elements/channel/sndr-webhooks.test.ts +26 -0
  64. package/src/elements/channel.ts +10 -1
  65. package/src/elements/index.ts +9 -0
  66. package/src/i18n/catalogs/ar.ts +67 -0
  67. package/src/i18n/catalogs/en.ts +68 -0
  68. package/src/i18n/failure-message.test.ts +56 -0
  69. package/src/i18n/failure-message.ts +93 -0
  70. package/src/i18n/format.ts +67 -0
  71. package/src/i18n/index.ts +57 -0
  72. package/src/i18n/locale-context.ts +48 -0
  73. package/src/i18n/messages.test.ts +173 -0
  74. package/src/i18n/messages.ts +169 -0
  75. package/src/i18n/types.ts +90 -0
  76. package/src/index.ts +26 -0
  77. package/src/kernel/app.ts +92 -2
  78. package/src/kernel/boot-bind/ai.test.ts +60 -0
  79. package/src/kernel/boot-bind/ai.ts +125 -2
  80. package/src/kernel/boot-bind/channel.test.ts +68 -3
  81. package/src/kernel/boot-bind/channel.ts +93 -2
  82. package/src/kernel/boot.test.ts +4 -3
  83. package/src/kernel/boot.ts +1 -1
  84. package/src/kernel/errors.ts +56 -5
  85. package/src/kernel/fx.test.ts +27 -0
  86. package/src/kernel/fx.ts +74 -18
  87. package/src/kernel/pipeline.test.ts +4 -0
  88. package/src/kernel/pipeline.ts +1 -1
  89. package/src/kernel/plugin.ts +16 -0
  90. package/src/kernel/registry.ts +15 -0
  91. package/src/plugins/auth/shared.ts +5 -1
  92. package/src/plugins/auth-delivery.mailpit.integration.test.ts +336 -0
  93. package/src/plugins/auth-methods.security.test.ts +12 -10
  94. package/src/plugins/email-otp.ts +54 -1
  95. package/src/plugins/index.ts +16 -2
  96. package/src/plugins/magic-link.ts +63 -3
  97. package/src/plugins/username-policy.test.ts +302 -0
  98. package/src/plugins/username.ts +290 -9
  99. package/src/release/exports.test.ts +26 -0
  100. package/src/release/exports.ts +64 -5
  101. package/src/release/index.ts +5 -0
  102. package/src/release/measure.exports.test.ts +13 -1
  103. package/src/release/measure.ts +84 -14
  104. package/src/release/official-plugins.ts +46 -0
  105. package/src/release/readme.test.ts +30 -2
@@ -5,12 +5,13 @@ icon: "Link2"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
8
- `magicLink()` issues a hashed, single-use token (default 10 minutes). Verify exchanges it for a
9
- hybrid session and creates the user on first success.
8
+ `magicLink()` issues a hashed, single-use token (default 10 minutes). Request sends the link
9
+ via Channel (`auth-magic-link`); verify exchanges it for a hybrid session and creates the user
10
+ on first success.
10
11
 
11
12
  <Callout title="The one rule">
12
13
  Enable `gate.auth`, then `.plug(magicLink())`. Tokens are hashed at rest — never log the raw link.
13
- Channel email delivery is not wired yet; use `exposeDevToken` locally.
14
+ Delivery goes through `fx.send`; use `exposeDevToken` only for local DX without SMTP.
14
15
  </Callout>
15
16
 
16
17
  ## Quick start
@@ -28,7 +29,7 @@ export const app = oke({
28
29
  name: "shop",
29
30
  env: "local",
30
31
  gate: { auth: {} },
31
- }).plug(magicLink({ exposeDevToken: true }));
32
+ }).plug(magicLink({ baseUrl: "http://127.0.0.1:6530" }));
32
33
  ```
33
34
 
34
35
  </Step>
@@ -38,10 +39,12 @@ export const app = oke({
38
39
 
39
40
  ```typescript
40
41
  const { data } = await api.auth.requestMagicLink({ email: "ali@example.com" });
41
- // data.ok === true; data.devToken only when exposeDevToken
42
+ // data.ok === true; Channel delivers auth-magic-link
43
+ // data.devToken only when exposeDevToken
42
44
  ```
43
45
 
44
- `POST /auth/magic-link/request`.
46
+ `POST /auth/magic-link/request`. Locally the `console` driver captures mail; with
47
+ `oke dev --docker`, Mailpit receives the real SMTP message.
45
48
 
46
49
  </Step>
47
50
 
@@ -49,7 +52,7 @@ const { data } = await api.auth.requestMagicLink({ email: "ali@example.com" });
49
52
  ### Verify
50
53
 
51
54
  ```typescript
52
- const { data } = await api.auth.verifyMagicLink({ token: data!.devToken! });
55
+ const { data } = await api.auth.verifyMagicLink({ token });
53
56
  // session tokens + userId
54
57
  ```
55
58
 
@@ -62,14 +65,16 @@ const { data } = await api.auth.verifyMagicLink({ token: data!.devToken! });
62
65
 
63
66
  ## Options
64
67
 
65
- | Option | Type | Default | Meaning |
66
- | ---------------- | ------------------- | -------- | ----------------------------------------- |
67
- | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
68
- | `sessions` | `SessionStore` | active\* | Session store |
69
- | `ttlMs` | `number` | 10m | Challenge lifetime |
70
- | `exposeDevToken` | `boolean` | `false` | Include raw token in the request response |
71
- | `identities` | `IdentityStore` | new | Email user map |
72
- | `verifications` | `VerificationStore` | new | Challenge store |
68
+ | Option | Type | Default | Meaning |
69
+ | ---------------- | ------------------- | -------------------------- | ----------------------------------------- |
70
+ | `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
71
+ | `sessions` | `SessionStore` | active\* | Session store |
72
+ | `ttlMs` | `number` | 10m | Challenge lifetime |
73
+ | `baseUrl` | `string` | `OKE_APP_URL` or `:6530` | Origin used to build the magic link |
74
+ | `from` | `string` | `OKE <no-reply@oke.local>` | Template From address |
75
+ | `exposeDevToken` | `boolean` | `false` | Include raw token in the request response |
76
+ | `identities` | `IdentityStore` | new | Email → user map |
77
+ | `verifications` | `VerificationStore` | new | Challenge store |
73
78
 
74
79
  ## Surfaces
75
80
 
@@ -78,8 +83,8 @@ const { data } = await api.auth.verifyMagicLink({ token: data!.devToken! });
78
83
  | `auth.requestMagicLink` | `POST /auth/magic-link/request` | `gate.public` + otp rate |
79
84
  | `auth.verifyMagicLink` | `POST /auth/magic-link/verify` | `gate.public` + otp rate |
80
85
 
81
- **Consequence:** until Channel delivery ships, production apps must send the link themselves
82
- (or keep `exposeDevToken` off and never log tokens).
86
+ **Consequence:** the plugin contributes the `auth-magic-link` Channel template and EN/AR
87
+ catalog bodies (`{{link}}`, `{{token}}`). Override copy by merging your own catalog at boot.
83
88
 
84
89
  ## Troubleshooting
85
90
 
@@ -91,8 +96,9 @@ Token expired (default 10m), already used, or mistyped. Request a new link.
91
96
  </Accordion>
92
97
  <Accordion title="No email arrived">
93
98
 
94
- v1 does not send mail. Use `exposeDevToken` in local/test, or deliver `devToken` / your own
95
- link via a Channel Flow you own.
99
+ In `local` / `test` the `console` driver captures mail into the inbox nothing hits a mailbox.
100
+ Run `oke dev --docker` and open Mailpit (`MAILPIT_UI_URL`) to see the rendered message. For
101
+ unit tests without SMTP, set `exposeDevToken: true`.
96
102
 
97
103
  </Accordion>
98
104
  </Accordions>
@@ -101,12 +107,12 @@ link via a Channel Flow you own.
101
107
 
102
108
  - [Email OTP](/docs/plugins/email-otp) — numeric code instead of a link
103
109
  - [Gate](/docs/elements/gate) — `gate.auth`
104
- - [Channel](/docs/elements/channel) — when you wire delivery yourself
110
+ - [Channel](/docs/elements/channel) — `fx.send`, Mailpit, consents
105
111
 
106
112
  ## Next
107
113
 
108
114
  <Cards>
109
115
  <Card title="Email OTP" description="Six-digit email codes." href="/docs/plugins/email-otp" />
110
116
  <Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
111
- <Card title="Username" description="Password sign-in." href="/docs/plugins/username" />
117
+ <Card title="Channel" description="Email delivery and Mailpit." href="/docs/elements/channel" />
112
118
  </Cards>
@@ -30,6 +30,7 @@ drivers: {
30
30
  vault: { local: "dotenv", docker: "openbao", test: "memory", prod: "openbao" },
31
31
  channel: {
32
32
  email: { local: "console", docker: "smtp", test: "console", prod: "smtp" },
33
+ sms: { local: "console", docker: "taqnyat", test: "console", prod: "taqnyat" },
33
34
  },
34
35
  },
35
36
  ```
@@ -43,10 +44,10 @@ drivers: {
43
44
  | `signal` | env driver map | `memory` · `postgres` · `redis` · `nats` |
44
45
  | `clock` | env driver map | `memory` · `postgres` · `frozen` |
45
46
  | `vault` | env driver map | `dotenv` · `openbao` · `memory` |
46
- | `channel.email` | env driver map | `console` · `smtp` · `resend` |
47
- | `channel.sms` | env driver map | `console` · `unifonic` |
48
- | `channel.whatsapp` | env driver map | medium declared; drivers added per provider |
49
- | `channel.push` | env driver map | medium declared; drivers added per provider |
47
+ | `channel.email` | env driver map | `console` · `smtp` · `resend` · `sndr` |
48
+ | `channel.sms` | env driver map | `console` · `taqnyat` · `msegat` · `unifonic` |
49
+ | `channel.whatsapp` | env driver map | `console` · `wa-cloud` |
50
+ | `channel.push` | env driver map | `console` · `webpush` · `fcm` |
50
51
  | `ai` | env driver map | `mock` · `anthropic` · `openai-compatible` · `bedrock` · `vertex` · `ollama` |
51
52
  | `runs` | env driver map | runs-store driver (distinct from top-level `runs`) |
52
53
  | `prod` | `string[]` | flat protocol list for the Manifest — nested maps are preferred |
@@ -76,9 +77,12 @@ images: {
76
77
  "store.files": "rustfs/rustfs:1.0.0-beta.11",
77
78
  "channel.email": "axllent/mailpit:v1.22.3",
78
79
  vault: "openbao/openbao:2.6.1",
80
+ ai: "ollama/ollama:latest",
79
81
  },
80
82
  ```
81
83
 
84
+ For `ai`, the Ollama recipe pulls `OKE_AI_MODEL` (default `qwen3.5:9b` — a balanced local-dev starting point; override freely; on Apple Silicon prefer `qwen3.5:9b-mlx`) and writes `OKE_AI_URL`.
85
+
82
86
  ## i18n
83
87
 
84
88
  | Option | Type | Default | Meaning |
@@ -91,6 +95,10 @@ images: {
91
95
  i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
92
96
  ```
93
97
 
98
+ `fx.t` catalogs are ICU strings from `defineLocale` / `defineMessages` — see
99
+ [i18n](/docs/reference/i18n). Built-ins also cover failure `error.message` and
100
+ OKE cause/fix; Channel templates keep separate `locales` + `{{field}}` bodies.
101
+
94
102
  ## tenancy
95
103
 
96
104
  Multi-tenant isolation policy.
@@ -61,22 +61,51 @@ Written by the first-boot bootstrap; you normally only set these when pointing a
61
61
  | `OKE_VAULT_TOKEN` | App token (least-privilege) | — |
62
62
  | `OKE_VAULT_MOUNT` | KV v2 mount path | `"secret"` |
63
63
 
64
- ## Channel (email)
65
-
66
- | Variable | Used for |
67
- | ----------------------- | ----------------------------------------- |
68
- | `SMTP_URL` | Full SMTP connection string (`smtp://…`) |
69
- | `SMTP_USER` | Overrides the user embedded in `SMTP_URL` |
70
- | `SMTP_PASSWORD` | Overrides the password in `SMTP_URL` |
71
- | `OKE_CHANNEL_EMAIL_URL` | OKE-specific alternative to `SMTP_URL` |
64
+ ## Channel (email) — boot binder
65
+
66
+ Read when `drivers.channel.email` resolves to that driver id.
67
+
68
+ | Variable | Used for |
69
+ | ----------------------- | ------------------------------------------------ |
70
+ | `SMTP_URL` | `smtp` full SMTP URL (`smtp://…`) |
71
+ | `SMTP_USER` | Overrides the user embedded in `SMTP_URL` |
72
+ | `SMTP_PASSWORD` | Overrides the password in `SMTP_URL` |
73
+ | `OKE_CHANNEL_EMAIL_URL` | Alternative to `SMTP_URL` |
74
+ | `RESEND_API_KEY` | `resend` API key |
75
+ | `SNDR_API_KEY` | `sndr` API key |
76
+ | `SNDR_BASE_URL` | Optional SNDR API origin (default `api.sndr.sh`) |
77
+
78
+ ## Channel (SMS) — boot binder
79
+
80
+ Read when `drivers.channel.sms` resolves to that driver id (`console` opens nothing).
81
+
82
+ | Variable | Used for |
83
+ | ---------------------- | -------------------------------- |
84
+ | `TAQNYAT_BEARER_TOKEN` | `taqnyat` bearer token |
85
+ | `TAQNYAT_TOKEN` | Alias for `TAQNYAT_BEARER_TOKEN` |
86
+ | `TAQNYAT_SENDER` | Taqnyat pre-approved sender id |
87
+ | `MSEGAT_USERNAME` | `msegat` account username |
88
+ | `MSEGAT_API_KEY` | `msegat` API key |
89
+ | `MSEGAT_SENDER` | Msegat pre-approved sender id |
90
+ | `UNIFONIC_APPSID` | `unifonic` AppSid |
91
+ | `UNIFONIC_APP_SID` | Alias for `UNIFONIC_APPSID` |
92
+ | `UNIFONIC_SENDER` | Unifonic SenderID (optional) |
93
+
94
+ WhatsApp (`wa-cloud`) and push (`webpush` / `fcm`) are not opened from env at
95
+ boot — pass them on `BootOptions.channel.drivers` with their open options.
72
96
 
73
97
  ## AI providers
74
98
 
75
- | Variable | Used for |
76
- | ------------------- | --------------------------------------- |
77
- | `ANTHROPIC_API_KEY` | `anthropic` driver credential |
78
- | `ANTHROPIC_MODEL` | Model override for the anthropic driver |
79
- | `OPENAI_API_KEY` | `openai-compatible` driver credential |
99
+ | Variable | Used for | Default when unset |
100
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
101
+ | `ANTHROPIC_API_KEY` | `anthropic` driver credential | — |
102
+ | `ANTHROPIC_MODEL` | Model override for the anthropic driver | |
103
+ | `OPENAI_API_KEY` | `openai-compatible` driver credential | — |
104
+ | `OPENAI_BASE_URL` | `openai-compatible` base URL | OpenAI cloud |
105
+ | `OKE_AI_DRIVER` | Force the AI driver id (honoured in docker mode) | config map |
106
+ | `OKE_AI_URL` | Ollama (or openai-compatible) base URL | `http://127.0.0.1:11434` (ollama) |
107
+ | `OKE_AI_MODEL` | Default model for `ollama` (any pulled model; `qwen3.5:9b` is a balanced local-dev starting point; on Apple Silicon try `qwen3.5:9b-mlx`) | `qwen3.5:9b` |
108
+ | `OLLAMA_HOST` | Alternate Ollama host (`host:port` or URL) when `OKE_AI_URL` is unset | — |
80
109
 
81
110
  ## Framework behavior
82
111
 
@@ -14,6 +14,19 @@ OKE1101 domain table not found — migrations have not been applied.
14
14
 
15
15
  Codes are permanent once published — you can match on them safely across upgrades.
16
16
 
17
+ ## Localized messages
18
+
19
+ Typed failures and OKE codes ship English and Arabic ICU catalogs. Locale comes
20
+ from `Accept-Language` (matched to `i18n.locales`); fallback is `i18n.default`.
21
+
22
+ | Surface | Keys | How it appears |
23
+ | ------------------ | ------------------------------------------ | ---------------------------------------- |
24
+ | `fx.fail` / `fail` | `errors.{code}` · `errors.{code}.{reason}` | Optional `error.message` on the envelope |
25
+ | Thrown `OkeError` | `oke.{code}.cause` · `oke.{code}.fix` | Cause + fix lines in the thrown message |
26
+
27
+ Override via `defineLocale`. Pass `fail(code, data, { message })` for a custom
28
+ string. Custom app codes stay message-less until registered.
29
+
17
30
  ## OKE numeric codes
18
31
 
19
32
  | Code | Name | Cause | Fix |
@@ -75,6 +88,7 @@ Thrown by specific subsystems — each names its own cause:
75
88
 
76
89
  ## Learn more
77
90
 
91
+ - [i18n](/docs/reference/i18n) — catalogs, `fx.t`, locale matching
78
92
  - [Flow](/docs/elements/flow) — `fx.fail` and the response envelope
79
93
  - [Gate](/docs/elements/gate) — where the three denials come from
80
94
  - [CLI Reference](/docs/reference/cli) — `oke db migrate` and friends
@@ -102,20 +102,23 @@ const rows = await fx.using(
102
102
 
103
103
  ## Channel
104
104
 
105
- | Signature | Records | Notes |
106
- | ----------------------------------------- | ------- | ------------------------------------------------------------------- |
107
- | `fx.send(template, { to?, data?, via? })` | `send` | `via` orders the fallback chain; dry runs record _would have fired_ |
105
+ | Signature | Records | Notes |
106
+ | -------------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
107
+ | `fx.send(template, { to?, data?, via?, })` | `send` | `via` orders fallback; `locale` / `profileLocale` / `acceptLanguage` feed the locale chain |
108
+
109
+ Omit locale opts and the send uses `fx.locale`. Dry runs record _would have fired_ and never
110
+ contact a provider. Channel bodies use `{{field}}` catalogs — not ICU (see [Channel](/docs/elements/channel)).
108
111
 
109
112
  ## AI
110
113
 
111
- | Signature | Records | Returns |
112
- | -------------------------------------- | ------- | ------------------------------------------- |
113
- | `fx.ask(prompt, input?, { via? })` | `ask` | Object validated against the prompt's `out` |
114
- | `fx.run(agent, input?)` | `ask` | Agent result |
115
- | `fx.stream(model, { prompt?, data? })` | `ask` | `AsyncIterable<string>` of chunks |
116
- | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
114
+ | Signature | Records | Returns |
115
+ | ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------- |
116
+ | `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
117
+ | `fx.run(agent, input?)` | `ask` | Agent result |
118
+ | `fx.stream(model, { prompt?, data? })` | `ask` | `AsyncIterable<string>` real driver stream; cancels via ambient `fx.signal` |
119
+ | `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
117
120
 
118
- AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them.
121
+ AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
119
122
 
120
123
  ## Vault
121
124
 
@@ -153,11 +156,59 @@ Returning a plain value instead answers 200 with `{ data: value, error: null }`
153
156
 
154
157
  ## Logging, i18n, ids
155
158
 
156
- | Signature | Notes |
157
- | ------------------------------------------ | ---------------------------------------- |
158
- | `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***` |
159
- | `fx.t(key, params?)` | Localized message from the `i18n` config |
160
- | `fx.id()` | UUID the only legal id generator |
159
+ | Signature | Notes |
160
+ | ------------------------------------------ | ----------------------------------------------------------- |
161
+ | `fx.log.debug/info/warn/error(msg, data?)` | Redacting — secrets print as `***` |
162
+ | `fx.t(key, values?)` | ICU MessageFormat active locale → `i18n.default` key |
163
+ | `fx.locale` | Active locale (`Accept-Language` matched to `i18n.locales`) |
164
+ | `fx.id()` | UUID — the only legal id generator |
165
+
166
+ `fx.t` uses [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/)
167
+ (FormatJS): `{name}`, plurals, `select` / `selectordinal`, rich-text tags.
168
+ Full syntax and escaping: [i18n · ICU](/docs/reference/i18n#icu-messageformat).
169
+
170
+ Register catalogs with `defineLocale` before boot (nested trees → dot keys). Augment
171
+ `Register` so keys autocomplete and typos fail at compile time:
172
+
173
+ ```typescript
174
+ import { defineLocale, defineMessages, type MessagesFor } from "okengine";
175
+
176
+ export const en = defineMessages({
177
+ greeting: "Hello, {name}",
178
+ items: "{count, plural, one {# item} other {# items}}",
179
+ place: "You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}!",
180
+ status: "{status, select, online {Online} offline {Offline} other {Unknown}}",
181
+ cta: "Read <docs>the docs</docs>",
182
+ });
183
+ defineLocale("en", en);
184
+
185
+ defineLocale("ar", {
186
+ greeting: "مرحباً، {name}",
187
+ items: "{count, plural, zero {لا عناصر} one {عنصر واحد} other {# عناصر}}",
188
+ place: "أنهيت في المرتبة {place, selectordinal, other {#}}!",
189
+ status: "{status, select, online {متصل} offline {غير متصل} other {غير معروف}}",
190
+ cta: "اقرأ <docs>التوثيق</docs>",
191
+ } satisfies MessagesFor<typeof en>);
192
+
193
+ declare module "okengine" {
194
+ interface Register {
195
+ messages: typeof en;
196
+ }
197
+ }
198
+ ```
199
+
200
+ ```typescript
201
+ fx.t("greeting", { name: input.name });
202
+ fx.t("items", { count: 3 });
203
+ fx.t("cta", { docs: (chunks) => `<a href="/docs">${chunks.join("")}</a>` });
204
+ ```
205
+
206
+ Supported locales and the fallback come from `oke.config.ts` `i18n`.
207
+ `create-oke` scaffolds English and Arabic locale modules already wired this way.
208
+
209
+ Built-in `errors.*` / `oke.*` catalogs localize `fx.fail` messages and thrown
210
+ `OkeError` cause/fix the same way — full i18n guide:
211
+ [i18n](/docs/reference/i18n), [Errors](/docs/reference/errors).
161
212
 
162
213
  ## Principals
163
214
 
@@ -181,5 +232,6 @@ must log who started the call chain.
181
232
  ## Learn more
182
233
 
183
234
  - [Flow](/docs/elements/flow) — why `fx` is the only door
235
+ - [Channel](/docs/elements/channel) — `fx.send`, consent, locale chain, `{{field}}` catalogs
184
236
  - [Errors](/docs/reference/errors) — what `fx.fail` produces
185
- - [Configuration](/docs/reference/configuration) — the drivers behind these calls
237
+ - [Configuration](/docs/reference/configuration) — drivers and the `i18n` block