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.
Translation status
See the translation dashboard for current progress across all locales.
Who can translate
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.
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
-
Check the translation dashboard to see what needs work. Check open PRs to avoid duplicating effort.
-
Fork the repo and create a branch:
git checkout -b i18n/de -
Open your locale’s PO file (e.g.,
packages/admin/src/locales/de/messages.po). -
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" -
Test your translations (see below).
-
Open a PR targeting
main. Title format:i18n(de): add/update German translations.
What to translate
- The
msgstrvalue for each entry.
What NOT to translate
msgidvalues — 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:
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
-
Compile and run the demo:
pnpm run locale:compile pnpm build pnpm --filter emdash-demo dev -
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:
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.
Adding a new language
If your language doesn’t have a PO file yet:
-
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.tsand the admin runtime all derive their locale lists from this file. Setenabled: falseunless your translations have 100% coverage — we’ll enable it once the translation reaches sufficient coverage. -
Run extraction to generate the empty PO file:
pnpm run locale:extractThis creates
packages/admin/src/locales/{your-locale}/messages.powith all strings ready to translate. -
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 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
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.