Skip to content

Translations

Andy Byers edited this page Nov 25, 2025 · 1 revision

Intrepid Translation System Documentation

Overview

Intrepid uses a hybrid translation system that combines two approaches:

  1. Django Model Translation (django-modeltranslation) - For content stored in database models
  2. SiteText Model - A custom translation system for UI strings and static content

This dual approach provides flexibility for both content editors (who manage pages and dynamic content) and developers (who manage UI strings).


System Architecture

1. Languages Supported

Configuration (src/intrepid/settings.py):

LANGUAGES = (
    ('de', gettext('German')),
    ('en', gettext('English')),
)

MODELTRANSLATION_DEFAULT_LANGUAGE = 'en'
MODELTRANSLATION_LANGUAGES = ('en', 'de')
MODELTRANSLATION_FALLBACK_LANGUAGES = ('en',)
MODELTRANSLATION_PREPOPULATE_LANGUAGE = 'en'
  • Default Language: English (en)
  • Supported Languages: English (en) and German (de)
  • Fallback: If a translation is missing, the system falls back to English

2. Middleware

Django's LocaleMiddleware is enabled:

MIDDLEWARE = [
    # ...
    "django.middleware.locale.LocaleMiddleware",
    # ...
]

This middleware:

  • Detects the user's preferred language from browser settings, cookies, or session
  • Sets the active language for each request
  • Enables Django's translation system (gettext)

Part 1: Django Model Translation

What is Translated?

The following models have translatable fields defined in src/cms/translation.py:

SiteText Model

class SiteTextTranslation(TranslationOptions):
    fields = ('body',)
  • Translates: body field

Version Model

class VersionTranslation(TranslationOptions):
    fields = (
        'first_paragraph',
        'pre_break_content',
        'pull_quote',
        'body',
    )
  • Translates: Page content fields for versioned pages/updates

PageUpdate Model

class PageUpdateTranslation(TranslationOptions):
    fields = (
        'title',
        'abstract_paragraph',
    )
  • Translates: Page titles and abstracts

WhoWeAreProfileItem Model

class WhoWeAreProfileItemTranslation(TranslationOptions):
    fields = ('bio',)
  • Translates: Team member bios

HomePageQuote Model

class HomePageQuoteTranslation(TranslationOptions):
    fields = (
        'pill_name',
        'quotation',
        'person_attribution',
        'organization_attribution',
    )
  • Translates: Homepage quotes and attributions

How It Works

Behind the scenes, django-modeltranslation creates additional database fields for each translatable field:

Original Model:

class SiteText(models.Model):
    body = models.TextField()

After Translation Setup:

# Database has these fields:
body       # Original field (acts as default)
body_en    # English translation
body_de    # German translation

Migration Example (src/cms/migrations/0039_auto_20240914_0823.py):

operations = [
    migrations.AddField(
        model_name='sitetext',
        name='body_de',
        field=models.TextField(null=True),
    ),
    migrations.AddField(
        model_name='sitetext',
        name='body_en',
        field=models.TextField(null=True),
    ),
]

Accessing Translations in Code

Automatic (Recommended):

from django.utils import translation

# Django automatically returns the correct language version
with translation.override('de'):
    site_text = SiteText.objects.get(key='welcome')
    print(site_text.body)  # Returns body_de if available, else body_en

Explicit Access:

site_text = SiteText.objects.get(key='welcome')
print(site_text.body_en)  # English version
print(site_text.body_de)  # German version

Part 2: SiteText Model (Custom Translation System)

The SiteText Model

Definition (src/cms/models.py):

class SiteText(models.Model):
    """
    A site text object for managing UI strings and static content
    """
    key = models.SlugField(max_length=255, unique=True)  # Unique identifier
    body = models.TextField()                             # Translatable content
    help_text = models.TextField()                        # Developer note
    rich_text = models.BooleanField(default=False)       # HTML allowed?
    frontend = models.BooleanField(default=False)        # Public-facing?

Fields:

  • key: Unique identifier (e.g., "index_header", "welcome_message")
  • body: The translatable text content (has body_en and body_de in database)
  • help_text: Describes where/how this text is used (for developers/admins)
  • rich_text: Whether HTML tags are allowed in the body
  • frontend: Whether this text appears on public-facing pages

Using SiteText in Templates

Template Tag (src/cms/templatetags/site_text.py):

Load the template tag:

{% load site_text %}

Basic Usage:

{% get_site_text 'welcome_message' %}

How it works:

  1. Looks up the SiteText object with key='welcome_message'
  2. Returns the body field in the current language (automatic via django-modeltranslation)
  3. Caches results for performance

Example in Template (src/templates/initiatives/list.html):

{% extends "base/admin_base.html" %}
{% load site_text %}

{% block title %}
{% get_site_text 'initiatives' %}
{% endblock title %}

{% block content %}
    <h1>{% get_site_text 'initiative_list' %}</h1>
    <p class="lead">{% get_site_text 'initiative_list_helper' %}</p>

    <table class="table">
        <thead>
            <tr>
                <th>{% get_site_text 'name' %}</th>
                <th>{% get_site_text 'accounts' %}</th>
            </tr>
        </thead>
    </table>
{% endblock content %}

Performance Optimization

The template tag includes automatic caching:

def get_site_text(context, site_text_key, cms_prefetched=None):
    if cms_prefetched:
        # Use prefetched data
        return mark_safe(cms_prefetched[site_text_key].display(context))
    else:
        # Load all site texts once and cache on request
        cms_prefetched = {o.key: o for o in models.SiteText.objects.all()}
        # Store on request object for reuse
        context["request"].cms_prefetched = cms_prefetched

Benefits:

  • All SiteText objects loaded once per request
  • Subsequent calls use cached data
  • Avoids N+1 query problem

Managing Translations

1. Admin Interface

List All Site Texts:

  • URL: /cms/site-text/
  • View: list_site_text in src/cms/views.py
  • Shows all SiteText objects with language dropdown

Edit a Translation:

  • URL: /cms/site-text/edit/<key>/<lang_code>/
  • View: edit_site_text in src/cms/views.py
  • Opens modal to edit specific language version

Implementation:

@staff_member_required
def edit_site_text(request, key, lang_code):
    site_text = get_object_or_404(models.SiteText, key=key)

    # Temporarily set language context
    with translation.override(lang_code):
        if request.method == "POST":
            body = request.POST.get("body")
            site_text.body = body  # Saves to body_en or body_de
            site_text.save()
            return HttpResponse('<div class="alert alert-success">Saved</div>')

2. CSV Export/Import

Export All Translations:

  • URL: /cms/site-text/csv/
  • View: site_text_csv in src/cms/views.py

CSV Format:

ID,Key,Is Frontend?,English,German
1,index_header,False,"<h1>Collective, Connected, Sustainable</h1>","<h1>Kollektiv, Vernetzt, Nachhaltig</h1>"
2,welcome_message,True,"Welcome to OBC","Willkommen bei OBC"

Implementation:

def site_text_csv(request):
    site_texts = models.SiteText.objects.all()

    for site_text in site_texts:
        english = site_text.body_en
        german = site_text.body_de
        csvwriter.writerow([
            site_text.pk,
            site_text.key,
            site_text.frontend,
            english,
            german
        ])

Workflow:

  1. Export CSV with all current translations
  2. Send to translators
  3. Import updated translations back into database

Fixtures System

What Are Fixtures?

Fixtures are JSON files that contain initial data for the database. Intrepid uses fixtures to version-control the SiteText strings.

Location: /fixtures/

SiteText Fixtures

Files:

  • fixtures/site_text.json
  • fixtures/site_text_2.json
  • fixtures/site_text_3.json
  • fixtures/site_text_4.json
  • fixtures/site_text_5.json

Structure:

[
  {
    "model": "cms.sitetext",
    "pk": 1,
    "fields": {
      "key": "index_header",
      "body": "<h1>Collective, Connected, Sustainable</h1>",
      "help_text": "Appears on the home page left hand block.",
      "rich_text": false,
      "frontend": true
    }
  },
  {
    "model": "cms.sitetext",
    "pk": 2,
    "fields": {
      "key": "index_sub_header",
      "body": "We bring together publishers...",
      "help_text": "Displays below the main header on the home page.",
      "rich_text": false,
      "frontend": true
    }
  }
]

Loading Fixtures

Load into database:

python manage.py loaddata fixtures/site_text.json
python manage.py loaddata fixtures/site_text_2.json
python manage.py loaddata fixtures/site_text_3.json
python manage.py loaddata fixtures/site_text_4.json
python manage.py loaddata fixtures/site_text_5.json

Or load all at once:

python manage.py loaddata fixtures/site_text*.json

Creating New Fixtures

Export current database state:

python manage.py dumpdata cms.sitetext --indent=2 > fixtures/site_text_new.json

Developer Workflow

Adding a New Translatable String

Step 1: Add to fixtures

Edit fixtures/site_text.json (or create new file):

{
  "model": "cms.sitetext",
  "pk": 999,
  "fields": {
    "key": "my_new_string",
    "body": "Hello World",
    "help_text": "Displays on the example page",
    "rich_text": false,
    "frontend": true
  }
}

Step 2: Load fixture

python manage.py loaddata fixtures/site_text.json

Step 3: Add translations via admin

  • Visit /cms/site-text/
  • Find "my_new_string"
  • Click edit for German
  • Add German translation

Step 4: Use in template

{% load site_text %}
{% get_site_text 'my_new_string' %}

Adding a New Translatable Model Field

Step 1: Update translation.py

Add to src/cms/translation.py:

class MyModelTranslation(TranslationOptions):
    fields = ('field_to_translate',)

translator.register(models.MyModel, MyModelTranslation)

Step 2: Create migration

python manage.py makemigrations
python manage.py migrate

Step 3: Update existing data (optional)

# Migration to copy existing data
from django.db import migrations

def copy_to_english(apps, schema_editor):
    MyModel = apps.get_model('myapp', 'MyModel')
    for obj in MyModel.objects.all():
        obj.field_to_translate_en = obj.field_to_translate
        obj.save()

class Migration(migrations.Migration):
    dependencies = [...]
    operations = [
        migrations.RunPython(copy_to_english),
    ]

Translation Best Practices

1. Key Naming Conventions

Use descriptive, hierarchical keys:

Good:
  - index_header
  - index_sub_header
  - profile_edit_button
  - error_invalid_email

Bad:
  - header1
  - text
  - button

2. Help Text

Always provide clear help_text:

{
  "key": "checkout_total",
  "body": "Total",
  "help_text": "Displays at the bottom of the checkout page showing the sum of all items"
}

3. Rich Text vs Plain Text

Set rich_text: true only when HTML is needed:

{
  "key": "welcome_banner",
  "body": "<h1>Welcome</h1><p>To our site</p>",
  "rich_text": true
}

Security: Plain text is auto-escaped, rich text is marked safe

4. Frontend vs Backend

Use frontend: true for public-facing strings:

{
  "key": "public_homepage_title",
  "frontend": true
}

Use frontend: false for admin interface:

{
  "key": "admin_dashboard_title",
  "frontend": false
}

5. Translation Coverage

Ensure all languages have translations:

  • Export CSV regularly
  • Send to translators
  • Import updated translations
  • Test in both languages

Testing Translations

Switch Languages Manually

In Browser:

  1. Open Django admin
  2. Navigate to the page you want to test
  3. Change language in browser settings or use language switcher
  4. Verify translations appear correctly

In Code:

from django.utils import translation

# Test German translations
with translation.override('de'):
    response = client.get('/page/')
    assert 'Willkommen' in response.content.decode()

# Test English translations
with translation.override('en'):
    response = client.get('/page/')
    assert 'Welcome' in response.content.decode()

Check for Missing Translations

Query for empty translations:

from cms.models import SiteText

# Find SiteTexts with missing German translations
missing = SiteText.objects.filter(body_de__isnull=True)
for text in missing:
    print(f"Missing DE translation: {text.key}")

Common Issues and Solutions

Issue 1: Translation Not Appearing

Symptom: English shows instead of German

Solution:

  1. Check language is set correctly: translation.get_language()
  2. Verify translation exists in database: site_text.body_de
  3. Clear cache if caching is enabled
  4. Check middleware is active

Issue 2: New Field Not Translating

Symptom: Added field to translation.py but not working

Solution:

  1. Restart development server (translation.py is loaded at startup)
  2. Run migrations: python manage.py migrate
  3. Verify field appears in database with _en and _de suffixes

Issue 3: Fixture Load Fails

Symptom: DeserializationError when loading fixture

Solution:

  1. Validate JSON syntax
  2. Ensure all required fields are present
  3. Check model name matches: "model": "cms.sitetext"
  4. Verify primary keys don't conflict

Summary

Intrepid's translation system uses:

  1. django-modeltranslation for database-backed content

    • Automatically creates field_en and field_de columns
    • Transparent access via Django's translation system
    • Used for: PageUpdates, Versions, HomePageQuotes, etc.
  2. SiteText model for UI strings

    • Centralized management of static text
    • Template tag: {% get_site_text 'key' %}
    • Admin interface for editing translations
    • CSV export for translator workflow
    • Fixtures for version control

This hybrid approach provides flexibility for both developers (managing UI strings) and content editors (managing pages and content), while maintaining a consistent experience across languages.