> ## Documentation Index
> Fetch the complete documentation index at: https://docs.levelrail.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Internationalization (i18n)

> How the dashboard's i18n works for contributors: where strings live, how to add one to a component, and how TypeScript catches a mistyped key.

This page is for contributors to the dashboard. New user-facing strings go through i18n, but only English exists today, there is no language switcher, and many older components still hardcode English. Migrate a component when you touch it. No blanket sweep is planned.

The stack is `react-i18next` with `i18next`, in the plain Vite and React app. It gives you plain `t('key')` calls with no extraction step, namespace-based lazy loading, and typed keys.

## Where strings live

Translations are in `web/src/locales/<language>/<namespace>.json`. A namespace maps to a feature area, not to one component: `deploys.json` holds keys for several deploy-settings cards. The current namespaces are `common`, `deploys`, `settings`, `dashboard`, `auditLog`, `streams`, `networkProxy`, `databases`, `https` and `nodes`.

Nest keys by component inside the file, and name a key for what the string is for, not its English text:

```json theme={"dark"}
{
  "autoRollback": {
    "title": "Auto-rollback on crashloop",
    "enabledLabel": "Enabled",
    "toast": {
      "enabled": "Auto-rollback on crashloop enabled.",
      "errorTitle": "Could not update auto-rollback."
    }
  }
}
```

A new feature area gets its own namespace file. Don't grow an unrelated one.

## Add a string to a component

<Steps>
  <Step title="Add the key to the JSON file">
    Find or create the namespace file under `web/src/locales/en/` and add the key first. The JSON file is the type schema, so a call site cannot use a key that is not there.
  </Step>

  <Step title="Call t() in the component">
    ```tsx theme={"dark"}
    import { useTranslation } from 'react-i18next'

    export function SomeCard() {
      const { t } = useTranslation('deploys')
      return <p>{t('someFeature.someLabel')}</p>
    }
    ```

    For interpolation, use `{{placeholder}}` in the value and pass the data as the second argument: `t('autoRollbackSloBurn.toast.modeSet', { mode: label })`. For strings from more than one namespace, pass an array to `useTranslation` and prefix the non-default key, for example `t('common:actions.copy')`.
  </Step>

  <Step title="Cover every user-facing string">
    That includes `aria-label` values, toast titles and descriptions, tooltip text and `alt` text. Test ids, internal log messages and class names do not belong in a namespace file.
  </Step>

  <Step title="Check types">
    From `web/`, run `npx tsc -b` and `npx eslint src`. A missing or mistyped key fails `tsc`.
  </Step>
</Steps>

### Adding a new namespace

Create the JSON file, then register it in two places: add the name to `NAMESPACES` in `web/src/i18n/index.ts` and add its type to `web/src/i18n/resources.ts`.

## How loading works

Each namespace is its own chunk. `web/src/i18n/lazyBackend.ts` imports `../locales/${language}/${namespace}.json` with a fixed directory and extension, which lets Vite split every JSON file out of the main bundle. A namespace downloads the first time a component calls `useTranslation` for it. Only `common`, the default namespace, loads at boot.

Because `useSuspense` is on, a component suspends while its namespace loads. `web/src/main.tsx` wraps the app in one `<Suspense fallback={<PageSpinner />}>`, so this looks like any other route loading state. If you render a component outside that tree, make sure an ancestor has a `Suspense` boundary.

## Type-safe keys

`web/src/i18n/resources.ts` augments i18next's `CustomTypeOptions` with the type of each `en` JSON file:

```ts theme={"dark"}
declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common'
    resources: {
      common: typeof common
      deploys: typeof deploys
      // one entry per namespace
    }
  }
}
```

With that in place, `t('autoRollback.titl')` is a `tsc` error at the call site instead of a silent fallback to the raw key at runtime. This needs `"resolveJsonModule": true` in `tsconfig.app.json`, which is set.

## Testing

Test a translated component with a dedicated `i18next.createInstance()` seeded with the real namespace JSON through `I18nextProvider`, not the app's lazy-loading singleton. Tests then get deterministic, already-loaded translations with no chunk-loading race. `web/src/components/AutoRollbackCard.test.tsx` shows the pattern.

## Not done yet

* No second language. Add one only when there is a real reason to ship it.
* No language switcher. `i18next-browser-languagedetector` is wired in and `supportedLngs` is `['en']`, so adding a language and a switcher later won't need another migration of call sites.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.