Internationalization (i18n)¶
Overview¶
The xynap Platform provides a global i18n system with automatic translation, Redis caching, and admin management.
| Component | Technology |
|---|---|
| Translation API | FastAPI /api/v1/i18n/ |
| Cache | Redis Hash (i18n:bundle:{locale}, TTL 1h) |
| Auto-Translator | LibreTranslate (local) |
| Source of Truth | en.json (English source texts) |
| Frontend | vue-i18n with dynamic loading |
| Admin UI | Settings → Languages |
Architecture¶
Frontend (Vue) Backend API Storage
┌─────────────┐ GET /i18n ┌──────────────┐ ┌──────────┐
│ vue-i18n │ ──────────→ │ i18n Router │ ──→│ Redis │ (Cache)
│ loadMessages │ /{locale} │ │ └──────────┘
└─────────────┘ │ │ ↓ miss
│ │ ┌──────────┐
│ │ ──→│ DB │ (translations)
│ │ └──────────┘
│ │ ↓ miss
│ │ ┌──────────┐
│ │ ──→│ en.json │ + Auto-Translate
└──────────────┘ └──────────┘
Lookup Chain¶
- Redis —
HGETALL i18n:bundle:{locale}→ instant return - DB —
translationstable → cache in Redis - en.json + LibreTranslate → translate, store in DB + Redis
- Fallback — return the key itself
Public API Endpoints¶
No authentication required.
List Languages¶
Response:
[
{"code": "de", "label": "Deutsch"},
{"code": "en", "label": "English"},
{"code": "es", "label": "Español"}
]
Load Translation Bundle¶
Returns all translation keys as flat JSON. Supports ETag/If-None-Match for conditional requests (304 Not Modified).
Response Headers:
Response Body:
{
"error.not_found": "Not found",
"auth.login_success": "Login successful",
"invoice.created": "Invoice {number} created"
}
Get Single Key¶
Admin Endpoints¶
Require settings.manage permission.
Add Language¶
On first bundle request for the new language, all keys are automatically translated via LibreTranslate.
Remove Language¶
Restriction
de and en cannot be removed.
Set Manual Translation¶
PUT /api/v1/i18n/admin/translations/{locale}/{key}
Content-Type: application/json
{"value": "User not found"}
Overrides automatic translations. Marked as auto_translated=false.
Rebuild Translations¶
Regenerates all stale translations via LibreTranslate.
Forget Auto-Translations¶
Deletes all auto-translations for a language. They will be regenerated on next request.
Statistics¶
{
"total_keys": 111,
"languages": [
{
"code": "de", "label": "Deutsch",
"total": 111, "translated": 111,
"auto": 0, "manual": 111, "stale": 0
}
]
}
Stale Translations¶
Stale Detection¶
Each translation stores the MD5 hash of the English source text (source_hash). When en.json changes:
- Hash no longer matches
- Translation is marked as
stale=true - On next bundle request or rebuild, it's automatically re-translated
Frontend Integration¶
Static Files (Fallback)¶
de.ts and en.ts are immediately available on load — no flash of untranslated content.
Dynamic Loading¶
After mount, the frontend loads translations from the API:
import { loadMessages } from '@/i18n/loader'
const msgs = await loadMessages('de')
// → API call with ETag cache → localStorage cache
Language Switching¶
import { useLocale } from '@/shell/composables/useLocale'
const { locale, languages, setLocale, toggleLocale } = useLocale()
// Available languages (dynamically from API)
// languages.value → [{code: 'de', label: 'Deutsch'}, ...]
setLocale('es') // Automatically loads the Spanish bundle
Files¶
| File | Description |
|---|---|
app/core_models/translation.py |
DB model (translations table) |
app/i18n/cache.py |
Redis cache layer |
app/i18n/translator.py |
LibreTranslate client |
app/i18n/service.py |
Translation service (core logic) |
app/i18n/router.py |
API endpoints |
app/i18n/__init__.py |
t(key, locale) function |
frontend/src/i18n/loader.ts |
API-based loader |
frontend/src/shell/composables/useLocale.ts |
Locale composable |
frontend/src/shell/settings/SettingsLanguages.vue |
Admin UI |