Skip to content

Translating EmDash

EmDash’s admin UI is translatable using Lingui for message extraction and Lunaria for tracking translation progress. All translations live in PO (gettext) files — one per locale.

See the translation dashboard for current progress across all locales.

Translations must come from native or fluent speakers. We don’t accept machine-generated translations. If you use AI tools to assist, you must review every string by hand and test the result in context (see Testing your translations below).

We’d rather have no translation for a string than a bad one. A wrong translation is worse than showing the English fallback — it actively misleads users.

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.

  1. Check the translation dashboard to see what needs work. Check open PRs to avoid duplicating effort.

  2. Fork the repo and create a branch:

    Terminal window
    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:

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

    Fill in the msgstr:

    #: 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.

  • The msgstr value for each entry.
  • 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.

Some strings contain placeholders and tags:

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.

  1. Compile and run the demo:

    Terminal window
    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.

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:

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.

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

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

    export const LOCALES: LocaleDefinition[] = [
    { code: "en", label: "English", enabled: true },
    { code: "de", label: "Deutsch", enabled: true },
    // ...
    { code: "ja", label: "日本語", enabled: false }, // add yours
    ];

    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 unless your translations have 100% coverage — we’ll enable it once the translation reaches sufficient coverage.

  2. Run extraction to generate the empty PO file:

    Terminal window
    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.

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.

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.

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

You may use AI tools to draft translations, but:

  • You must review every string yourself. AI tools make subtle errors that only a fluent speaker would catch — wrong register, unnatural phrasing, incorrect technical terms.
  • You must test the result 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 obvious unreviewed machine translations will be closed.

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.