Skip to content

Mastering Astryx / lesson 3 of 13

Internationalization & Localization (i18n)

Localize the board with Astryx's i18n system: locale catalogs, useTranslator, runtime language swapping, and typography that stays aligned.

Internationalization is baked into the foundation of Astryx. The library ships a complete i18n system inside @astryxdesign/core — there is nothing extra to install. You wrap your app in an InternationalizationProvider, point it at a locale, and Astryx components pick up localized strings from that provider.

Files this lesson: src/app/messages.ts (new), src/app/providers.tsx (extended), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.

Message Catalogs & the Translator

A catalog is a plain object mapping message keys to messages. Each entry uses the shape { defaultMessage, description? }. Keys should be namespaced — keep your app keys in your own namespace (e.g. @app.*) so they never collide with Astryx’s built-in @astryx.* strings.

Create src/app/messages.ts:

src/app/messages.ts
import type { Catalog, MessagesByLocale } from '@astryxdesign/core/i18n';
export const en: Catalog = {
'@app.board.title': { defaultMessage: 'Sprint Board' },
'@app.switchLanguage': { defaultMessage: 'En français' },
'@app.tasks': { defaultMessage: 'tasks' },
};
export const fr: Catalog = {
'@app.board.title': { defaultMessage: 'Tableau de sprint' },
'@app.switchLanguage': { defaultMessage: 'In English' },
'@app.tasks': { defaultMessage: 'tâches' },
};
// MessagesByLocale types the map you pass to the provider's `messages` prop.
export const messages: MessagesByLocale = { en, fr };

Include your own en catalog even though English is the built-in fallback: that fallback only contains Astryx’s own component strings, not your app’s. If a key is missing from the active locale, Astryx walks the locale chain (for example pt-BRpt → built-in en) before giving up.

Reading Strings with useTranslator

Component authors resolve strings with the useTranslator() hook rather than hardcoding user-facing text:

import { useTranslator } from '@astryxdesign/core/i18n';
function BoardHeader() {
const t = useTranslator();
return <h1>{t('@app.board.title')}</h1>;
}

t() is just a function — call it anywhere in a component rendered under the provider.

The Preferences Context: State That Lives Above the Board

The language toggle button has to live inside the board, but the locale state has to live above the board — inside the providers, because that is where InternationalizationProvider sits. Rather than drilling callbacks through every layer, we add a small context. This same context will carry the light/dark mode in lesson 5, so we build it fully now.

Extend src/app/providers.tsx:

src/app/providers.tsx
'use client';
import type { ReactNode } from 'react';
import {
createContext,
useContext,
useState,
type ReactNode,
} from 'react';
import { Theme } from '@astryxdesign/core';
import { neutralTheme } from '@astryxdesign/theme-neutral/built';
import { InternationalizationProvider } from '@astryxdesign/core/i18n';
import { messages } from './messages';
export type ThemeMode = 'system' | 'light' | 'dark';
export type AppLocale = 'en' | 'fr';
interface AppPreferences {
mode: ThemeMode;
setMode: (mode: ThemeMode) => void;
locale: AppLocale;
setLocale: (locale: AppLocale) => void;
}
const AppPreferencesContext = createContext<AppPreferences | null>(null);
export function usePreferences() {
const ctx = useContext(AppPreferencesContext);
if (!ctx) {
throw new Error('usePreferences must be used inside <Providers>');
}
return ctx;
}
export function Providers({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<ThemeMode>('system');
const [locale, setLocale] = useState<AppLocale>('en');
return (
<Theme theme={neutralTheme} mode="system">
<Theme theme={neutralTheme} mode={mode}>
<InternationalizationProvider locale={locale} messages={messages}>
<AppPreferencesContext.Provider
value={{ mode, setMode, locale, setLocale }}
>
{children}
</AppPreferencesContext.Provider>
</InternationalizationProvider>
</Theme>
);
}

Using It: The Board Header

Rewrite src/app/components/KanbanBoard.tsx to read the localized title, show a live task counter with tabular numbers, and carry a language toggle button:

src/app/components/KanbanBoard.tsx
'use client';
import { Section } from '@astryxdesign/core/Section';
import {
Layout,
LayoutHeader,
LayoutContent,
HStack,
} from '@astryxdesign/core/Layout';
import { Heading, Text } from '@astryxdesign/core/Text';
import { Card } from '@astryxdesign/core/Card';
import { Button } from '@astryxdesign/core/Button';
import { useTranslator } from '@astryxdesign/core/i18n';
import { usePreferences } from '../providers';
export default function KanbanBoard() {
const t = useTranslator();
const { locale, setLocale } = usePreferences();
return (
<Section height="100dvh">
<Layout
height="fill"
header={
<LayoutHeader hasDivider padding={4}>
<HStack hAlign="between" vAlign="center">
<Heading level={3}>Sprint Board</Heading>
<Heading level={3}>{t('@app.board.title')}</Heading>
<Text type="supporting" color="secondary">
Layout shell — real columns arrive in lesson 6
</Text>
<HStack gap={2} vAlign="center">
{/* Tabular numbers keep digits from jittering as counts change */}
<Text type="supporting" color="secondary" hasTabularNumbers>
4 {t('@app.tasks')}
</Text>
<Button
label={t('@app.switchLanguage')}
variant="secondary"
onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}
/>
</HStack>
</HStack>
</LayoutHeader>
}
content={
<LayoutContent padding={4}>
<HStack gap={4}>
{[0, 1, 2, 3].map(i => (
<Card
key={i}
variant="muted"
padding={3}
width={300}
style={{ flexShrink: 0 }}
>
<Heading level={4}>Column {i + 1}</Heading>
<Text type="supporting" color="secondary">
Placeholder — data arrives in lesson 6.
</Text>
</Card>
))}
</HStack>
</LayoutContent>
}
/>
</Section>
);
}

Typography That Holds Up Across Languages

See It in Action

Run npm run dev and open http://localhost:3000. The header reads Sprint Board with a task counter. Click En français — the title flips to Tableau de sprint, the counter label to tâches, and the button label to In English, all without a reload. Because locale lives in the providers above the board, every component under the tree would re-localize together.

What You Built

In the next lesson, we will master styling components with StyleX and the xstyle prop.


Share this post on:

Previous
Layout & Spatial Composition
Next
Styling Components & StyleX Integration