***

title: 'Localization'
metaTitle: "Localizing Vendure Dashboard Extensions with Lingui"
metaDescription: "Localize your Vendure Dashboard extensions with Lingui, and contribute UI translations of the Dashboard itself via a pull request."
-----------------------------------------------------------------------------------------------------------------------------------------------------

:::note
Support for localization of Dashboard extensions was added in v3.5.1
:::

The Dashboard uses [Lingui](https://lingui.dev/), which provides a powerful i18n solution for React:

* ICU MessageFormat support
* Automatic message extraction
* TypeScript integration
* Pluralization support
* Compile-time optimization

## Wrap your strings

First you'll need to wrap any strings that need to be localized:

```tsx
import { Trans, useLingui } from '@lingui/react/macro';

function MyComponent() {
    const { t } = useLingui();

    return (
        <div>
            <h1>
                <Trans>Welcome to Dashboard</Trans> {/* [!code highlight] */}
            </h1>
            <p>{t`Click here to continue`}</p> {/* [!code highlight] */}
        </div>
    );
}
```

You will mainly make use of the [Trans component](https://lingui.dev/ref/react#trans)
and the [useLingui hook](https://lingui.dev/ref/react#uselingui).

## Extract translations

Create a `lingui.config.js` file in your project root, with references to any plugins that need to be localized:

```js title="lingui.config.js"
import { defineConfig } from '@lingui/cli';

export default defineConfig({
    sourceLocale: 'en',
    // Add any locales you wish to support
    locales: ['en', 'de'],
    catalogs: [
        // For each plugin you want to localize, add a catalog entry
        {
            // This is the output location of the generated .po files
            path: '<rootDir>/src/plugins/reviews/dashboard/i18n/{locale}',
            // This is the pattern that tells Lingui which files to scan
            // to extract translation strings
            include: ['<rootDir>/src/plugins/reviews/dashboard/**'],
        },
    ],
});
```

Then extract the translations:

```bash
npx lingui extract
```

This will output the given locale files in the directories specified in the config file above.
In this case:

```
src/
└── plugins/
    └── reviews/
        └── dashboard/
            └── i18n/
                ├── en.po
                └── de.po
```

Since we set the "sourceLocale" to be "en", the `en.po` file will already be complete. You'll then need to
open up the `de.po` file and add German translations for each of the strings, by filling out the empty `msgstr` values:

```text title="de.po"
#: src/plugins/reviews/dashboard/review-list.tsx:51
msgid "Welcome to Dashboard"
msgstr "Willkommen zum Dashboard" # [!code highlight]
```

## Reducing merge conflicts in your .po files

The `#:` comment above each message records where the string was found, down to the line number. Those
line numbers are recalculated on every extraction, so any edit that shifts code up or down rewrites
them — adding one import at the top of a file changes the recorded line of every string below it, even
though none of the strings themselves changed.

If several people work on the same plugin, this can make the `.po` files a recurring source of merge
conflicts that have nothing to do with the translations. You can turn the line numbers off by passing an
explicit PO formatter to your config:

```js title="lingui.config.js"
import { defineConfig } from '@lingui/cli';
import { formatter } from '@lingui/format-po'; // [!code highlight]

export default defineConfig({
    sourceLocale: 'en',
    locales: ['en', 'de'],
    format: formatter({ lineNumbers: false }), // [!code highlight]
    catalogs: [
        {
            path: '<rootDir>/src/plugins/reviews/dashboard/i18n/{locale}',
            include: ['<rootDir>/src/plugins/reviews/dashboard/**'],
        },
    ],
});
```

The references then keep the file path and drop the line number, which is usually enough context for a
translator to find the string:

```text title="de.po"
#: src/plugins/reviews/dashboard/review-list.tsx
msgid "Welcome to Dashboard"
msgstr "Willkommen zum Dashboard"
```

The next `npx lingui extract` rewrites every reference in your catalogs, so expect one large diff when
you first make this change. After that the references only change when a string genuinely moves to a
different file.

:::note
`formatter` comes from `@lingui/format-po`, which is installed as part of the Lingui CLI. If your package
manager enforces strict dependency resolution (pnpm, or Yarn PnP), add it to your project explicitly with
`npm install --save-dev @lingui/format-po`.
:::

## Contributing a translation of the Dashboard itself

The sections above cover localizing **your own** extensions. If instead you want to translate the
built-in Dashboard UI into a new language — or improve an existing translation — those catalogs live
in the Vendure repository and the change is contributed via a pull request.

:::note
This is for the **React Dashboard** (`@vendure/dashboard`). The legacy Angular Admin UI has its own
[separate translation workflow](https://github.com/vendurehq/vendure/blob/master/packages/admin-ui/README.md#localization).
:::

The Dashboard's own message catalogs live at `packages/dashboard/src/i18n/locales/{locale}.po`, one
`.po` file per supported language. The English catalog (`en.po`) is the source and is always complete;
the others are translations of it.

### Improving an existing translation

Open the catalog for the language you want to improve — for example `de.po` for German — and fill in
or correct the `msgstr` values. An empty `msgstr` means that string is not yet translated and falls
back to English at runtime.

### Adding a new language

1. Add the locale code to the `locales` array in `packages/dashboard/lingui.config.js`. It must be a
   valid [`LanguageCode`](https://docs.vendure.io/reference/typescript-api/common/language-code/)
   value, which uses underscores for region variants (`zh_Hans`, `pt_BR`).
2. Add the same locale to `defaultAvailableLanguages` in `packages/dashboard/vite/constants.ts`. The
   **Display language** selector is built from this list, so a catalog missing from it can never be
   selected.
3. From `packages/dashboard`, generate the catalog:

   ```bash
   npx lingui extract
   ```

   This creates `src/i18n/locales/{locale}.po` with every UI string and empty `msgstr` values. Use
   `npx lingui extract` rather than `npm run i18n:extract`, which also runs an LLM helper that writes
   an untracked `missing-translations.txt`.
4. Fill in the `msgstr` values. The Dashboard globs that directory, so nothing else needs registering.

### Testing your translation locally

Run the development server so you can see your translations in the running Dashboard:

```bash
cd packages/dev-server
bun run populate   # first time only, to seed test data
bun run dev
```

`bun run dev` prints the Dashboard URL when it starts — open that. It serves from Vite, so `.po` edits
are picked up on reload (the static build served by `DashboardPlugin` would need a rebuild). Then switch
the UI language from the user menu at the bottom of the sidebar → **Display language**, and choose your
locale. The interface re-renders with your catalog; any string you left untranslated shows the English source.

### Opening the pull request

Commit the changed `.po` file(s) (and, for a new language, the `lingui.config.js` and
`vite/constants.ts` changes) and open a PR against `master`. A translation change is a `fix`, so title
it accordingly, e.g. `fix(dashboard): Add Ukrainian UI translations`. Translations are always welcome,
including partial ones — you don't have to translate every string to contribute.

Two CI checks gate a translation PR: **`dashboard i18n sync`** (runs `lingui extract` and fails if the
committed catalogs are out of date) and **`i18n:check`** (per-locale catalog validation). Run
`npx lingui extract` from `packages/dashboard` and commit the result so the sync check stays green.
