Skip to main content

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 through TrackunitProviders.

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:

  1. Create one JSON file per language under src/locales/.
  2. Declare a namespaced, type-safe translation resource in src/translation.tsx.
  3. Register that resource with TrackunitProviders in your app entry point.
  4. Read strings in your components with the local useTranslation hook (and Trans for 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:

  • namespace must be unique across every published app, so your keys never collide with another app's. Use a your-company.your-app style value — for example acme.fleet-insights.
  • TranslationKeys is derived from your English file, so useTranslation and Trans only accept keys that actually exist. Adding a key to en/translation.json makes it available with full type-safety.
  • default is your English resource; it is bundled eagerly. Other languages are loaded lazily via the dynamic import() entries under languages.

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.title or actions.save.
  • Avoid building translation keys dynamically; static keys keep type-safety and let tooling detect missing translations.
  • Use the local useTranslation hook exported from your app's translation.tsx, not i18next hooks directly.
  • Do not initialize i18next manually when using TrackunitProviders.

Test your translations

To verify localization while developing:

  1. Serve your app and open it in Trackunit Manager.
  2. Change your user's language (Manager applies the host language to your app), or switch the browser/host locale, and confirm the UI updates.
  3. Check that every visible string changes — any string still in English is likely hardcoded in a component rather than read through t()/Trans.
  4. 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.json complete 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.