Reference

i18n

ICU message catalogs for fx.t, typed keys, request locale, and how failures and channels pick a language.

App copy lives in message catalogs — greetings, plurals, and status lines you format inside a Flow with fx.t. Configure supported locales once in oke.config.ts; the request's Accept-Language picks the active tag.

The one rule

Register catalogs with defineLocale before boot, list every locale in i18n.locales, and call fx.t(key, values?) for Flow copy. Channel emails use a separate {{ field }} catalog — not ICU.

Quick start

Configure locales

oke.config.ts
i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },

If i18n is omitted, boot still defaults to locales: ["en", "ar"] and default: "en".

Register catalogs

Side-effect import locale modules before boot (the starter already does this from src/app.ts):

src/locales/en.ts
import { defineMessages, defineLocale } from "okengine";

export const en = defineMessages({
  greeting: "Hello, {name}",
  items: "{count, plural, one {# item} other {# items}}",
  errors: { notFound: "Not found" },
});
defineLocale("en", en);

declare module "okengine" {
  interface Register {
    messages: typeof en;
  }
}
src/locales/ar.ts
import { defineLocale, type MessagesFor } from "okengine";
import { en } from "./en";

defineLocale("ar", {
  greeting: "مرحباً، {name}",
  items: "{count, plural, zero {لا عناصر} one {عنصر واحد} other {# عناصر}}",
  errors: { notFound: "غير موجود" },
} satisfies MessagesFor<typeof en>);

Use fx.t in a Flow

do: async (input, fx) => {
  return {
    text: fx.t("greeting", { name: input.name }),
    countLabel: fx.t("items", { count: input.count }),
    locale: fx.locale,
  };
},

Send Accept-Language: ar (or ar-SA) against locales: ["en", "ar"] and fx.locale is "ar". Missing keys fall back through i18n.default, then the key string itself.

Config (i18n)

OptionTypeDefault (when omitted)Meaning
localesstring[]["en", "ar"]Tags matched against Accept-Language
defaultstring"en"Fallback for fx.t, Channel, fail messages
dirrecordPer-locale direction: "ltr" | "rtl"

Matching: exact tag → language subtag (ar-SAar) → default.

fx.t and fx.locale

SignatureNotes
fx.t(key, values?)ICU MessageFormat — active locale → i18n.default → key
fx.localeActive BCP 47 tag for this run

Nested trees flatten to dot keys (errors.notFound). App overlays win over built-in keys for the same locale.

ICU MessageFormat

fx.t formats catalog strings with ICU MessageFormat (FormatJS). The active locale drives plural/select rules — English one/other vs Arabic zero/two/few/many on the same key.

FeatureSyntax sketchvalues
InterpolationHello, {name}{ name: "Ada" }
Exact plural{count, plural, =0 {none} one {# item} other {# items}}{ count: 0 }
Cardinal plural{count, plural, one {…} other {…}}{ count: number }
Ordinal{place, selectordinal, one {#st} two {#nd} other {#th}}{ place: number }
Select{status, select, online {…} offline {…} other {…}}{ status: "online" }
Rich-text tagRead <docs>the docs</docs>{ docs: (chunks) => "…" }

# inside a plural/ordinal branch is the numeric argument. Always include an other (or =N) branch — ICU requires a fallback.

Interpolation

// catalog: "Hello, {name}"
fx.t("greeting", { name: "Ada" }); // → "Hello, Ada"

Values may be string, number, boolean, Date, null / undefined, or a rich-text function (below). Missing args leave the source string unformatted.

Plurals (cardinal)

// en: "{count, plural, =0 {no items} one {# item} other {# items}}"
fx.t("items", { count: 0 }); // → "no items"
fx.t("items", { count: 1 }); // → "1 item"
fx.t("items", { count: 5 }); // → "5 items"

Arabic cardinals

Arabic (ar) uses six CLDR cardinal categories. FormatJS picks the branch from fx.locale — an English one/other skeleton on ar misfires for dual, paucal, and hundreds.

CategoryWhen (integers)Typical form
zeron = 0No items / special zero phrasing
onen = 1Singular
twon = 2Dual
fewn % 100 in 3…10 (also 103…110 …)Paucal — often sound plural
manyn % 100 in 11…99Accusative / “tamyīz” style counts
other100…102, 200…202, … and fractionsGeneral plural / leftover integers

Write every branch on the Arabic catalog (starter items key):

// ar catalog
items: "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصراً} other {# عنصر}}";
// fx.locale === "ar"
fx.t("items", { count: 0 }); // → "لا عناصر"      (zero)
fx.t("items", { count: 1 }); // → "عنصر واحد"     (one)
fx.t("items", { count: 2 }); // → "عنصران"        (two)
fx.t("items", { count: 5 }); // → "5 عناصر"       (few)
fx.t("items", { count: 11 }); // → "11 عنصراً"     (many)
fx.t("items", { count: 100 }); // → "100 عنصر"      (other)
fx.t("items", { count: 103 }); // → "103 عناصر"     (few — 103 % 100 = 3)

Consequence: copy the six-way shape for Arabic noun counts; do not reuse an English one/other skeleton. # still inserts the number inside a branch.

Ordinals (selectordinal)

// "You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}!"
fx.t("place", { place: 1 }); // → "You finished 1st!"
fx.t("place", { place: 11 }); // → "You finished 11th!"

Select (enums)

// "{status, select, online {Online} offline {Offline} other {Unknown}}"
fx.t("status", { status: "online" }); // → "Online"
fx.t("status", { status: "away" }); // → "Unknown"

Rich-text tags

Tags in the message become function values. The function receives the formatted inner chunks and returns a string (HTML, Markdown, plain wrappers):

// catalog: "Read <docs>the docs</docs>"
fx.t("cta", {
  docs: (chunks) => `<a href="/docs">${chunks.join("")}</a>`,
});
// → 'Read <a href="/docs">the docs</a>'

Escaping

NeedWrite
Apostrophe in copyDouble it: this flow''s effects
Literal { / } in outputQuote the braces: '{'optional: true'}'
Channel-style {{field}}Not ICU — use Channel catalogs, not fx.t

Malformed ICU falls back to the raw catalog string (no throw from fx.t).

Typed keys

Augment Register with your English tree so fx.t autocompletes and rejects typos. Keep other locales aligned with satisfies MessagesFor<typeof en>.

HelperRole
defineMessagesPreserve a const English (or canonical) tree
defineLocaleRegister / replace a locale's flat catalog
MessagesFor<T>Same key shape as T; leaf values are strings
AppMessageKeyFlattened key union once Register is augmented

Built-in failure catalogs

English and Arabic ship for typed failures and OKE codes — no app registration required:

SurfaceKeysAppears as
fx.fail / failerrors.{code} · errors.{code}.{reason}Optional error.message
Thrown OkeErroroke.{code}.cause · oke.{code}.fixCause + fix lines in the message

Override any key with defineLocale. Pass fail(code, data, { message }) (or fx.t(...)) when you need a one-off string. Custom app codes stay message-less until registered. Full tables: Errors.

Channel catalogs are separate

fx.send templates use {{field}} bodies and their own locales list — not ICU. Omit locale / profileLocale / acceptLanguage on fx.send and the send uses fx.locale. Details: Channel.

Troubleshooting

Learn more

  • fx — full fx surface including fx.t / fx.locale
  • Errors — localized failure messages and OKE codes
  • Configurationi18n block next to drivers
  • Channel{{field}} templates and locale chain
  • Flow — envelope shape with optional error.message

Next

On this page