Skip to content

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

  1. RedisHGETALL i18n:bundle:{locale} → instant return
  2. DBtranslations table → cache in Redis
  3. en.json + LibreTranslate → translate, store in DB + Redis
  4. Fallback — return the key itself

Public API Endpoints

No authentication required.

List Languages

GET /api/v1/i18n/languages

Response:

[
  {"code": "de", "label": "Deutsch"},
  {"code": "en", "label": "English"},
  {"code": "es", "label": "Español"}
]

Load Translation Bundle

GET /api/v1/i18n/{locale}

Returns all translation keys as flat JSON. Supports ETag/If-None-Match for conditional requests (304 Not Modified).

Response Headers:

ETag: "9e1c7de849edd567e026cc559512f9d3"
Cache-Control: public, max-age=300

Response Body:

{
  "error.not_found": "Not found",
  "auth.login_success": "Login successful",
  "invoice.created": "Invoice {number} created"
}

Get Single Key

GET /api/v1/i18n/{locale}/{key}

Admin Endpoints

Require settings.manage permission.

Add Language

POST /api/v1/i18n/admin/languages
Content-Type: application/json

{"code": "es", "label": "Español"}

On first bundle request for the new language, all keys are automatically translated via LibreTranslate.

Remove Language

DELETE /api/v1/i18n/admin/languages/{locale}

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

POST /api/v1/i18n/admin/rebuild
POST /api/v1/i18n/admin/rebuild/{locale}

Regenerates all stale translations via LibreTranslate.

Forget Auto-Translations

POST /api/v1/i18n/admin/forget/{locale}

Deletes all auto-translations for a language. They will be regenerated on next request.

Statistics

GET /api/v1/i18n/admin/stats
{
  "total_keys": 111,
  "languages": [
    {
      "code": "de", "label": "Deutsch",
      "total": 111, "translated": 111,
      "auto": 0, "manual": 111, "stale": 0
    }
  ]
}

Stale Translations

GET /api/v1/i18n/admin/stale

Stale Detection

Each translation stores the MD5 hash of the English source text (source_hash). When en.json changes:

  1. Hash no longer matches
  2. Translation is marked as stale=true
  3. 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

Backend Usage

from app.i18n import t

# In endpoints
message = t("invoice.created", locale, number="2024-001")

# In email templates
subject = t("email.subject.invoice", user_locale, number=inv.number)