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)sfor 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¶
- Always use translation functions: Never hardcode user-facing strings
- Provide context: Add comments for translators
- Use placeholders: Never concatenate strings
- Test in multiple languages: Ensure UI doesn't break
- Keep strings short: Long strings may not fit in UI
For Translators¶
- Maintain tone: Keep translations consistent with OctoPrint
- Preserve placeholders: Don't translate
{variable}names - Consider length: Translations may be longer than English
- Test in UI: See how translations look in context
- 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¶
- Check
.mofile is compiled:pybabel compile -d translations - Restart OctoPrint after compiling
- Clear browser cache
- Verify locale is correct in settings
Missing Translations¶
- Extract new strings:
pybabel extract - Update catalogs:
pybabel update - Translate in
.pofiles - 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¶
- UI Placements - Where translations appear
- Contributing Guide - How to contribute translations
- Settings Reference - Translatable settings