alepha@docs:~/docs/guides/frontend$
cat 5-localization.md
3 min read
Last commit:

#Localization

Alepha provides $dictionary for registering translations and useI18n for accessing them in React components. Translations are lazy-loaded per language and support SSR with cookie-based language detection.

#Setup

Register translation dictionaries as class properties. The property name determines the language code.

typescript
1import { $dictionary } from "alepha/react/i18n";2 3class App {4  en = $dictionary({ lazy: () => import("./translations/en.ts") });5  fr = $dictionary({ lazy: () => import("./translations/fr.ts") });6}

Each translation file exports a default object mapping keys to strings:

typescript
1// translations/en.ts2export default {3  hello: "Hello",4  welcome: "Welcome, $1",5  items_count: "$1 items",6};
typescript
1// translations/fr.ts2export default {3  hello: "Bonjour",4  welcome: "Bienvenue, $1",5  items_count: "$1 elements",6};

Interpolation uses positional placeholders: $1, $2, $3, etc.

#$dictionary Options

Option Type Description
lazy () => Promise<{ default: T }> Async loader for the translation module.
lang string Language code. Defaults to the property name.
name string Dictionary name. Defaults to the property name.

#Multiple Dictionaries Per Language

You can split translations across files by using the lang option:

typescript
 1class App { 2  enCommon = $dictionary({ 3    lang: "en", 4    lazy: () => import("./translations/en-common.ts"), 5  }); 6  7  enDashboard = $dictionary({ 8    lang: "en", 9    lazy: () => import("./translations/en-dashboard.ts"),10  });11}

All dictionaries for the same language are merged. Key lookups search all dictionaries for the current language.

#Lazy Loading

Only the current language and the fallback language (default: "en") are loaded on startup. When the user switches languages, the new language's dictionaries are loaded on demand.

On the server, all languages are loaded at startup for SSR.

#useI18n Hook

typescript
1import { useI18n } from "alepha/react/i18n";

The hook accepts two type parameters for full type safety: the class containing the dictionaries and one of its dictionary keys (to infer available translation keys).

typescript
 1function Header() { 2  const { tr, l, setLang, lang, languages } = useI18n<App, "en">(); 3  4  return ( 5    <div> 6      <h1>{tr("hello")}</h1> 7      <p>Current: {lang}</p> 8      <select value={lang} onChange={(e) => setLang(e.target.value)}> 9        {languages.map((l) => (10          <option key={l} value={l}>{l}</option>11        ))}12      </select>13    </div>14  );15}

#tr(key, options?)

Translate a key. Returns the translated string, or the key itself if not found.

typescript
1tr("hello")2// "Hello"3 4tr("welcome", { args: ["Alice"] })5// "Welcome, Alice"6 7tr("missing_key", { default: "Fallback text" })8// "Fallback text"

Options:

Option Type Description
args string[] Positional arguments for $1, $2, etc.
default string Fallback if the key is not found.

Fallback behavior: if the key is not found in the current language, the fallback language ("en" by default) is checked. If still not found, the raw key string is returned.

#l(value, options?)

Localize a value (date, number, or error) according to the current locale.

Number formatting:

typescript
1l(1234.56)2// "1,234.56" (en) / "1 234,56" (fr)3 4l(1234.56, { number: { style: "currency", currency: "USD" } })5// "$1,234.56"6 7l(0.85, { number: { style: "percent" } })8// "85%"

Uses Intl.NumberFormat under the hood. The number option accepts standard Intl.NumberFormatOptions.

Date formatting:

typescript
 1l(new Date()) 2// "2/7/2026" (en) / "07/02/2026" (fr) 3  4l(new Date(), { date: "LLL" }) 5// "February 7, 2026 10:30 AM" (dayjs format string) 6  7l(new Date(), { date: "fromNow" }) 8// "2 hours ago" 9 10l(new Date(), { date: { year: "numeric", month: "long", day: "numeric" } })11// "February 7, 2026" (Intl.DateTimeFormatOptions)12 13l(new Date(), { date: "LLL", timezone: "America/New_York" })14// Formatted in Eastern time

The date option accepts:

  • A dayjs format string (e.g., "LLL", "YYYY-MM-DD", "dddd, MMMM D YYYY")
  • "fromNow" for relative time
  • Intl.DateTimeFormatOptions for native formatting

The timezone option accepts IANA timezone names.

TypeBox error localization:

typescript
1l(typeBoxError)2// Localized validation error message

#setLang(lang)

Switch the current language. On the browser, this:

  1. Loads the new language's dictionaries if not already loaded
  2. Sets a lang cookie (persists for 1 year)
  3. Updates the store, causing all useI18n consumers to re-render
typescript
1setLang("fr")

#lang

The current language code string.

#languages

Array of all available language codes, derived from registered dictionaries.

#Localize Component

A component wrapper around l() for inline use in JSX:

typescript
1import { Localize } from "alepha/react/i18n";2 3<Localize value={new Date()} date="LLL" />4<Localize value={1234.56} number={{ style: "currency", currency: "EUR" }} />5<Localize value={new Date()} date="fromNow" timezone="Europe/Paris" />

Props:

Prop Type Description
value string | number | Date | DateTime | TypeBoxError The value to localize.
number Intl.NumberFormatOptions Number formatting options.
date string | Intl.DateTimeFormatOptions Date formatting options.
timezone string IANA timezone name.

#SSR Language Detection

On the server, the language is determined by:

  1. The lang cookie value (set by setLang)
  2. Falls back to the configured fallback language ("en" by default)

The language state is set on each request via a server hook, so SSR renders use the correct language.

#Configuration

The fallback language defaults to "en". It can be changed on the I18nProvider:

typescript
1const i18n = alepha.inject(I18nProvider);2i18n.options.fallbackLang = "es";