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.

Play
Transcript

00:00 So one thing I really like about Astryx design system is that internationalization, localization is built right into it. It’s a top priority. You are able to use, you know, I18n-Next or React internationalization, whatever you want to use. But it comes built in with its own, which is recommended for like a small project, which we certainly qualify for, but maybe not for a large project. So I just want to point that out. But here in my source apps, I’m going to create a messages.ts. And we are going to bring in catalog and messages by locale from Astryx DesignCore i18n. So it’s built right in. You already have it. You might as well use it.

00:48 We’re going to set up our catalog. So this is our English catalog. It’s important to note that each of these has kind of a prefix namespace. So mine is going to be at app, but it can be whatever you want, followed by, you know, board titles, switch language and tasks in my case. But these could be whatever you want. They can be broken down as much as you like. So we’re going to be doing English and French. So on my app switch language, I have in French, but it’s in French. So I don’t, I’m not going to try to pronounce any French in this lesson. Yeah, I’m not going to do that. So then we’re going to have our second catalog of messages, which is going to be in French and therefore on the switch language, which is going to be a little toggle we’re going to create. It’s going to say in English. And then all we need to do is export that. I’m going to call it messages, messages by locale, and we send off the EN and the FR values or objects that we created.

01:42 So we’re going to need to add this to our provider. So we’re going to jump into our providers. I’m going to replace this just import type react node. I’m going to bring in create context, use context and use state. From Astryx, I’m going to bring in internationalization provider. Again, it’s coming directly from that i18n package or library within the core library. And then lastly, we’re going to import messages from the messages file we just created. Okay, I’m going to set up a quick couple of types here. We’re going to have a theme mode for system light and dark. So that’s your toggling through, you know, your accessibility, your light and dark mode, whatever you want to call it. And then we’ve got an enum of, or a union of EN and FR for app locale. We’re going to have an interface for app preferences. So we’ve got our mode and set mode. And we’ve got our locale and set locale. And we’re going to initialize our app preferences context with create context and pass in our app preferences are null. We’re going to set up a quick guardrail, use preferences. It’s simply making sure that we have the context of app preferences context. Otherwise, it’s going to throw an error that you have to use this inside a provider so that we know we have access to that context.

03:11 Right here inside our function, let me scroll this up a bit. We’re going to have a set state for our mode as well as for our locale. So we can update this mode to be the mode from our set state. And then here inside our theme, we’re going to have our internationalization provider. We’ll close that down here. This guy is going to get our locale and our messages. And then actually, yeah, AI is doing its job. So our app preferences context provider, which we created right here, is going to take in the values of mode, set mode, locale, and set locale so that we can use those inside of our application. So we’ll save that. We’re going to jump back over to our Kanban board.

04:05 We are going to bring in button, another standard component from the Astryx design system. And we’re going to bring in use translator. So if you’ve ever used, like I’m very familiar with IET Next, but any of those libraries, there’s usually a hook of this nature where traditionally by convention, you’re going to say something. You’re going to say T equals use the hook. And then throughout your application, you can translate or wrap it in that T function. So that T function becomes the translator that’s aware of the current context and updates it to the appropriate key or a fallback, which in our case is English. And we’re going to bring in our use preferences hook that we just created from providers. So right here, we’re going to set up our use translator followed by our locale and set locale from our use preferences and jump down here where we have sprint board. We’re going to replace that with our T function and we just pass in our key. So app.board.title. So now that’s going to respond to our use preferences values of locale and set locale.

05:18 I’m going to kill this text component. We’re going to add a new age stack. And inside of that, we’re going to drop a text. Now we’re outputting four tasks, whatever the English versus French version of that is. But I do want to point out this has tabular numbers property. It’s really cool. I think what they say is it keeps it from jittering when the value changes. So it knows that you have a tabular number here, that number may change and you can get some sort of layout shifts or whatever. But it’s also super cool for language switching. So certain languages numbers are very, very different, taller, wider, all sorts of things of that nature. This keeps an eye on that and keeps it under control. And then after that, we’re going to have a button. It’s got our switch language, a variant, and then an on click of set locale. So we’re just going to toggle back and forth between English and French. So we’ll save that and jump over to our application.

06:24 So we can see we have, we are looking at English right now. This text and this text, I believe, are what’s going to change. I click on that and now it’s in French. Amazing. Now it’s in English. Now it’s in French. So again, internationalization, localization seems to be really important to the Astryx design system. That makes me really happy. It should make you really happy. It’s super easy to implement and get started with. Yeah, that’s awesome. And that’s internationalization in the Astryx design system.

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