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.
Use $dictionary to register translations. Each dictionary is a language:
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.
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.
Use $1, $2, etc. for dynamic values:
1{2 "greeting": "Hello, $1!",3 "items.count": "You have $1 items in your cart",4 "transfer": "Transfer $1 to $2",5}
The useI18n hook gives you access to translations:
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.
Pass dynamic values using the args option:
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};
If a key is missing, provide a default:
1{tr("some.missing.key", { default: "Fallback text" })}
If no translation exists, the key itself is returned. Useful for spotting missing translations in development.
The i18n provider exposes setLang and lang:
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.
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.
The l() function formats dates, numbers, and errors according to the current locale:
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" })
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.
Here's a complete i18n setup:
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:
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};
"header.login" beats "loginButton". Easier to find and organize.useI18n<I18n, "en">() generic gives you autocomplete and catches typos."[Login]". Missing translations become obvious.Internationalization is tedious. But at least Alepha makes the plumbing invisible.