Files
orion/docs/development/migration/language-i18n-implementation.md
Samir Boulahtit 784bcb9d23
Some checks failed
CI / dependency-scanning (push) Successful in 34s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 11s
CI / pytest (push) Failing after 48m7s
CI / validate (push) Successful in 28s
docs(i18n): document CMS template translations and multi-language content pages
Add sections covering CMS locale file structure, translated template
inventory, TranslatableText pattern for sections, and the new
title_translations/content_translations model API with migration cms_002.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:00:00 +01:00

13 KiB

Language & Internationalization (i18n) Implementation

Overview

This document describes the implementation of multi-language support for the Orion platform. The system supports four languages (English, French, German, Luxembourgish) with flexible configuration at store, user, and customer levels.

Supported Languages

Code Language Native Name Flag
en English English GB
fr French Francais FR
de German Deutsch DE
lb Luxembourgish Letzebuerg LU

Default Language: French (fr) - reflecting the Luxembourg market context.

Database Changes

Migration: fcfdc02d5138_add_language_settings_to_store_user_customer

Stores Table

New columns added to stores:

ALTER TABLE stores ADD COLUMN default_language VARCHAR(5) NOT NULL DEFAULT 'fr';
ALTER TABLE stores ADD COLUMN dashboard_language VARCHAR(5) NOT NULL DEFAULT 'fr';
ALTER TABLE stores ADD COLUMN storefront_language VARCHAR(5) NOT NULL DEFAULT 'fr';
ALTER TABLE stores ADD COLUMN storefront_languages JSON NOT NULL DEFAULT '["fr", "de", "en"]';
Column Type Description
default_language VARCHAR(5) Fallback language for content when translation unavailable
dashboard_language VARCHAR(5) Default UI language for store dashboard
storefront_language VARCHAR(5) Default language for customer-facing shop
storefront_languages JSON Array of enabled languages for storefront selector

Users Table

ALTER TABLE users ADD COLUMN preferred_language VARCHAR(5) NULL;
Column Type Description
preferred_language VARCHAR(5) User's preferred dashboard language (overrides store setting)

Customers Table

ALTER TABLE customers ADD COLUMN preferred_language VARCHAR(5) NULL;
Column Type Description
preferred_language VARCHAR(5) Customer's preferred shop language (overrides store setting)

Architecture

Language Resolution Flow

Store Dashboard

User preferred_language
        |
        v (if not set)
Store dashboard_language
        |
        v (if not set)
System DEFAULT_LANGUAGE (fr)

Storefront

Customer preferred_language
        |
        v (if not set)
Session/Cookie language
        |
        v (if not set)
Store storefront_language
        |
        v (if not set)
Browser Accept-Language header
        |
        v (if not supported)
System DEFAULT_LANGUAGE (fr)

Files Created/Modified

New Files

File Description
app/utils/i18n.py Core i18n utilities, translation loading, language resolution
middleware/language.py Language detection middleware
app/api/v1/shared/language.py Language API endpoints
app/templates/shared/macros/language_selector.html UI components for language selection
static/locales/en.json English translations
static/locales/fr.json French translations
static/locales/de.json German translations
static/locales/lb.json Luxembourgish translations

Modified Files

File Changes
models/database/store.py Added language settings columns
models/database/user.py Added preferred_language column
models/database/customer.py Added preferred_language column
models/schema/store.py Added language fields to Pydantic schemas
models/schema/auth.py Added preferred_language to user schemas
models/schema/customer.py Added preferred_language to customer schemas
main.py Registered LanguageMiddleware
app/api/main.py Registered language API router

API Endpoints

Language API (/api/v1/language)

Endpoint Method Description
/api/v1/language/set POST Set language preference (cookie)
/api/v1/language/current GET Get current language info
/api/v1/language/list GET List all available languages
/api/v1/language/clear DELETE Clear language preference

Request/Response Examples

Set Language

POST /api/v1/language/set
Content-Type: application/json

{
    "language": "de"
}

Response:

{
    "success": true,
    "language": "de",
    "message": "Language set to Deutsch"
}

Translation Files

Translation files are stored in static/locales/{lang}.json with the following structure:

{
  "common": {
    "save": "Save",
    "cancel": "Cancel"
  },
  "auth": {
    "login": "Login",
    "logout": "Logout"
  },
  "nav": {
    "dashboard": "Dashboard",
    "products": "Products"
  }
}

Key Naming Convention

  • Use dot notation for nested keys: common.save, auth.login
  • Use snake_case for key names: product_name, order_status
  • Group by feature/section: products.add_product, orders.confirm_order

Template Integration

Using Translations in Jinja2

{# Import the translation function #}
{% from 'shared/macros/language_selector.html' import language_selector %}

{# Use translation function #}
{{ _('common.save') }}

{# With interpolation #}
{{ _('welcome.message', name=user.name) }}

{# Language selector component #}
{{ language_selector(
    current_language=request.state.language,
    enabled_languages=store.storefront_languages
) }}

Request State Variables

The LanguageMiddleware sets these on request.state:

Variable Type Description
language str Resolved language code
language_info dict Additional info (source, cookie value, browser value)

Middleware Order

The LanguageMiddleware must run after ContextMiddleware (to know the context type) and before ThemeContextMiddleware.

Execution order (request flow):

  1. LoggingMiddleware
  2. StoreContextMiddleware
  3. ContextMiddleware
  4. LanguageMiddleware <-- Detects language
  5. ThemeContextMiddleware
  6. FastAPI Router

UI Components

Full Language Selector

{{ language_selector(
    current_language='fr',
    enabled_languages=['fr', 'de', 'en'],
    position='right',
    show_label=true
) }}

Compact Selector (Flag Only)

{{ language_selector_compact(
    current_language='fr',
    enabled_languages=['fr', 'de', 'en']
) }}

Language Settings Form

For store settings pages:

{{ language_settings_form(
    current_settings={
        'default_language': store.default_language,
        'dashboard_language': store.dashboard_language,
        'storefront_language': store.storefront_language,
        'storefront_languages': store.storefront_languages
    }
) }}

Flag Icons

The language selector uses flag-icons CSS library. Ensure this is included in your base template:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css">

Usage: <span class="fi fi-fr"></span> for French flag.

Testing

Manual Testing Checklist

  • Language cookie is set when selecting language
  • Page reloads with correct language after selection
  • Store dashboard respects user's preferred_language
  • Storefront respects customer's preferred_language
  • Browser language detection works (clear cookie, use browser with different language)
  • Fallback to default language works for unsupported languages
  • Language selector shows only enabled languages on storefront

API Testing

# Set language
curl -X POST http://localhost:8000/api/v1/language/set \
  -H "Content-Type: application/json" \
  -d '{"language": "de"}'

# Get current language
curl http://localhost:8000/api/v1/language/current

# List languages
curl http://localhost:8000/api/v1/language/list

CMS Template i18n

All platform-facing templates use the _() translation function with keys from module-scoped locale files.

Locale File Structure

CMS locale files are at app/modules/cms/locales/{en,fr,de,lb}.json with 340 keys organized as:

platform.nav.*           — Navigation labels
platform.hero.*          — Default homepage hero section
platform.pricing.*       — Default homepage pricing section
platform.features.*      — Default homepage feature labels
platform.signup.*        — Signup flow
platform.success.*       — Post-signup success page
platform.cta.*           — Call-to-action section
platform.content_page.*  — Content page UI chrome
platform.footer.*        — Footer labels
platform.modern.*        — Modern homepage template (~90 keys)
platform.minimal.*       — Minimal homepage template (17 keys)
platform.find_shop.*     — Find-your-shop page
platform.addons.*        — Add-on pricing labels
messages.*               — Flash/toast messages
confirmations.*          — Confirmation dialogs
permissions.*            — Permission labels
features.*               — Feature gate labels
menu.*                   — Menu section labels

Template Usage

{# Simple key lookup #}
{{ _("cms.platform.modern.hero_title_1") }}

{# With interpolation #}
{{ _("cms.platform.modern.cta_trial", trial_days=14) }}

Translated Templates (Complete)

Template Keys Used Status
homepage-default.html + section partials platform.hero.*, platform.pricing.*, etc. Fully translated
homepage-modern.html platform.modern.* (~90 keys) Fully translated
homepage-minimal.html platform.minimal.* (17 keys) Fully translated
signup.html platform.signup.* Fully translated
signup-success.html platform.success.* Fully translated
content-page.html platform.content_page.* Fully translated
find-shop.html platform.find_shop.* Fully translated
base.html platform.nav.*, platform.footer.* Fully translated

CMS Section Translations (TranslatableText Pattern)

Homepage sections stored in the ContentPage.sections JSON column use the TranslatableText pattern:

{
  "hero": {
    "title": {"translations": {"fr": "Bienvenue", "en": "Welcome", "de": "Willkommen", "lb": "Wëllkomm"}}
  }
}

Resolution in templates via _t() macro:

{% macro _t(field, fallback='') %}
{%- if field and field.translations -%}
    {{ field.translations.get(lang) or field.translations.get(default_lang) or fallback }}
{%- endif -%}
{% endmacro %}

Multi-Language Content Pages

Migration: cms_002_add_title_content_translations

Added two nullable JSON columns to content_pages:

ALTER TABLE content_pages ADD COLUMN title_translations JSON;
ALTER TABLE content_pages ADD COLUMN content_translations JSON;

Data Format

{
  "en": "About Us",
  "fr": "À propos",
  "de": "Über uns",
  "lb": "Iwwer eis"
}

Model API

# Get translated title with fallback chain:
# title_translations[lang] → title_translations[default_lang] → self.title
page.get_translated_title(lang="de", default_lang="fr")

# Same for content:
page.get_translated_content(lang="de", default_lang="fr")

Template Usage

In content-page.html:

{% set _lang = current_language|default('fr') %}
{% set _page_title = page.get_translated_title(_lang) %}
{% set _page_content = page.get_translated_content(_lang) %}

<h1>{{ _page_title }}</h1>
{{ _page_content | safe }}

In base.html (nav/footer):

{{ page.get_translated_title(current_language|default('fr')) }}

Seed Script

The seed script (scripts/seed/create_default_content_pages.py) uses the tt() helper:

def tt(en, fr, de, lb=None):
    """Build a language-keyed dict for title_translations."""
    d = {"en": en, "fr": fr, "de": de}
    if lb: d["lb"] = lb
    return d

All platform pages, legal pages, and store defaults include title_translations with en/fr/de/lb values.

Future Enhancements

  1. Admin Language Support: Currently admin is English-only. The system is designed to easily add admin language support later.

  2. Translation Management UI: Add a language-tabbed title/content editor to the CMS admin for editing title_translations and content_translations.

  3. RTL Language Support: The is_rtl_language() function is ready for future RTL language support (Arabic, Hebrew, etc.).

  4. Auto-Translation: Integration with translation APIs for automatic content translation.

  5. Content Translation: Translate content_translations for platform marketing pages (currently only titles are translated; content remains English-only in the seed data).

Rollback

To rollback the content page translations:

alembic downgrade cms_001

This will remove title_translations and content_translations from content_pages.

To rollback the base language settings:

alembic downgrade -1

This will remove:

  • default_language, dashboard_language, storefront_language, storefront_languages from stores
  • preferred_language from users
  • preferred_language from customers