Argos and similar MT often corrupt %(name)s (e.g. "% (horas)") or swap in positional %s, causing ValueError during dashboard render. - Add scripts/sanitize_po_format_strings.py to clear invalid msgstr / plural strings so gettext falls back to English msgids. - Run sanitizer on translations/pt; msgfmt --check-format now passes. - Document sanitizer + msgfmt after bulk fill in TRANSLATION_SYSTEM and fill_po_argos header.
9.5 KiB
Translation System Documentation
Overview
TimeTracker includes a comprehensive internationalization (i18n) system powered by Flask-Babel. Enabled locales are defined in app/config.py (LANGUAGES), for example:
- English (en) - Default
- Dutch (nl - Nederlands)
- German (de - Deutsch)
- French (fr - Français)
- Italian (it - Italiano)
- Finnish (fi - Suomi)
- Portuguese (pt - Português)
- Spanish (es), Norwegian (no), Arabic (ar), Hebrew (he), and others as configured
Regional Portuguese tags (pt-BR, pt-PT, etc.) are normalized to pt so a single catalog under translations/pt/ is used.
User Experience
Language Switcher
The language switcher is located in the top navigation bar, positioned between the command palette button and the user profile menu. It features:
- 🌐 Globe icon for easy recognition
- Current language label (on larger screens)
- Dropdown menu with all available languages
- Visual indicator (checkmark) for the currently selected language
- Smooth hover transitions and animations
Language Selection
Users can change the interface language in two ways:
- Via Navigation Bar: Click the globe icon and select a language from the dropdown
- Direct URL: Visit
/i18n/set-language?lang=<code>(e.g.,?lang=defor German)
Language preference is persisted:
- For authenticated users: Saved to user profile in database
- For guests: Stored in session
Technical Details
Translation Files
Translation files are located in translations/ directory (see app/config.py for the full list of enabled locales; additional .po files may exist for locales not yet wired in config).
translations/
├── en/LC_MESSAGES/messages.po # English
├── nl/LC_MESSAGES/messages.po # Dutch
├── de/LC_MESSAGES/messages.po # German
├── fr/LC_MESSAGES/messages.po # French
├── it/LC_MESSAGES/messages.po # Italian
├── fi/LC_MESSAGES/messages.po # Finnish
├── es/LC_MESSAGES/messages.po # Spanish
├── pt/LC_MESSAGES/messages.po # Portuguese
└── ... # Other locales as configured
Configuration
Language configuration is defined in app/config.py:
LANGUAGES = {
'en': 'English',
'nl': 'Nederlands',
'de': 'Deutsch',
'fr': 'Français',
'it': 'Italiano',
'fi': 'Suomi',
'pt': 'Português',
# ... es, no, ar, he, etc. — see app/config.py for the full map
}
BABEL_DEFAULT_LOCALE = 'en'
Locale Selection Priority
The system determines the user's language in the following order:
- User preference from database (for authenticated users)
- Session override (via set-language route)
- Browser Accept-Language header (best match)
- Default locale (en)
The locale normalizer maps no → nb (Norwegian catalog folder) and folds pt-* (e.g. pt-BR, pt-PT) to pt.
See app/__init__.py for the locale selector implementation.
In Templates
Use the _() function to mark strings for translation:
<h1>{{ _('Welcome to TimeTracker') }}</h1>
<button>{{ _('Start Timer') }}</button>
For strings with variables, use named parameters:
<p>{{ _('%(app)s is a web-based time tracking application', app='TimeTracker') }}</p>
In Python Code
Import and use the translation function:
from flask_babel import _
message = _('Timer started successfully')
flash(_('Project created'), 'success')
Translation Compilation
Translation files (.po) are automatically compiled to binary files (.mo) when the application starts. The compilation is handled by app/utils/i18n.py which:
- Checks if
.mofiles exist and are up-to-date - Compiles
.poto.mousing Babel's message tools - Runs automatically during application initialization
Adding a New Language
To add a new language:
-
Add to configuration in
app/config.py:LANGUAGES = { # ... existing languages ... 'es': 'Español', # Add Spanish } -
Create translation directory:
mkdir -p translations/es/LC_MESSAGES -
Initialize translation file:
pybabel init -i messages.pot -d translations -l es -
Translate the strings in
translations/es/LC_MESSAGES/messages.po -
Restart the application - translations will compile automatically
Updating Translations
When you add new translatable strings to the application:
-
Use a virtualenv with project dependencies (at least Babel and Jinja2) so
pybabelcan scan templates. -
Extract messages (writes
messages.potat the repo root; it is gitignored):pybabel extract -F babel.cfg -o messages.pot .babel.cfgdefines a[extractors]alias so the jinja2 method resolves tojinja2.ext:babel_extract(needed on some Python/setuptools setups where thebabel.extractorsentry point is not visible). -
Update all translation files:
pybabel update -i messages.pot -d translations --no-wrapTo drop obsolete entries after a large refactor:
pybabel update -i messages.pot -d translations --ignore-obsolete --no-wrap -
Translate new strings in each
.pofile (human review, Crowdin, or a local helper).For a first-pass Portuguese fill using offline Argos models (machine quality; always review):
pip install polib argostranslate python -c "import argostranslate.package as p; p.update_package_index(); pkg=next(x for x in p.get_available_packages() if x.from_code=='en' and x.to_code=='pt'); p.install_from_path(pkg.download())" python scripts/fill_po_argos.py translations/pt/LC_MESSAGES/messages.po --from en --to ptMachine translators often break
%(name)s/{name}placeholders. Runpython scripts/sanitize_po_format_strings.py translations/pt/LC_MESSAGES/messages.poafterward, thenmsgfmt --check-format -o /dev/null …/messages.poto confirm the catalog is safe for Python’s%/.format()at runtime. -
Restart application (or set
TT_COMPILE_TRANSLATIONS_ON_STARTUP=true) so.mofiles are compiled when needed
Translation File Format
Translation files use the PO (Portable Object) format:
# Comment
msgid "Original English text"
msgstr "Translated text"
# With context
msgid "Dashboard"
msgstr "Tableau de bord" # French
# Plurals
msgid "1 hour"
msgid_plural "%d hours"
msgstr[0] "1 heure"
msgstr[1] "%d heures"
Best Practices
-
Keep strings short and contextual
- Good:
_('Save') - Avoid:
_('Click this button to save your changes to the database')
- Good:
-
Use sentence case
- Good:
_('Start timer') - Avoid:
_('START TIMER')
- Good:
-
Avoid concatenation
- Good:
_('Welcome back, %(name)s', name=user.name) - Avoid:
_('Welcome back,') + ' ' + user.name
- Good:
-
Provide context in comments
# Translators: This is the button to start the time tracking timer _('Start Timer') -
Test in multiple languages to ensure UI layout works correctly
Troubleshooting
Language not changing
- Check browser console for JavaScript errors
- Verify the language code exists in
LANGUAGESconfig - Clear browser cache and cookies
- Check that
.mofiles exist intranslations/<lang>/LC_MESSAGES/
Translations not showing
- Ensure strings are wrapped in
_()function - Check that
.mofiles are compiled (restart application) - Verify translation exists in the
.pofile - Check for syntax errors in
.pofile
Compilation errors
If translations fail to compile:
- Check
.pofile syntax (must be valid) - Ensure
msgidandmsgstrare properly quoted - Look for encoding issues (files must be UTF-8)
Styling
Language switcher styling is defined in app/static/base.css:
- Smooth hover transitions
- Consistent with application design system
- Responsive design (icon-only on small screens)
- Follows light/dark theme
Accessibility
The language switcher includes:
- Proper ARIA labels and attributes
- Keyboard navigation support
- Clear visual indication of current language
- Tooltip with current language name
- Semantic HTML structure
Performance
- Translations are compiled at startup (one-time operation)
- Compiled
.mofiles are cached in memory - No runtime performance impact
- Minimal bundle size increase per language (~50-100KB)
Future Enhancements
Potential improvements:
- Add more languages (Japanese, Chinese, etc.)
- Right-to-left (RTL) language support (Arabic, Hebrew)
- User-contributed translations via CONTRIBUTING_TRANSLATIONS.md (issues, spreadsheet, Crowdin — Drytrix TimeTracker, or Weblate)
- Automatic language detection improvement
- Translation coverage reporting
Support
For questions or issues with translations:
- Check this documentation
- Contributors without Git: see CONTRIBUTING_TRANSLATIONS.md (issue template, spreadsheet option, maintainer workflow, and Crowdin — Drytrix TimeTracker using root
crowdin.ymland the Crowdin sync GitHub Action) - Review
app/__init__.pylocale selector - Inspect browser network requests to
/i18n/set-language - Check application logs for translation compilation errors
Last Updated: 2026-04-29 (catalog sync and babel.cfg extractors note)
Flask-Babel Version: 4.0.0
Babel Version: 2.14.0