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.
Translation status
Section titled “Translation status”See the translation dashboard for current progress across all locales.
Who can translate
Section titled “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
Section titled “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
Section titled “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:
Terminal window 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:304msgid "Sign in with Passkey"msgstr ""Fill in the
msgstr:#: packages/admin/src/components/LoginPage.tsx:304msgid "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
Section titled “What to translate”- The
msgstrvalue for each entry.
What NOT to translate
Section titled “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
Section titled “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
Section titled “Testing your translations”-
Compile and run the demo:
Terminal window pnpm run locale:compilepnpm buildpnpm --filter emdash-demo dev -
Switch locale in the admin Settings page and verify your translations look correct in context.
Pseudo locale
Section titled “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=1Then 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
Section titled “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:
Terminal window 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
Section titled “Translation standards”Accuracy
Section titled “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
Section titled “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.
The admin UI uses a direct, professional tone. Match that in your language — avoid overly formal or overly casual phrasing.
AI-assisted translations
Section titled “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
Section titled “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.