# Translating EmDash

> How to contribute translations for the EmDash admin UI.

Source: https://emdashcmseverything.com/official-docs/contributing/translating/

EmDash's admin UI is translatable using [Lingui](https://lingui.dev) for message extraction and [Lunaria](https://lunaria.dev) for tracking translation progress. All translations live in PO (gettext) files — one per locale.

## Translation status

See the [translation dashboard](https://i18n.emdashcms.com) for current progress across all locales.

## Who can translate

Every translation must be supervised by a **native or fluent speaker**. AI-generated translations are accepted, but only when a fluent speaker reviews every string and previews it in the running admin panel before submitting. Unsupervised machine output is not accepted. See [AI-assisted translations](#ai-assisted-translations) and [Testing your translations](#testing-your-translations) below.

Leaving a string untranslated is better than translating it wrongly. A wrong translation misleads users; the English fallback only inconveniences them.

## File structure

Translation catalogs live in `packages/admin/src/locales/`:

```
packages/admin/src/locales/
├── en/
│   └── messages.po    # English (source)
├── de/
│   └── messages.po    # German
└── ...
```

Each `.po` file contains `msgid`/`msgstr` pairs. The `msgid` is the English source text; the `msgstr` is your translation. Empty `msgstr` means "not yet translated" — Lingui will fall back to English at runtime.

## Translating strings

1. **Check the [translation dashboard](https://i18n.emdashcms.com)** to see what needs work. Check open PRs to avoid duplicating effort.

2. **Fork the repo and create a branch:**

   ```bash
   git checkout -b i18n/de
   ```

3. **Open your locale's PO file** (e.g., `packages/admin/src/locales/de/messages.po`).

4. **Fill in translations.** Each entry looks like this:

   ```po
   #: packages/admin/src/components/LoginPage.tsx:304
   msgid "Sign in with Passkey"
   msgstr ""
   ```

   Fill in the `msgstr`:

   ```po
   #: packages/admin/src/components/LoginPage.tsx:304
   msgid "Sign in with Passkey"
   msgstr "Mit Passkey anmelden"
   ```

5. **Test your translations** (see below).

6. **Open a PR** targeting `main`. Title format: `i18n(de): add/update German translations`.

   Keep the PR to the PO file, a changeset, and the locale files described in [Adding a new language](#adding-a-new-language). Automation comments on a translation PR that changes anything else.

### What to translate

- The `msgstr` value for each entry.

### What NOT to translate

- `msgid` values — these are lookup keys.
- Interpolation placeholders like `{error}`, `{email}`, `{label}` — keep them exactly as-is.
- XML-style tags like `<0>`, `</0>` — these wrap interactive elements (links, buttons). Keep the tags and translate the text between them.
- Comments starting with `#:` — these are source references added by Lingui.

### Interpolation and tags

Some strings contain placeholders and tags:

```po
msgid "Authentication error: {error}"
msgstr "Authentifizierungsfehler: {error}"

msgid "Don't have an account? <0>Sign up</0>"
msgstr "Noch kein Konto? <0>Registrieren</0>"

msgid "If an account exists for <0>{email}</0>, we've sent a sign-in link."
msgstr "Falls ein Konto für <0>{email}</0> existiert, haben wir einen Anmeldelink gesendet."
```

Placeholders (`{error}`, `{email}`) are replaced with dynamic values at runtime. Tags (`<0>...</0>`) wrap React components. Both must appear in your translation exactly as they appear in the source — same names, same nesting.

## Testing your translations

> **Caution**
>
> Always test your translations in the running admin UI. Strings that read well in isolation can look wrong in context — truncated buttons, broken layouts, or awkward phrasing that only shows up when you see the actual screen.

1. **Compile and run the demo:**

   ```bash
   pnpm run locale:compile
   pnpm build
   pnpm --filter emdash-demo dev
   ```

2. **Switch locale** in the admin Settings page and verify your translations look correct in context.

### Pseudo locale

EmDash ships a **pseudo locale** that garbles all wrapped strings into accented lookalikes — `"Dashboard"` becomes `"Ðàšĥƀöàřð"`, and so on. Any string that appears in normal English while the pseudo locale is active is either missing a `t\`...\`` wrapper or is coming from outside the catalog.

To enable it, add the following to your `.env` file in the demo directory:

```ini title="demos/simple/.env"
EMDASH_PSEUDO_LOCALE=1
```

Then restart the dev server. The pseudo locale appears as **Pseudo** in the language picker on the login page and in Settings. Switch to it to spot unwrapped strings at a glance.

> **Note**
>
> The pseudo locale is only available in development (`import.meta.env.DEV`). It is never exposed in production builds regardless of environment variables.

## Adding a new language

If your language doesn't have a PO file yet:

1. **Add the locale to `packages/admin/src/locales/locales.ts`:**

   ```ts
   export const LOCALES: LocaleDefinition[] = [
     { code: "en", label: "English", enabled: true },
     // ...
     {
       code: "vi",
       label: "Tiếng Việt",
       enabled: false,
       dateLocale: () => import("react-day-picker/locale/vi").then((m) => m.vi),
     },
   ];
   ```

   `dateLocale` loads the calendar and date-format localization for your language. Pick the closest file in [`react-day-picker/locale`](https://github.com/gpbl/react-day-picker/tree/main/packages/react-day-picker/src/locale), such as `pt` for `pt-PT`, and use the export named in that file. Omit it only if your language uses the US English calendar.

   This is the single source of truth — `lingui.config.ts`, `lunaria.config.ts` and the admin runtime all derive their locale lists from this file. Set `enabled: false` while translation is in progress. A maintainer enables the locale once it has sufficient coverage for use in the admin UI.

2. **Run extraction** to generate the empty PO file:

   ```bash
   pnpm run locale:extract
   ```

   This creates `packages/admin/src/locales/{your-locale}/messages.po` with all strings ready to translate.

3. **Translate and test** following the steps above.

## Translation standards

### Accuracy

Translations should faithfully represent the English source text at a native speaker's level. Don't add, remove, or reinterpret meaning. If a source string is ambiguous, check the `#:` comment for the source file location — read the component code to understand the context.

### Consistency

Use consistent terminology within your locale. If you translate "collection" as "Sammlung" in one place, don't switch to "Kollektion" elsewhere. If your language already has translations, read through the existing PO file before starting to match the established terminology.

### Tone

The admin UI uses a direct, professional tone. Match that in your language — avoid overly formal or overly casual phrasing.

### AI-assisted translations

You may generate translations with AI tools, including a full first pass, but a fluent speaker must supervise the result:

- A fluent speaker **must** review every string. AI tools make subtle errors only a fluent speaker catches — wrong register, unnatural phrasing, incorrect technical terms.
- A fluent speaker **must** preview the translations in the running admin UI. AI tools have no awareness of layout constraints or UI context.
- Disclose AI usage in your PR description.
- PRs with unsupervised machine translations will be closed.

## Partial translations

Partial translations are welcome. You don't need to translate every string in one PR — any progress helps. Untranslated strings will fall back to English at runtime.
