Skip to content

Translations

How translations work in Zephyr and how to add or improve them.

Stack

Zephyr uses Inlang Paraglide for i18n. It's a build-time message compiler - translations are JSON files; the build step turns them into typed JS functions, so missed strings become a build error rather than a silent runtime issue.

File layout

messages/
├── en.json        # English (source of truth)
├── fr.json
├── es-ES.json
├── pt-BR.json
├── zh-CN.json
├── ru.json
└── ar.json

Each file is a flat JSON dict where keys are message IDs and values are the translation:

json
{
  "randomizer_runtime_download": "Download & install",
  "randomizer_runtime_installFailed": "Runtime install failed"
}

Keys use snake_case with a topic prefix (randomizer_, mods_, prefs_, etc.). They're stable - once shipped, a key is never renamed (only deprecated and replaced).

Adding a new language

  1. Pick the BCP-47 code for your language (it for Italian, de for German, ja for Japanese, etc.).

  2. Copy messages/en.json to messages/<your-code>.json.

  3. Translate the values. Keys stay identical, including the language_name key (set its value to the autonym for your language - it's what shows up in Zephyr's language dropdown).

  4. Add the locale to project.inlang/settings.json:

    json
    {
      "locales": ["en", "fr", "es-ES", "pt-BR", "zh-CN", "ru", "ar", "<your-code>"]
    }
  5. Open a PR.

You don't need to translate everything before submitting. Missing keys fall back to the base locale at compile time.

Improving an existing translation

Just edit the .json file and PR. For multi-string changes, run the app and pick your language from Settings -> Language to verify nothing broke.

Style guide

  • Tone: same register as English - direct, no exclamation marks unless the original has one, no marketing fluff.
  • Capitalization: follow the target language's conventions (sentence case for French, e.g.), not English title case.
  • Numbers and dates: use the target language's format. 1,000.50 in English; 1 000,50 in French.
  • Length: try to stay close to the English string's length so layouts don't break. If a translation is necessarily longer, it's fine - the UI is mostly fluid - but flag it in the PR so we can sanity-check.
  • Mod-author content: don't translate it. Mod names, descriptions, configs, etc. are passed through verbatim.

Plurals

Paraglide handles plurals via a count parameter. Don't translate plural forms by hand - use the existing key structure:

json
{
  "mods_count_one": "{count} mod",
  "mods_count_other": "{count} mods"
}

Languages with more than two plural forms (Russian, Arabic, etc.) use the standard CLDR forms (zero, one, two, few, many, other). Add only the forms your language needs.

RTL languages

Arabic is the only RTL language currently. The ar.json translations follow the same pattern as everything else - the layout direction flip is handled by the app, not by the translations.

Machine translation

Machine translation is available as a starting point for new keys, never for shipped strings:

  1. Add the new key to en.json with the canonical text.
  2. Run pnpm machine-translate - this calls the Inlang CLI to fill missing keys in other locales with machine output.
  3. Native speakers review and correct the machine output via PR before the next release.

Testing

sh
pnpm tauri dev

In the running app, swap locales via Settings -> Language for instant comparison.

Credits

If you contribute a translation, mention yourself in the PR description.

Released under GPL-3.0.