-
Notifications
You must be signed in to change notification settings - Fork 0
Translations
Intrepid uses a hybrid translation system that combines two approaches:
-
Django Model Translation (
django-modeltranslation) - For content stored in database models - 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).
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
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)
The following models have translatable fields defined in src/cms/translation.py:
class SiteTextTranslation(TranslationOptions):
fields = ('body',)- Translates:
bodyfield
class VersionTranslation(TranslationOptions):
fields = (
'first_paragraph',
'pre_break_content',
'pull_quote',
'body',
)- Translates: Page content fields for versioned pages/updates
class PageUpdateTranslation(TranslationOptions):
fields = (
'title',
'abstract_paragraph',
)- Translates: Page titles and abstracts
class WhoWeAreProfileItemTranslation(TranslationOptions):
fields = ('bio',)- Translates: Team member bios
class HomePageQuoteTranslation(TranslationOptions):
fields = (
'pill_name',
'quotation',
'person_attribution',
'organization_attribution',
)- Translates: Homepage quotes and attributions
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 translationMigration 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),
),
]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_enExplicit Access:
site_text = SiteText.objects.get(key='welcome')
print(site_text.body_en) # English version
print(site_text.body_de) # German versionDefinition (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 (hasbody_enandbody_dein 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
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:
- Looks up the
SiteTextobject withkey='welcome_message' - Returns the
bodyfield in the current language (automatic viadjango-modeltranslation) - 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 %}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_prefetchedBenefits:
- All
SiteTextobjects loaded once per request - Subsequent calls use cached data
- Avoids N+1 query problem
List All Site Texts:
- URL:
/cms/site-text/ - View:
list_site_textinsrc/cms/views.py - Shows all
SiteTextobjects with language dropdown
Edit a Translation:
- URL:
/cms/site-text/edit/<key>/<lang_code>/ - View:
edit_site_textinsrc/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>')Export All Translations:
- URL:
/cms/site-text/csv/ - View:
site_text_csvinsrc/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:
- Export CSV with all current translations
- Send to translators
- Import updated translations back into database
Fixtures are JSON files that contain initial data for the database. Intrepid uses fixtures to version-control the SiteText strings.
Location: /fixtures/
Files:
fixtures/site_text.jsonfixtures/site_text_2.jsonfixtures/site_text_3.jsonfixtures/site_text_4.jsonfixtures/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
}
}
]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.jsonOr load all at once:
python manage.py loaddata fixtures/site_text*.jsonExport current database state:
python manage.py dumpdata cms.sitetext --indent=2 > fixtures/site_text_new.jsonStep 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.jsonStep 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' %}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 migrateStep 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),
]Use descriptive, hierarchical keys:
Good:
- index_header
- index_sub_header
- profile_edit_button
- error_invalid_email
Bad:
- header1
- text
- button
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"
}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
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
}Ensure all languages have translations:
- Export CSV regularly
- Send to translators
- Import updated translations
- Test in both languages
In Browser:
- Open Django admin
- Navigate to the page you want to test
- Change language in browser settings or use language switcher
- 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()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}")Symptom: English shows instead of German
Solution:
- Check language is set correctly:
translation.get_language() - Verify translation exists in database:
site_text.body_de - Clear cache if caching is enabled
- Check middleware is active
Symptom: Added field to translation.py but not working
Solution:
- Restart development server (translation.py is loaded at startup)
- Run migrations:
python manage.py migrate - Verify field appears in database with
_enand_desuffixes
Symptom: DeserializationError when loading fixture
Solution:
- Validate JSON syntax
- Ensure all required fields are present
- Check model name matches:
"model": "cms.sitetext" - Verify primary keys don't conflict
Intrepid's translation system uses:
-
django-modeltranslationfor database-backed content- Automatically creates
field_enandfield_decolumns - Transparent access via Django's translation system
- Used for: PageUpdates, Versions, HomePageQuotes, etc.
- Automatically creates
-
SiteTextmodel 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.