Skip to content

Internationalization (i18n)

OctoPrint-TempETA supports multiple languages using Flask-Babel for backend and Jinja2 templates.

Supported Languages

Currently supported:

  • šŸ‡¬šŸ‡§ English (en) - Default
  • šŸ‡©šŸ‡Ŗ German (de)

Translation Workflow

1. Extract Messages

Extract all translatable strings from code:

pybabel extract -F babel.cfg -o translations/messages.pot .

This creates a POT (Portable Object Template) file with all strings.

2. Initialize New Language

To add a new language (e.g., French):

pybabel init -i translations/messages.pot -d translations -l fr

This creates translations/fr/LC_MESSAGES/messages.po.

3. Update Existing Translations

After adding new strings:

pybabel update -i translations/messages.pot -d translations

This updates all .po files with new strings.

4. Translate

Edit the .po files:

# translations/de/LC_MESSAGES/messages.po
msgid "Heating to {target}°C"
msgstr "Aufheizen auf {target}°C"

msgid "ETA: {eta}"
msgstr "Verbleibende Zeit: {eta}"

5. Compile

Compile .po files to binary .mo files:

pybabel compile -d translations

This must be done before the translations are usable.

Babel Configuration

babel.cfg in project root:

[python: **.py]
[jinja2: **/templates/**.jinja2]
encoding = utf-8
extensions = jinja2.ext.i18n

Translation in Python

Backend Translations

from flask_babel import gettext

# Simple translation
message = gettext("Heating complete")

# With variables
message = gettext("Heating to {target}°C").format(target=200)

# Plural forms
from flask_babel import ngettext
message = ngettext(
    "{count} heater active",
    "{count} heaters active",
    count
).format(count=count)

Lazy Translations

For class-level strings:

from flask_babel import lazy_gettext

class MyClass:
    # Translated when accessed, not when defined
    ERROR_MESSAGE = lazy_gettext("An error occurred")

Translation in Templates

Jinja2 Templates

{# Simple translation #}
<h3>{{ _('Temperature ETA') }}</h3>

{# With variables #}
<p>{{ _('Heating to %(target)s°C', target=200) }}</p>

{# Plural forms #}
<p>{{ ngettext(
    '%(count)s heater',
    '%(count)s heaters',
    heaters|length
) }}</p>

{# Block translation #}
{% trans %}
  This is a longer block of text that needs translation.
  It can span multiple lines.
{% endtrans %}

{# Block with variables #}
{% trans target=200, eta='02:00' %}
  Heating to {{ target }}°C
  ETA: {{ eta }}
{% endtrans %}

Translation in JavaScript

Loading Translations

OctoPrint provides translations to JavaScript:

// Access translation
var message = gettext("Heating complete");

// With interpolation
var message = interpolate(
    gettext("Heating to %(target)s°C"),
    {target: 200},
    true
);

Translation Catalog

Translations are loaded from backend:

// In view model
self._ = function(text) {
    return gettext(text);
};

// Usage in Knockout bindings
<span data-bind="text: _('Temperature ETA')"></span>

String Conventions

Naming

  • Use descriptive keys in English
  • Keep keys consistent across plugin

Format

  • Use {variable} for Python format strings
  • Use %(variable)s for Babel/gettext format
  • Be consistent within each context

Context

For ambiguous strings, add context:

# For tool temperature
pgettext("tool", "Temperature")

# For bed temperature
pgettext("bed", "Temperature")

Translation Keys

Common Strings

# Status messages
_("Heating")
_("Cooling")
_("Target reached")
_("Calculating")
_("Stalled")

# Time formats
_("{hours}h {minutes}m")
_("{minutes}m {seconds}s")
_("{seconds}s")

# Settings
_("Enable Plugin")
_("Algorithm")
_("Update Interval")
_("Minimum Rate")

# Errors
_("Failed to calculate ETA")
_("Invalid temperature data")
_("MQTT connection failed")

Date and Time Formatting

Backend

from flask_babel import format_datetime, format_time

# Format datetime
formatted = format_datetime(datetime.now(), 'short')

# Format time
formatted = format_time(time.now(), 'short')

# Custom format
formatted = format_datetime(
    datetime.now(),
    "yyyy-MM-dd HH:mm:ss"
)

Number Formatting

from flask_babel import format_decimal, format_percent

# Format decimal
temp = format_decimal(200.5, locale='de_DE')  # "200,5"

# Format percent
progress = format_percent(0.75)  # "75%"

Locale Detection

OctoPrint uses browser locale:

# Get current locale
from flask_babel import get_locale
locale = get_locale()

# Force specific locale (testing)
from flask import request
with request.test_request_context():
    request.accept_languages = [('de', 1)]
    # Translations now in German

Testing Translations

Command Line

# Extract messages
pybabel extract -F babel.cfg -o translations/messages.pot .

# Check for missing translations
msgfmt --check translations/de/LC_MESSAGES/messages.po

# Get statistics
msgfmt --statistics translations/de/LC_MESSAGES/messages.po

Python Tests

def test_translations():
    """Test that all strings are translated."""
    # Load catalog
    from babel.messages.pofile import read_po

    with open('translations/de/LC_MESSAGES/messages.po', 'rb') as f:
        catalog = read_po(f)

    # Check for untranslated strings
    for message in catalog:
        if message.id and not message.string:
            print(f"Untranslated: {message.id}")

Browser Testing

Change OctoPrint language:

Settings → Appearance → Language → Deutsch

Then verify all UI strings are translated.

Translation Guidelines

For Developers

  1. Always use translation functions: Never hardcode user-facing strings
  2. Provide context: Add comments for translators
  3. Use placeholders: Never concatenate strings
  4. Test in multiple languages: Ensure UI doesn't break
  5. Keep strings short: Long strings may not fit in UI

For Translators

  1. Maintain tone: Keep translations consistent with OctoPrint
  2. Preserve placeholders: Don't translate {variable} names
  3. Consider length: Translations may be longer than English
  4. Test in UI: See how translations look in context
  5. Ask questions: Use comments to clarify unclear strings

Translation File Structure

translations/
ā”œā”€ā”€ messages.pot           # Template (all strings)
ā”œā”€ā”€ de/
│   └── LC_MESSAGES/
│       ā”œā”€ā”€ messages.po    # German translations
│       └── messages.mo    # Compiled (binary)
└── fr/
    └── LC_MESSAGES/
        ā”œā”€ā”€ messages.po
        └── messages.mo

CI/CD Integration

GitHub Actions

# .github/workflows/i18n.yml
name: Check Translations

on: [push, pull_request]

jobs:
  check-i18n:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Babel
        run: pip install Babel

      - name: Extract messages
        run: pybabel extract -F babel.cfg -o translations/messages.pot .

      - name: Check translations
        run: |
          for po in translations/*/LC_MESSAGES/messages.po; do
            echo "Checking $po"
            msgfmt --check "$po"
          done

Adding a New Language

Complete example for adding Spanish:

# 1. Initialize
pybabel init -i translations/messages.pot -d translations -l es

# 2. Translate
# Edit translations/es/LC_MESSAGES/messages.po

# 3. Compile
pybabel compile -d translations

# 4. Test
# Set OctoPrint language to Spanish

# 5. Commit
git add translations/es/
git commit -m "Add Spanish translations"

Common Issues

Strings Not Translating

  1. Check .mo file is compiled: pybabel compile -d translations
  2. Restart OctoPrint after compiling
  3. Clear browser cache
  4. Verify locale is correct in settings

Missing Translations

  1. Extract new strings: pybabel extract
  2. Update catalogs: pybabel update
  3. Translate in .po files
  4. Compile: pybabel compile

Encoding Errors

Ensure files are UTF-8:

file -bi translations/de/LC_MESSAGES/messages.po
# Should show: text/plain; charset=utf-8

Resources

Next Steps