Skip to content

Translations (i18n / Weblate)

The frontend (apps/web, Quasar/Vue 3) is internationalized with vue-i18n. English (en-US) is the source of truth; every other locale is a translation managed in Weblate. English is also the default display locale and fallback (src/boot/i18n.ts); for the system language setting the UI maps the browser language to a bundled locale and falls back to English (src/stores/appearance.ts).

Where the messages live

apps/web/src/i18n/
├── index.ts            # imports the locale JSON, builds the messages map
├── en-US/index.json    # SOURCE — edit English strings here
└── de-DE/index.json    # translation (managed by Weblate)
  • Format: nested JSON, one file per locale. Keys are dot-addressed in code (t('chat.placeholder')). The files are pre-compiled at build time by @intlify/unplugin-vue-i18n (include: ./src/i18n in quasar.config.ts).
  • src/boot/i18n.ts pins MessageSchema = typeof messages['en-US'], so the English file defines the TypeScript shape — a missing key in en-US is a type error.

Day-to-day: adding or changing UI strings

  1. Add/edit the key in en-US/index.json only.
  2. Reference it in components via useI18n()t('your.key').
  3. Use named placeholders, e.g. "stepOf": "Step {n} of {total}".
  4. Run the parity check (also enforced by the pre-commit hook and CI):
node tools/i18n_check.mjs

It fails on missing/extra keys and on {placeholder} mismatches against the English source.

Do not translate into de-DE (or any other locale) by hand in normal development — Weblate owns those files and will overwrite manual edits. New keys you add to en-US show up in Weblate as untranslated strings for translators to fill in. (German strings that already exist stay as-is.)

Conventions Weblate relies on

  • Placeholders use vue-i18n's single-brace named form {name}. The Weblate component flag python-brace-format validates that translations keep the same set of placeholders.
  • Plurals use vue-i18n's pipe form ("no items | one item | {count} items"). Weblate stores these as a single string (the pipes are not split into CLDR plural categories), so translators must preserve the | segments.

Adding a new language

  1. In Weblate, "Start new translation" for the web component and pick the locale. Weblate creates apps/web/src/i18n/<locale>/index.json and opens a PR.
  2. After merge, wire the locale into the app:
  3. src/i18n/index.ts — import and register the new JSON.
  4. src/stores/appearance.ts — extend UiLanguage, navLang() and resolvedLocale() (these currently special-case only de/en).

Weblate setup (Hosted Weblate, libre)

Translations are hosted on Hosted Weblate under the libre plan (free for open projects). Repo-side config lives in .weblate (for the wlc CLI); the component itself is configured server-side:

Setting Value
File format JSON nested structure file
File mask apps/web/src/i18n/*/index.json
Monolingual base language file apps/web/src/i18n/en-US/index.json
Source / template language English (en)
Edit base file off (English source is edited in code)
Translation flags python-brace-format

Git integration: enable GitHub pull requests with push branch weblate — Weblate commits translations to that branch and opens PRs against main for review (it never pushes to main directly). Connect the repo via the Weblate GitHub App (or an SSH deploy key + webhook).