KeeFeeRe KeeFeeRe

DevOps Engineer | CI/CD | Kubernetes | Cloud Infrastructure | Automation | Observability 🚀

GitHub LinkedIn Django Facebook Instagram Gmail

Over the past weeks, I’ve been working on localizing and extending the translation system for the FrogPilot - fork of OpenPilot open source car driving assist projects.
This post summarizes the process, challenges, and solutions implemented — especially around Qt translations and handling dynamic alerts.

Background

Qt provides a built-in translation system using:

To enable language support and/or change the translation text, you need to recompile the UI. By default, most but not all UI strings in Frog\OpenPilot wrapped in translated format with tr(). Some dynamic alerts and Python-sourced messages weren’t translatable at all.

Goals

Building and Deployment on Device and Mac

Working with OpenPilot and FrogPilot involves different environments: the target device (usually a comma 3x), local Mac development, and sometimes Docker containers for testing.

Compiling and Updating Translations

To regenerate translation catalogs, Qt provides lupdate:

lupdate -locations none \
  -recursive selfdrive/ui frogpilot/ui \
  -ts selfdrive/ui/translations/main_uk.ts \
  -I . -no-obsolete

update_translations.py script

The standard OpenPilot package includes helpr script, update_translations.py, which helps add and update translation languages.

However, in real testing, you don’t need to update .ts manually every time - that is performed on the device during compilation, and there is no need to do it manually. To add new language it is sufficient to add defenition to /openpilot/selfdrive/ui/translations/languages.json and run the update_translations.py script once.

The Challenge: Dynamic Alerts

OpenPilot displays alerts such as:

These were hardcoded English strings assembled at runtime, and not present in .ts files. That meant they could never be translated with the standard Qt pipeline.

My Solution: Alert Translation Map

I introduce a static translation map (in new alert_tr.h alert translation file) containing known alert templates:

static const std::vector<AlertTranslation> alertTranslations = {
  {"Calibration in Progress: %1%", QT_TRANSLATE_NOOP("Alerts", "Калібрування триває: %1%")},
  {"Drive Above %1", QT_TRANSLATE_NOOP("Alerts", "Рухайтесь швидше ніж %1")},
};

Then vibe-coded a translateAlert() function that:

  1. Matches the incoming alert text against known patterns (using QRegularExpression).
  2. Applies QObject::tr(...) to load the proper translation.
  3. Substitutes parameters (%1, %2, …) with runtime values.

This way, even dynamic messages are properly localized.

Example: Integrating Dynamic Alert Translation

Dynamic alerts are generated at runtime, often containing parameters such as percentages or speed values. my translateAlert() function handles this:

// alert_tr.h
inline QString translateAlert(const QString &text, const QStringList &params = {}) {
for (const auto &alert : alertTranslations) {
QRegularExpression rx(alert.raw_text1); // can include (%1) placeholders
QRegularExpressionMatch match = rx.match(text);
if (match.hasMatch()) {
QString translated = QObject::tr(alert.tr_text1);
// automatically substitute %1, %2 ... if parameters are present
for (int i = 1; i < match.lastCapturedIndex() + 1; ++i) {
translated = translated.arg(match.captured(i));
}
return translated;
}
}
return text; // fallback to original text if no match
}

Usage in alerts.cc when drawing alerts:

if (alert.size == cereal::ControlsState::AlertSize::MID) {
  p.setFont(InterFont(sidebarsOpen ? 78 : 88, QFont::Bold));
  p.drawText(QRect(0, c.y() - 125, width(), 150),
             Qt::AlignHCenter | Qt::AlignTop,
             translateAlert(alert.text1));
  p.setFont(InterFont(sidebarsOpen ? 56 : 66));
  p.drawText(QRect(0, c.y() + 21, width(), 90),
             Qt::AlignHCenter,
             translateAlert(alert.text2));
}

With this setup:

My helper Script

Made helper python script that helps to fill translation dictionary array.

update_alerts.py:

The script requires refinement, as it does not count for complex alerts with high variability, so I had to manually refine the dictionary array a bit.

Localized Formatting

Me also updated date/time and file size displays to respect the selected locale:

QLocale locale(uiState()->language.mid(5));
QString date = locale.toString(QDate::currentDate(), "d MMMM yyyy");

and

if (totalSize >= GB)
  return QString::number(totalSize / GB, 'f', 2) + QObject::tr(" GB");
else
  return QString::number(totalSize / MB, 'f', 2) + QObject::tr(" MB");

So users now see ГБ / МБ and 24-hour time where appropriate.

Lessons Learned

Contribution

Me upstreamed the changes in this pull request: 👉 FrogPilot PR #282

If you’re interested in contributing translations:

Final Thoughts

Translating a large C++/Qt project like OpenPilot is not only about .ts files — it also requires rethinking how runtime messages are generated and displayed. The result is a much more user-friendly interface, now available in Ukrainian, with groundwork laid for other languages.