Localize the UI
This guide shows how to add multilingual (i18n) support to an IrisX App so its UI is displayed in the language of the person using it. It uses Trackunit's i18n utilities and requires no manual i18next setup.
The setup relies on two SDK packages:
@trackunit/i18n-library-translation— namespaced, type-safe translations.@trackunit/react-core-contexts— initializes translations throughTrackunitProviders.
Translations live in JSON files under src/locales/<language>/translation.json. English (en) is the required fallback language — every key must exist in English, and any language you do not provide falls back to it.
Overview
At a high level you will:
- Create one JSON file per language under
src/locales/. - Declare a namespaced, type-safe translation resource in
src/translation.tsx. - Register that resource with
TrackunitProvidersin your app entry point. - Read strings in your components with the local
useTranslationhook (andTransfor rich text).
For where these files sit inside the workspace, see Project Structure.
File structure
Add a locales folder next to your app entry point, with one folder per language:
src/
index.tsx
translation.tsx
locales/
en/
translation.json
da/
translation.json
de/
translation.json
en is required as the fallback. Add other locale folders only for the languages your app supports.
Create translation files
Each language file is a flat JSON object of translation keys to strings. Use explicit, dot-separated keys such as page.title or actions.save.
Create src/locales/en/translation.json:
{
"page.title": "My Iris App",
"page.description": "This app demonstrates localized content.",
"actions.save": "Save"
}
Then add a file per additional language with the same keys. For example, src/locales/da/translation.json:
{
"page.title": "Min Iris App",
"page.description": "Denne app demonstrerer lokaliseret indhold.",
"actions.save": "Gem"
}
Set up translations
Create src/translation.tsx. This file declares your namespace, wires up the translation resource, and exports a type-safe useTranslation hook and Trans component scoped to your app:
import {
NamespaceTrans,
NamespaceTransProps,
TransForLibs,
TranslationResource,
useNamespaceTranslation,
} from '@trackunit/i18n-library-translation';
import defaultTranslations from './locales/en/translation.json';
export type TranslationKeys = keyof typeof defaultTranslations;
export const namespace = 'your-company.your-app';
export const translations: TranslationResource<TranslationKeys> = {
ns: namespace,
default: defaultTranslations,
languages: {
da: () => import('./locales/da/translation.json'),
de: () => import('./locales/de/translation.json'),
},
};
export const useTranslation = () => useNamespaceTranslation<TranslationKeys>(namespace);
export type TranslationFunction = TransForLibs<TranslationKeys>;
export const Trans = (props: NamespaceTransProps<TranslationKeys>) => (
<NamespaceTrans<TranslationKeys> {...props} namespace={namespace} />
);
A few things to note:
namespacemust be unique across every published app, so your keys never collide with another app's. Use ayour-company.your-appstyle value — for exampleacme.fleet-insights.TranslationKeysis derived from your English file, souseTranslationandTransonly accept keys that actually exist. Adding a key toen/translation.jsonmakes it available with full type-safety.defaultis your English resource; it is bundled eagerly. Other languages are loaded lazily via the dynamicimport()entries underlanguages.
Register translations
Pass the translation resource to TrackunitProviders in your app entry point. TrackunitProviders registers the translations and initializes i18next for you — see Trackunit React Providers.
Example src/index.tsx:
import '@trackunit/css-core';
import { TrackunitProviders } from '@trackunit/react-core-contexts';
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import singleSpaReact from 'single-spa-react';
import { App } from './App';
import { translations } from './translation';
const RootComponent = () => (
<TrackunitProviders translations={translations}>
<App />
</TrackunitProviders>
);
const lifeCycle = singleSpaReact({
React,
ReactDOMClient,
rootComponent: RootComponent,
errorBoundary() {
return <div>Something went wrong.</div>;
},
});
export const bootstrap = lifeCycle.bootstrap;
export const mount = lifeCycle.mount;
export const unmount = lifeCycle.unmount;
Do not initialize i18next yourself when using TrackunitProviders — it is handled for you.
Use translations in components
Use the local useTranslation hook exported from src/translation.tsx. It returns a tuple whose first element is the translation function t:
import { useTranslation } from './translation';
export const App = () => {
const [t] = useTranslation();
return (
<main>
<h1>{t('page.title')}</h1>
<p>{t('page.description')}</p>
<button type="button">{t('actions.save')}</button>
</main>
);
};
Because the hook is typed against your English keys, t('page.titel') (a typo) fails to compile — a missing translation is caught at build time, not in production.
Translate rich text
When a translated string contains markup, use the local Trans component instead of t(). Put the markup directly in the translation value:
{
"help.description": "Read the <strong>documentation</strong> before continuing."
}
import { Trans } from './translation';
export const HelpText = () => (
<p>
<Trans i18nKey="help.description" />
</p>
);
Translate manifest and custom field labels
Some IrisX App SDK configuration accepts a translation key instead of a hardcoded label. Where supported, provide a key so those labels are localized too:
{
name: {
key: 'page.title',
},
}
The SDK resolves these keys from the same files as your UI:
src/locales/<language>/translation.json
Prefer translation keys over hardcoded strings anywhere the manifest or a custom field definition supports them.
Supported languages
Add an entry under languages in src/translation.tsx for each locale your app ships. Each entry lazily imports that language's JSON file:
languages: {
da: () => import('./locales/da/translation.json'),
de: () => import('./locales/de/translation.json'),
fr: () => import('./locales/fr/translation.json'),
}
You do not list en here — it is always loaded as the default. If the active language is missing from languages, i18next falls back to English.
Best practices
- Keep all user-facing strings in translation files — never hardcode display text in components.
- Always provide English (
en) translations for every key. - Use a unique namespace for your app (
your-company.your-app). - Use explicit, static keys such as
page.titleoractions.save. - Avoid building translation keys dynamically; static keys keep type-safety and let tooling detect missing translations.
- Use the local
useTranslationhook exported from your app'stranslation.tsx, not i18next hooks directly. - Do not initialize i18next manually when using
TrackunitProviders.
Test your translations
To verify localization while developing:
- Serve your app and open it in Trackunit Manager.
- Change your user's language (Manager applies the host language to your app), or switch the browser/host locale, and confirm the UI updates.
- Check that every visible string changes — any string still in English is likely hardcoded in a component rather than read through
t()/Trans. - Temporarily remove a key from a non-English file and confirm that string falls back to English rather than showing the raw key.
Because keys are type-checked against your English file, a build (or a t() call to a non-existent key) fails before you ship, so most "missing translation" mistakes are caught at compile time.
How fallback works at runtime
Translations resolve within your app's namespace, in this order:
- Language present, key present — the translated string is used.
- Language missing (not listed under
languages, or its file failed to load) — i18next falls back to your English (default) resource. - Key missing in the active language but present in English — the English value is used for that key.
- Key missing everywhere — there is no English fallback to use, so the key itself may render. Always keep
en/translation.jsoncomplete to avoid this.
In short: English is the safety net. As long as every key exists in en, users always see readable text even for partially translated languages.
Troubleshooting
Nothing is translated / strings never change language
Make sure your app is wrapped in TrackunitProviders and that you passed translations to it in src/index.tsx. Without the provider, no namespace is registered and t() returns keys or defaults only.
Some strings translate, others do not
The untranslated strings are probably hardcoded in your components. Move them into the translation files and read them through t() or Trans.
A key renders as the literal key (for example page.title) instead of text
The key is missing from en/translation.json (so there is no fallback), or you are calling it from the wrong namespace. Confirm the key exists in English and that you import useTranslation/Trans from your app's src/translation.tsx.
Manifest or custom field labels are not localized
Confirm you used the { key: '...' } form (not a plain string) and that the key exists under src/locales/<language>/translation.json.
A whole language shows English
That language is not listed under languages in translation.tsx, or its dynamic import() path is wrong. Add or fix the entry so the locale file loads.
Types complain about a key
TranslationKeys is derived from en/translation.json. Add the key to the English file first; it then becomes available (with type-safety) to t() and Trans.