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

#Localization

Your app works great. In English. Now product wants French, German, and Japanese by next quarter.

Alepha's i18n system handles translations, date formatting, number formatting, and language switching. No external libraries to configure. No build-time extraction. Just define your strings and use them.

#Defining Translations

Use $dictionary to register translations. Each dictionary is a language:

typescript
 1import { $dictionary } from "@alepha/react/i18n"; 2  3class I18n { 4  en = $dictionary({ 5    lang: "en", 6    lazy: async () => ({ 7      default: { 8        "header.title": "My App", 9        "header.login": "Sign In",10        "header.logout": "Log Out",11        "greeting": "Hello, $1!",12      },13    }),14  });15 16  // even better with dynamic imports17  fr = $dictionary({18    lazy: () => import("./locales/fr.ts"),19  });20}

The property name (en, fr) becomes the language code by default. Override with the lang option if needed.

#Why Lazy Loading?

Each $dictionary is lazy-loaded on demand. If a user's language is French, only the French dictionary is downloaded. English stays on the server.

This matters when you have 20 languages and 500 translation keys. Instead of shipping a 200KB JSON blob with every language, users download only what they need — typically 10-15KB for their language.

When the user switches languages, Alepha fetches the new dictionary automatically. Already-loaded dictionaries are cached, so switching back is instant.

#String Interpolation

Use $1, $2, etc. for dynamic values:

typescript
1{2  "greeting": "Hello, $1!",3  "items.count": "You have $1 items in your cart",4  "transfer": "Transfer $1 to $2",5}

#Using Translations

The useI18n hook gives you access to translations:

tsx
 1import { useI18n } from "@alepha/react/i18n"; 2  3const Header = () => { 4  const { tr } = useI18n<I18n, "en">(); // <- weird syntax 5  6  return ( 7    <header> 8      <h1>{tr("header.title")}</h1> 9      <button>{tr("header.login")}</button>10    </header>11  );12};

The generic parameters <I18n, "en"> give you autocomplete for translation keys. The second parameter is your default language — it's used for type inference.

#With Arguments

Pass dynamic values using the args option:

tsx
1const Greeting = ({ name }: { name: string }) => {2  const { tr } = useI18n<I18n, "en">();3 4  return <p>{tr("greeting", { args: [name] })}</p>;5  // "Hello, John!" or "Bonjour, John !"6};

#Fallback Values

If a key is missing, provide a default:

tsx
1{tr("some.missing.key", { default: "Fallback text" })}

If no translation exists, the key itself is returned. Useful for spotting missing translations in development.

#Switching Languages

The i18n provider exposes setLang and lang:

tsx
 1import { useI18n } from "@alepha/react/i18n"; 2  3const LanguageSwitcher = () => { 4  const { lang, setLang, languages } = useI18n<I18n, "en">(); 5  6  return ( 7    <select value={lang} onChange={(e) => setLang(e.target.value)}> 8      {languages.map((code) => ( 9        <option key={code} value={code}>10          {code.toUpperCase()}11        </option>12      ))}13    </select>14  );15};

Language preference is stored in a cookie. Users get their preferred language on return visits.

#SSR-Friendly

Here's the thing most i18n libraries get wrong: they store the language in localStorage or React state. That means the server renders in the default language, then the client hydrates and flashes to the correct language. Ugly.

Alepha stores the language in a cookie. The server reads it on every request and renders the page in the correct language from the start. No flash. No hydration mismatch. The user sees their language immediately, even before JavaScript loads.

#Localizing Values

The l() function formats dates, numbers, and errors according to the current locale:

tsx
 1const { l } = useI18n<I18n, "en">(); 2  3// Numbers 4l(1234.56)                              // "1,234.56" (en) or "1 234,56" (fr) 5l(0.75, { number: { style: "percent" }}) // "75%" 6l(99.99, { number: { style: "currency", currency: "EUR" }}) // "€99.99" 7  8// Dates 9l(new Date())                           // "1/15/2024" (en) or "15/01/2024" (fr)10l(new Date(), { date: "YYYY-MM-DD" })   // "2024-01-15"11l(new Date(), { date: "MMMM D, YYYY" }) // "January 15, 2024"12l(new Date(), { date: "fromNow" })      // "2 hours ago"13 14// With timezone15l(new Date(), { date: "HH:mm", timezone: "America/New_York" })

#Date Format Options

The date option accepts:

Format Example Output
"fromNow" "2 hours ago", "in 3 days"
"YYYY-MM-DD" "2024-01-15"
"MMMM D, YYYY" "January 15, 2024"
"LLL" "January 15, 2024 2:30 PM"
"dddd" "Monday"
Intl options { dateStyle: "full" }

Alepha uses dayjs under the hood. See the dayjs format docs for all options.

#Real-World Example

Here's a complete i18n setup:

typescript
 1// src/web/app/services/I18n.ts 2import { $dictionary } from "@alepha/react/i18n"; 3  4export class I18n { 5  en = $dictionary({ 6    lazy: async () => ({ 7      default: { 8        "app.name": "Task Manager", 9 10        "nav.home": "Home",11        "nav.tasks": "Tasks",12        "nav.settings": "Settings",13 14        "tasks.empty": "No tasks yet. Create one!",15        "tasks.count": "$1 tasks",16        "tasks.create": "New Task",17        "tasks.delete.confirm": "Delete this task?",18 19        "settings.language": "Language",20        "settings.theme": "Theme",21        "settings.save": "Save Changes",22 23        "common.cancel": "Cancel",24        "common.confirm": "Confirm",25        "common.loading": "Loading...",26      },27    }),28  });29 30  fr = $dictionary({31    lazy: async () => ({32      default: {33        "app.name": "Gestionnaire de Tâches",34 35        "nav.home": "Accueil",36        "nav.tasks": "Tâches",37        "nav.settings": "Paramètres",38 39        "tasks.empty": "Aucune tâche. Créez-en une !",40        "tasks.count": "$1 tâches",41        "tasks.create": "Nouvelle Tâche",42        "tasks.delete.confirm": "Supprimer cette tâche ?",43 44        "settings.language": "Langue",45        "settings.theme": "Thème",46        "settings.save": "Enregistrer",47 48        "common.cancel": "Annuler",49        "common.confirm": "Confirmer",50        "common.loading": "Chargement...",51      },52    }),53  });54}

Use it in components:

tsx
 1import { useI18n } from "@alepha/react/i18n"; 2import type { I18n } from "../services/I18n"; 3  4const TaskList = ({ tasks }: { tasks: Task[] }) => { 5  const { tr, l } = useI18n<I18n, "en">(); 6  7  if (tasks.length === 0) { 8    return <p>{tr("tasks.empty")}</p>; 9  }10 11  return (12    <div>13      <h2>{tr("tasks.count", { args: [String(tasks.length)] })}</h2>14      {tasks.map((task) => (15        <div key={task.id}>16          <span>{task.title}</span>17          <span>{l(task.createdAt, { date: "fromNow" })}</span>18        </div>19      ))}20    </div>21  );22};

#Tips

  1. Use namespaced keys"header.login" beats "loginButton". Easier to find and organize.
  2. Keep translations flat — Nested objects aren't supported. Use dot notation in keys.
  3. Type your i18n class — The useI18n<I18n, "en">() generic gives you autocomplete and catches typos.
  4. Lazy load everything — Translations are loaded on demand. Don't inline them.
  5. Test with a fake language — Add a "pseudo" locale that wraps strings in brackets: "[Login]". Missing translations become obvious.

Internationalization is tedious. But at least Alepha makes the plumbing invisible.

On This Page
No headings found...
ready
mainTypeScript
UTF-8guides_frontend_localization.md