Translations (i18n / Weblate)
The frontend (the personal-agent-org/frontend repo, 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
In the frontend repo (personal-agent-org/frontend, SPA at the repo root):
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/i18ninquasar.config.ts). src/boot/i18n.tspinsMessageSchema = typeof messages['en-US'], so the English file defines the TypeScript shape - a missing key inen-USis a type error.
Day-to-day: adding or changing UI strings
-
Add/edit the key in
en-US/index.jsononly. -
Reference it in components via
useI18n()→t('your.key'). -
Use named placeholders, e.g.
"stepOf": "Step {n} of {total}"(setup.stepOf). -
Run the parity check:
just i18n # or: node scripts/i18n_check.mjsIt flattens each locale to dotted leaf paths and fails (exit 1) on missing keys, extra keys, or mismatched
{named}placeholder sets against the English source. It is part ofjust checkand runs in CI (.github/workflows/ci.yml, step “i18n parity check”). The pre-commit hooks (.pre-commit-config.yaml) only cover formatting and conventional-commit messages, so run it yourself before pushing.
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 flagpython-brace-formatvalidates that translations keep the same set of placeholders;scripts/i18n_check.mjsenforces the same locally. - Counts are interpolated with a named argument, not CLDR plurals: the
message embeds the count placeholder (e.g.
"chunks": "{n} chunks",files.chunks) and the caller passest('files.chunks', { n }). The source uses no vue-i18n pipe-plural (a | b) strings, so there are no|segments for translators to preserve.
Adding a new language
- In Weblate, “Start new translation” for the
webcomponent and pick the locale. Weblate createssrc/i18n/<locale>/index.jsonand opens a PR. - After merge, wire the locale into the app:
src/i18n/index.ts— import and register the new JSON.src/stores/appearance.ts— extendUiLanguage,navLang()andresolvedLocale()(these currently special-case onlyde/en).
Weblate setup
Translations are managed in Weblate. The repo carries no
.weblate / wlc config file; the component is configured entirely server-side:
| Setting | Value |
|---|---|
| File format | JSON nested structure file |
| File mask | src/i18n/*/index.json |
| Monolingual base language file | 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).
Browser extension (WebExtension i18n)
The browser extension lives in its own repository
(personal-agent-org/browser-extension)
and carries its own WebExtension i18n (_locales/*/messages.json) plus a Weblate component there —
see that repo’s README. It is no longer part of this repository’s translation setup.