raylib: wrap text for multilang (#36410)

* fix multilang dialog height

* split to file

* stash

* Revert "stash"

This reverts commit deb4239fe6.

* add updater

* add files

* stuff

* try

rev

* stash

* works!

* works!

* this should be the flow?

* cursor wrapping -- it missed entire sections, changed formatting, and didn't use trn properly!!!!!!!!!!!!!!!!!

* update translations

* learned my lesson

* this should be the one thing it's good at

* update trans

* onroad wrap

* spanish

* rename

* clean up

* load all

* Revert "load all"

This reverts commit 6f2a45861c.

* jp translations

* try jp

* Revert "try jp"

This reverts commit d0524b1011.

* remove languages we can't add rn

* tr

* pt and fr

* ai cannot be trusted

* ai cannot be trusted

* missing trans

* add fonts

* Revert "remove languages we can't add rn"

This reverts commit 73dc75fae2.

* painfully slow to startup

* only load what we need

* Reapply "remove languages we can't add rn"

This reverts commit 52cb48f3b8.

* stash!

* rm

* Revert "stash!"

This reverts commit 31d7c36107.

* revert this

* revert that

* make this dynamic!

* device

* revert

* firehose

* stuff

* revert application

* back

* full revert

* clean up

* network

* more system

* fix dat

* fixy
pull/36412/head
Shane Smiskol 3 days ago committed by GitHub
parent 650946cd2a
commit 9b2f7341d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      selfdrive/ui/layouts/home.py
  2. 15
      selfdrive/ui/layouts/onboarding.py
  3. 19
      selfdrive/ui/layouts/settings/developer.py
  4. 59
      selfdrive/ui/layouts/settings/device.py
  5. 14
      selfdrive/ui/layouts/settings/firehose.py
  6. 13
      selfdrive/ui/layouts/settings/settings.py
  7. 41
      selfdrive/ui/layouts/settings/software.py
  8. 53
      selfdrive/ui/layouts/settings/toggles.py
  9. 39
      selfdrive/ui/layouts/sidebar.py
  10. 13
      selfdrive/ui/onroad/alert_renderer.py
  11. 3
      selfdrive/ui/onroad/driver_camera_dialog.py
  12. 5
      selfdrive/ui/onroad/hud_renderer.py
  13. 4
      selfdrive/ui/update_translations.py
  14. 3
      selfdrive/ui/widgets/exp_mode_button.py
  15. 11
      selfdrive/ui/widgets/offroad_alerts.py
  16. 11
      selfdrive/ui/widgets/pairing_dialog.py
  17. 13
      selfdrive/ui/widgets/prime.py
  18. 15
      selfdrive/ui/widgets/setup.py
  19. 15
      selfdrive/ui/widgets/ssh_key.py
  20. 10
      system/ui/lib/multilang.py
  21. 9
      system/ui/widgets/confirm_dialog.py
  22. 3
      system/ui/widgets/html_render.py
  23. 5
      system/ui/widgets/keyboard.py
  24. 5
      system/ui/widgets/list_view.py
  25. 49
      system/ui/widgets/network.py
  26. 5
      system/ui/widgets/option_dialog.py

@ -9,6 +9,7 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -151,7 +152,7 @@ class HomeLayout(Widget):
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255) highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color) rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
text = "UPDATE" text = tr("UPDATE")
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE) text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2 text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2 text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
@ -165,7 +166,7 @@ class HomeLayout(Widget):
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255) highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color) rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
alert_text = f"{self.alert_count} ALERT{'S' if self.alert_count > 1 else ''}" alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE) text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2 text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2 text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2

@ -6,6 +6,7 @@ from enum import IntEnum
import pyray as rl import pyray as rl
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.widgets.label import Label
@ -107,12 +108,12 @@ class TermsPage(Widget):
self._on_accept = on_accept self._on_accept = on_accept
self._on_decline = on_decline self._on_decline = on_decline
self._title = Label("Welcome to openpilot", font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing.", self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button("Decline", click_callback=on_decline) self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button("Agree", button_style=ButtonStyle.PRIMARY, click_callback=on_accept) self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
def _render(self, _): def _render(self, _):
welcome_x = self._rect.x + 165 welcome_x = self._rect.x + 165
@ -141,10 +142,10 @@ class TermsPage(Widget):
class DeclinePage(Widget): class DeclinePage(Widget):
def __init__(self, back_callback=None): def __init__(self, back_callback=None):
super().__init__() super().__init__()
self._text = Label("You must accept the Terms and Conditions in order to use openpilot.", self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button("Back", click_callback=back_callback) self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button("Decline, uninstall openpilot", button_style=ButtonStyle.DANGER, self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked) click_callback=self._on_uninstall_clicked)
def _on_uninstall_clicked(self): def _on_uninstall_clicked(self):

@ -6,19 +6,20 @@ from openpilot.system.ui.widgets.list_view import toggle_item
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
# Description constants # Description constants
DESCRIPTIONS = { DESCRIPTIONS = {
'enable_adb': ( 'enable_adb': tr(
"ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " + "ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. " +
"See https://docs.comma.ai/how-to/connect-to-comma for more info." "See https://docs.comma.ai/how-to/connect-to-comma for more info."
), ),
'ssh_key': ( 'ssh_key': tr(
"Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " + "Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " +
"other than your own. A comma employee will NEVER ask you to add their GitHub username." "other than your own. A comma employee will NEVER ask you to add their GitHub username."
), ),
'alpha_longitudinal': ( 'alpha_longitudinal': tr(
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" + "<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " + "On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha." "Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha."
@ -34,7 +35,7 @@ class DeveloperLayout(Widget):
# Build items and keep references for callbacks/state updates # Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item( self._adb_toggle = toggle_item(
"Enable ADB", tr("Enable ADB"),
description=DESCRIPTIONS["enable_adb"], description=DESCRIPTIONS["enable_adb"],
initial_state=self._params.get_bool("AdbEnabled"), initial_state=self._params.get_bool("AdbEnabled"),
callback=self._on_enable_adb, callback=self._on_enable_adb,
@ -43,7 +44,7 @@ class DeveloperLayout(Widget):
# SSH enable toggle + SSH key management # SSH enable toggle + SSH key management
self._ssh_toggle = toggle_item( self._ssh_toggle = toggle_item(
"Enable SSH", tr("Enable SSH"),
description="", description="",
initial_state=self._params.get_bool("SshEnabled"), initial_state=self._params.get_bool("SshEnabled"),
callback=self._on_enable_ssh, callback=self._on_enable_ssh,
@ -51,7 +52,7 @@ class DeveloperLayout(Widget):
self._ssh_keys = ssh_key_item("SSH Keys", description=DESCRIPTIONS["ssh_key"]) self._ssh_keys = ssh_key_item("SSH Keys", description=DESCRIPTIONS["ssh_key"])
self._joystick_toggle = toggle_item( self._joystick_toggle = toggle_item(
"Joystick Debug Mode", tr("Joystick Debug Mode"),
description="", description="",
initial_state=self._params.get_bool("JoystickDebugMode"), initial_state=self._params.get_bool("JoystickDebugMode"),
callback=self._on_joystick_debug_mode, callback=self._on_joystick_debug_mode,
@ -59,14 +60,14 @@ class DeveloperLayout(Widget):
) )
self._long_maneuver_toggle = toggle_item( self._long_maneuver_toggle = toggle_item(
"Longitudinal Maneuver Mode", tr("Longitudinal Maneuver Mode"),
description="", description="",
initial_state=self._params.get_bool("LongitudinalManeuverMode"), initial_state=self._params.get_bool("LongitudinalManeuverMode"),
callback=self._on_long_maneuver_mode, callback=self._on_long_maneuver_mode,
) )
self._alpha_long_toggle = toggle_item( self._alpha_long_toggle = toggle_item(
"openpilot Longitudinal Control (Alpha)", tr("openpilot Longitudinal Control (Alpha)"),
description=DESCRIPTIONS["alpha_longitudinal"], description=DESCRIPTIONS["alpha_longitudinal"],
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"), initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled, callback=self._on_alpha_long_enabled,
@ -163,7 +164,7 @@ class DeveloperLayout(Widget):
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" + content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
f"<p>{self._alpha_long_toggle.description}</p>") f"<p>{self._alpha_long_toggle.description}</p>")
dlg = ConfirmDialog(content, "Enable", rich=True) dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback) gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else: else:

@ -12,6 +12,7 @@ from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.html_render import HtmlModal from openpilot.system.ui.widgets.html_render import HtmlModal
@ -21,10 +22,10 @@ from openpilot.system.ui.widgets.scroller import Scroller
# Description constants # Description constants
DESCRIPTIONS = { DESCRIPTIONS = {
'pair_device': "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.", 'pair_device': tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
'driver_camera': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", 'driver_camera': tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
'reset_calibration': "openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down.", 'reset_calibration': tr("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': "Review the rules, features, and limitations of openpilot", 'review_guide': tr("Review the rules, features, and limitations of openpilot"),
} }
@ -45,27 +46,27 @@ class DeviceLayout(Widget):
ui_state.add_offroad_transition_callback(self._offroad_transition) ui_state.add_offroad_transition_callback(self._offroad_transition)
def _initialize_items(self): def _initialize_items(self):
dongle_id = self._params.get("DongleId") or "N/A" dongle_id = self._params.get("DongleId") or tr("N/A")
serial = self._params.get("HardwareSerial") or "N/A" serial = self._params.get("HardwareSerial") or tr("N/A")
self._pair_device_btn = button_item("Pair Device", "PAIR", DESCRIPTIONS['pair_device'], callback=self._pair_device) self._pair_device_btn = button_item(tr("Pair Device"), tr("PAIR"), DESCRIPTIONS['pair_device'], callback=self._pair_device)
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired()) self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
self._reset_calib_btn = button_item("Reset Calibration", "RESET", DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt) self._reset_calib_btn = button_item(tr("Reset Calibration"), tr("RESET"), DESCRIPTIONS['reset_calibration'], callback=self._reset_calibration_prompt)
self._reset_calib_btn.set_description_opened_callback(self._update_calib_description) self._reset_calib_btn.set_description_opened_callback(self._update_calib_description)
self._power_off_btn = dual_button_item("Reboot", "Power Off", left_callback=self._reboot_prompt, right_callback=self._power_off_prompt) self._power_off_btn = dual_button_item(tr("Reboot"), tr("Power Off"), left_callback=self._reboot_prompt, right_callback=self._power_off_prompt)
items = [ items = [
text_item("Dongle ID", dongle_id), text_item(tr("Dongle ID"), dongle_id),
text_item("Serial", serial), text_item(tr("Serial"), serial),
self._pair_device_btn, self._pair_device_btn,
button_item("Driver Camera", "PREVIEW", DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad), button_item(tr("Driver Camera"), tr("PREVIEW"), DESCRIPTIONS['driver_camera'], callback=self._show_driver_camera, enabled=ui_state.is_offroad),
self._reset_calib_btn, self._reset_calib_btn,
button_item("Review Training Guide", "REVIEW", DESCRIPTIONS['review_guide'], self._on_review_training_guide, enabled=ui_state.is_offroad), button_item(tr("Review Training Guide"), tr("REVIEW"), DESCRIPTIONS['review_guide'], self._on_review_training_guide, enabled=ui_state.is_offroad),
regulatory_btn := button_item("Regulatory", "VIEW", callback=self._on_regulatory, enabled=ui_state.is_offroad), regulatory_btn := button_item(tr("Regulatory"), tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad),
# TODO: implement multilang # TODO: implement multilang
# button_item("Change Language", "CHANGE", callback=self._show_language_selection, enabled=ui_state.is_offroad), # button_item(tr("Change Language"), tr("CHANGE"), callback=self._show_language_selection, enabled=ui_state.is_offroad),
self._power_off_btn, self._power_off_btn,
] ]
regulatory_btn.set_visible(TICI) regulatory_btn.set_visible(TICI)
@ -106,7 +107,7 @@ class DeviceLayout(Widget):
def _reset_calibration_prompt(self): def _reset_calibration_prompt(self):
if ui_state.engaged: if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog("Disengage to Reset Calibration")) gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
return return
def reset_calibration(result: int): def reset_calibration(result: int):
@ -122,7 +123,7 @@ class DeviceLayout(Widget):
self._params.put_bool("OnroadCycleRequested", True) self._params.put_bool("OnroadCycleRequested", True)
self._update_calib_description() self._update_calib_description()
dialog = ConfirmDialog("Are you sure you want to reset calibration?", "Reset") dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
gui_app.set_modal_overlay(dialog, callback=reset_calibration) gui_app.set_modal_overlay(dialog, callback=reset_calibration)
def _update_calib_description(self): def _update_calib_description(self):
@ -136,7 +137,8 @@ class DeviceLayout(Widget):
if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated: if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated:
pitch = math.degrees(calib.rpyCalib[1]) pitch = math.degrees(calib.rpyCalib[1])
yaw = math.degrees(calib.rpyCalib[2]) yaw = math.degrees(calib.rpyCalib[2])
desc += f" Your device is pointed {abs(pitch):.1f}° {'down' if pitch > 0 else 'up'} and {abs(yaw):.1f}° {'left' if yaw > 0 else 'right'}." desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"),
abs(yaw), tr("left") if yaw > 0 else tr("right"))
except Exception: except Exception:
cloudlog.exception("invalid CalibrationParams") cloudlog.exception("invalid CalibrationParams")
@ -148,9 +150,9 @@ class DeviceLayout(Widget):
except Exception: except Exception:
cloudlog.exception("invalid LiveDelay") cloudlog.exception("invalid LiveDelay")
if lag_perc < 100: if lag_perc < 100:
desc += f"<br><br>Steering lag calibration is {lag_perc}% complete." desc += tr("<br><br>Steering lag calibration is {}% complete.").format(lag_perc)
else: else:
desc += "<br><br>Steering lag calibration is complete." desc += tr("<br><br>Steering lag calibration is complete.")
torque_bytes = self._params.get("LiveTorqueParameters") torque_bytes = self._params.get("LiveTorqueParameters")
if torque_bytes: if torque_bytes:
@ -160,24 +162,24 @@ class DeviceLayout(Widget):
if torque.useParams: if torque.useParams:
torque_perc = torque.calPerc torque_perc = torque.calPerc
if torque_perc < 100: if torque_perc < 100:
desc += f" Steering torque response calibration is {torque_perc}% complete." desc += tr(" Steering torque response calibration is {}% complete.").format(torque_perc)
else: else:
desc += " Steering torque response calibration is complete." desc += tr(" Steering torque response calibration is complete.")
except Exception: except Exception:
cloudlog.exception("invalid LiveTorqueParameters") cloudlog.exception("invalid LiveTorqueParameters")
desc += "<br><br>" desc += "<br><br>"
desc += ("openpilot is continuously calibrating, resetting is rarely required. " + desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
"Resetting calibration will restart openpilot if the car is powered on.") "Resetting calibration will restart openpilot if the car is powered on.")
self._reset_calib_btn.set_description(desc) self._reset_calib_btn.set_description(desc)
def _reboot_prompt(self): def _reboot_prompt(self):
if ui_state.engaged: if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog("Disengage to Reboot")) gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
return return
dialog = ConfirmDialog("Are you sure you want to reboot?", "Reboot") dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot) gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
def _perform_reboot(self, result: int): def _perform_reboot(self, result: int):
@ -186,10 +188,10 @@ class DeviceLayout(Widget):
def _power_off_prompt(self): def _power_off_prompt(self):
if ui_state.engaged: if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog("Disengage to Power Off")) gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
return return
dialog = ConfirmDialog("Are you sure you want to power off?", "Power Off") dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off) gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
def _perform_power_off(self, result: int): def _perform_power_off(self, result: int):
@ -210,5 +212,6 @@ class DeviceLayout(Widget):
if not self._training_guide: if not self._training_guide:
def completed_callback(): def completed_callback():
gui_app.set_modal_overlay(None) gui_app.set_modal_overlay(None)
self._training_guide = TrainingGuide(completed_callback=completed_callback) self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide) gui_app.set_modal_overlay(self._training_guide)

@ -8,19 +8,20 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.lib.api_helpers import get_token from openpilot.selfdrive.ui.lib.api_helpers import get_token
TITLE = "Firehose Mode" TITLE = tr("Firehose Mode")
DESCRIPTION = ( DESCRIPTION = tr(
"openpilot learns to drive by watching humans, like you, drive.\n\n" "openpilot learns to drive by watching humans, like you, drive.\n\n"
+ "Firehose Mode allows you to maximize your training data uploads to improve " + "Firehose Mode allows you to maximize your training data uploads to improve "
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode." + "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
) )
INSTRUCTIONS = ( INSTRUCTIONS = tr(
"For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n" "For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.\n\n"
+ "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n" + "Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.\n\n\n"
+ "Frequently Asked Questions\n\n" + "Frequently Asked Questions\n\n"
@ -106,7 +107,8 @@ class FirehoseLayout(Widget):
# Contribution count (if available) # Contribution count (if available)
if self.segment_count > 0: if self.segment_count > 0:
contrib_text = f"{self.segment_count} segment(s) of your driving is in the training dataset so far." contrib_text = trn("{} segment of your driving is in the training dataset so far.",
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE) y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
y += 20 + 20 y += 20 + 20
@ -132,9 +134,9 @@ class FirehoseLayout(Widget):
network_metered = ui_state.sm["deviceState"].networkMetered network_metered = ui_state.sm["deviceState"].networkMetered
if not network_metered and network_type != 0: # Not metered and connected if not network_metered and network_type != 0: # Not metered and connected
return "ACTIVE", self.GREEN return tr("ACTIVE"), self.GREEN
else: else:
return "INACTIVE: connect to an unmetered network", self.RED return tr("INACTIVE: connect to an unmetered network"), self.RED
def _fetch_firehose_stats(self): def _fetch_firehose_stats(self):
try: try:

@ -8,6 +8,7 @@ from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -58,12 +59,12 @@ class SettingsLayout(Widget):
wifi_manager.set_active(False) wifi_manager.set_active(False)
self._panels = { self._panels = {
PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), PanelType.DEVICE: PanelInfo(tr("Device"), DeviceLayout()),
PanelType.NETWORK: PanelInfo("Network", NetworkUI(wifi_manager)), PanelType.NETWORK: PanelInfo(tr("Network"), NetworkUI(wifi_manager)),
PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.TOGGLES: PanelInfo(tr("Toggles"), TogglesLayout()),
PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), PanelType.SOFTWARE: PanelInfo(tr("Software"), SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), PanelType.FIREHOSE: PanelInfo(tr("Firehose"), FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()), PanelType.DEVELOPER: PanelInfo(tr("Developer"), DeveloperLayout()),
} }
self._font_medium = gui_app.font(FontWeight.MEDIUM) self._font_medium = gui_app.font(FontWeight.MEDIUM)

@ -4,6 +4,7 @@ import datetime
from openpilot.common.time_helpers import system_time_valid from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem from openpilot.system.ui.widgets.list_view import button_item, text_item, ListItem
@ -15,7 +16,7 @@ UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
def time_ago(date: datetime.datetime | None) -> str: def time_ago(date: datetime.datetime | None) -> str:
if not date: if not date:
return "never" return tr("never")
if not system_time_valid(): if not system_time_valid():
return date.strftime("%a %b %d %Y") return date.strftime("%a %b %d %Y")
@ -26,16 +27,16 @@ def time_ago(date: datetime.datetime | None) -> str:
diff_seconds = int((now - date).total_seconds()) diff_seconds = int((now - date).total_seconds())
if diff_seconds < 60: if diff_seconds < 60:
return "now" return tr("now")
if diff_seconds < 3600: if diff_seconds < 3600:
m = diff_seconds // 60 m = diff_seconds // 60
return f"{m} minute{'s' if m != 1 else ''} ago" return trn("{} minute ago", "{} minutes ago", m).format(m)
if diff_seconds < 86400: if diff_seconds < 86400:
h = diff_seconds // 3600 h = diff_seconds // 3600
return f"{h} hour{'s' if h != 1 else ''} ago" return trn("{} hour ago", "{} hours ago", h).format(h)
if diff_seconds < 604800: if diff_seconds < 604800:
d = diff_seconds // 86400 d = diff_seconds // 86400
return f"{d} day{'s' if d != 1 else ''} ago" return trn("{} day ago", "{} days ago", d).format(d)
return date.strftime("%a %b %d %Y") return date.strftime("%a %b %d %Y")
@ -43,12 +44,12 @@ class SoftwareLayout(Widget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._onroad_label = ListItem(title="Updates are only downloaded while the car is off.") self._onroad_label = ListItem(title=tr("Updates are only downloaded while the car is off."))
self._version_item = text_item("Current Version", ui_state.params.get("UpdaterCurrentDescription") or "") self._version_item = text_item(tr("Current Version"), ui_state.params.get("UpdaterCurrentDescription") or "")
self._download_btn = button_item("Download", "CHECK", callback=self._on_download_update) self._download_btn = button_item(tr("Download"), tr("CHECK"), callback=self._on_download_update)
# Install button is initially hidden # Install button is initially hidden
self._install_btn = button_item("Install Update", "INSTALL", callback=self._on_install_update) self._install_btn = button_item(tr("Install Update"), tr("INSTALL"), callback=self._on_install_update)
self._install_btn.set_visible(False) self._install_btn.set_visible(False)
# Track waiting-for-updater transition to avoid brief re-enable while still idle # Track waiting-for-updater transition to avoid brief re-enable while still idle
@ -66,7 +67,7 @@ class SoftwareLayout(Widget):
self._install_btn, self._install_btn,
# TODO: implement branch switching # TODO: implement branch switching
# button_item("Target Branch", "SELECT", callback=self._on_select_branch), # button_item("Target Branch", "SELECT", callback=self._on_select_branch),
button_item("Uninstall", "UNINSTALL", callback=self._on_uninstall), button_item("Uninstall", tr("UNINSTALL"), callback=self._on_uninstall),
] ]
return items return items
@ -101,19 +102,19 @@ class SoftwareLayout(Widget):
self._download_btn.action_item.set_value(updater_state) self._download_btn.action_item.set_value(updater_state)
else: else:
if failed_count > 0: if failed_count > 0:
self._download_btn.action_item.set_value("failed to check for update") self._download_btn.action_item.set_value(tr("failed to check for update"))
self._download_btn.action_item.set_text("CHECK") self._download_btn.action_item.set_text(tr("CHECK"))
elif fetch_available: elif fetch_available:
self._download_btn.action_item.set_value("update available") self._download_btn.action_item.set_value(tr("update available"))
self._download_btn.action_item.set_text("DOWNLOAD") self._download_btn.action_item.set_text(tr("DOWNLOAD"))
else: else:
last_update = ui_state.params.get("LastUpdateTime") last_update = ui_state.params.get("LastUpdateTime")
if last_update: if last_update:
formatted = time_ago(last_update) formatted = time_ago(last_update)
self._download_btn.action_item.set_value(f"up to date, last checked {formatted}") self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
else: else:
self._download_btn.action_item.set_value("up to date, last checked never") self._download_btn.action_item.set_value(tr("up to date, last checked never"))
self._download_btn.action_item.set_text("CHECK") self._download_btn.action_item.set_text(tr("CHECK"))
# If we've been waiting too long without a state change, reset state # If we've been waiting too long without a state change, reset state
if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT): if self._waiting_for_updater and (time.monotonic() - self._waiting_start_ts > UPDATED_TIMEOUT):
@ -127,7 +128,7 @@ class SoftwareLayout(Widget):
if update_available: if update_available:
new_desc = ui_state.params.get("UpdaterNewDescription") or "" new_desc = ui_state.params.get("UpdaterNewDescription") or ""
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace") new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
self._install_btn.action_item.set_text("INSTALL") self._install_btn.action_item.set_text(tr("INSTALL"))
self._install_btn.action_item.set_value(new_desc) self._install_btn.action_item.set_value(new_desc)
self._install_btn.set_description(new_release_notes) self._install_btn.set_description(new_release_notes)
# Enable install button for testing (like Qt showEvent) # Enable install button for testing (like Qt showEvent)
@ -138,7 +139,7 @@ class SoftwareLayout(Widget):
def _on_download_update(self): def _on_download_update(self):
# Check if we should start checking or start downloading # Check if we should start checking or start downloading
self._download_btn.action_item.set_enabled(False) self._download_btn.action_item.set_enabled(False)
if self._download_btn.action_item.text == "CHECK": if self._download_btn.action_item.text == tr("CHECK"):
# Start checking for updates # Start checking for updates
self._waiting_for_updater = True self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic() self._waiting_start_ts = time.monotonic()
@ -154,7 +155,7 @@ class SoftwareLayout(Widget):
if result == DialogResult.CONFIRM: if result == DialogResult.CONFIRM:
ui_state.params.put_bool("DoUninstall", True) ui_state.params.put_bool("DoUninstall", True)
dialog = ConfirmDialog("Are you sure you want to uninstall?", "Uninstall") dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation) gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
def _on_install_update(self): def _on_install_update(self):

@ -5,6 +5,7 @@ from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_i
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
@ -12,24 +13,24 @@ PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
# Description constants # Description constants
DESCRIPTIONS = { DESCRIPTIONS = {
"OpenpilotEnabledToggle": ( "OpenpilotEnabledToggle": tr(
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " + "Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
"Your attention is required at all times to use this feature." "Your attention is required at all times to use this feature."
), ),
"DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.", "DisengageOnAccelerator": tr("When enabled, pressing the accelerator pedal will disengage openpilot."),
"LongitudinalPersonality": ( "LongitudinalPersonality": tr(
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " + "Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " + "In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"your steering wheel distance button." "your steering wheel distance button."
), ),
"IsLdwEnabled": ( "IsLdwEnabled": tr(
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " + "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)." "without a turn signal activated while driving over 31 mph (50 km/h)."
), ),
"AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", "AlwaysOnDM": tr("Enable driver monitoring even when openpilot is not engaged."),
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", 'RecordFront': tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
"IsMetric": "Display speed in km/h instead of mph.", "IsMetric": tr("Display speed in km/h instead of mph."),
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.", "RecordAudio": tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
} }
@ -42,49 +43,49 @@ class TogglesLayout(Widget):
# param, title, desc, icon, needs_restart # param, title, desc, icon, needs_restart
self._toggle_defs = { self._toggle_defs = {
"OpenpilotEnabledToggle": ( "OpenpilotEnabledToggle": (
"Enable openpilot", tr("Enable openpilot"),
DESCRIPTIONS["OpenpilotEnabledToggle"], DESCRIPTIONS["OpenpilotEnabledToggle"],
"chffr_wheel.png", "chffr_wheel.png",
True, True,
), ),
"ExperimentalMode": ( "ExperimentalMode": (
"Experimental Mode", tr("Experimental Mode"),
"", "",
"experimental_white.png", "experimental_white.png",
False, False,
), ),
"DisengageOnAccelerator": ( "DisengageOnAccelerator": (
"Disengage on Accelerator Pedal", tr("Disengage on Accelerator Pedal"),
DESCRIPTIONS["DisengageOnAccelerator"], DESCRIPTIONS["DisengageOnAccelerator"],
"disengage_on_accelerator.png", "disengage_on_accelerator.png",
False, False,
), ),
"IsLdwEnabled": ( "IsLdwEnabled": (
"Enable Lane Departure Warnings", tr("Enable Lane Departure Warnings"),
DESCRIPTIONS["IsLdwEnabled"], DESCRIPTIONS["IsLdwEnabled"],
"warning.png", "warning.png",
False, False,
), ),
"AlwaysOnDM": ( "AlwaysOnDM": (
"Always-On Driver Monitoring", tr("Always-On Driver Monitoring"),
DESCRIPTIONS["AlwaysOnDM"], DESCRIPTIONS["AlwaysOnDM"],
"monitoring.png", "monitoring.png",
False, False,
), ),
"RecordFront": ( "RecordFront": (
"Record and Upload Driver Camera", tr("Record and Upload Driver Camera"),
DESCRIPTIONS["RecordFront"], DESCRIPTIONS["RecordFront"],
"monitoring.png", "monitoring.png",
True, True,
), ),
"RecordAudio": ( "RecordAudio": (
"Record and Upload Microphone Audio", tr("Record and Upload Microphone Audio"),
DESCRIPTIONS["RecordAudio"], DESCRIPTIONS["RecordAudio"],
"microphone.png", "microphone.png",
True, True,
), ),
"IsMetric": ( "IsMetric": (
"Use Metric System", tr("Use Metric System"),
DESCRIPTIONS["IsMetric"], DESCRIPTIONS["IsMetric"],
"metric.png", "metric.png",
False, False,
@ -92,9 +93,9 @@ class TogglesLayout(Widget):
} }
self._long_personality_setting = multiple_button_item( self._long_personality_setting = multiple_button_item(
"Driving Personality", tr("Driving Personality"),
DESCRIPTIONS["LongitudinalPersonality"], DESCRIPTIONS["LongitudinalPersonality"],
buttons=["Aggressive", "Standard", "Relaxed"], buttons=[tr("Aggressive"), tr("Standard"), tr("Relaxed")],
button_width=255, button_width=255,
callback=self._set_longitudinal_personality, callback=self._set_longitudinal_personality,
selected_index=self._params.get("LongitudinalPersonality", return_default=True), selected_index=self._params.get("LongitudinalPersonality", return_default=True),
@ -119,7 +120,7 @@ class TogglesLayout(Widget):
toggle.action_item.set_enabled(not locked) toggle.action_item.set_enabled(not locked)
if needs_restart and not locked: if needs_restart and not locked:
toggle.set_description(toggle.description + " Changing this setting will restart openpilot if the car is powered on.") toggle.set_description(toggle.description + tr(" Changing this setting will restart openpilot if the car is powered on."))
# track for engaged state updates # track for engaged state updates
if locked: if locked:
@ -150,7 +151,7 @@ class TogglesLayout(Widget):
def _update_toggles(self): def _update_toggles(self):
ui_state.update_params() ui_state.update_params()
e2e_description = ( e2e_description = tr(
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " + "openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
"Experimental features are listed below:<br>" + "Experimental features are listed below:<br>" +
"<h4>End-to-End Longitudinal Control</h4><br>" + "<h4>End-to-End Longitudinal Control</h4><br>" +
@ -174,15 +175,15 @@ class TogglesLayout(Widget):
self._long_personality_setting.action_item.set_enabled(False) self._long_personality_setting.action_item.set_enabled(False)
self._params.remove("ExperimentalMode") self._params.remove("ExperimentalMode")
unavailable = "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
long_desc = unavailable + " openpilot longitudinal control may come in a future update." long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
if ui_state.CP.alphaLongitudinalAvailable: if ui_state.CP.alphaLongitudinalAvailable:
if self._is_release: if self._is_release:
long_desc = unavailable + " " + ("An alpha version of openpilot longitudinal control can be tested, along with " + long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
"Experimental mode, on non-release branches.") "Experimental mode, on non-release branches.")
else: else:
long_desc = "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description) self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
else: else:
@ -221,7 +222,7 @@ class TogglesLayout(Widget):
# show confirmation dialog # show confirmation dialog
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" + content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>") f"<p>{self._toggles['ExperimentalMode'].description}</p>")
dlg = ConfirmDialog(content, "Enable", rich=True) dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback) gui_app.set_modal_overlay(dlg, callback=confirm_callback)
else: else:
self._update_experimental_mode_icon() self._update_experimental_mode_icon()

@ -5,6 +5,7 @@ from collections.abc import Callable
from cereal import log from cereal import log
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -39,13 +40,13 @@ class Colors:
NETWORK_TYPES = { NETWORK_TYPES = {
NetworkType.none: "--", NetworkType.none: tr("--"),
NetworkType.wifi: "Wi-Fi", NetworkType.wifi: tr("Wi-Fi"),
NetworkType.ethernet: "ETH", NetworkType.ethernet: tr("ETH"),
NetworkType.cell2G: "2G", NetworkType.cell2G: tr("2G"),
NetworkType.cell3G: "3G", NetworkType.cell3G: tr("3G"),
NetworkType.cell4G: "LTE", NetworkType.cell4G: tr("LTE"),
NetworkType.cell5G: "5G", NetworkType.cell5G: tr("5G"),
} }
@ -67,9 +68,9 @@ class Sidebar(Widget):
self._net_type = NETWORK_TYPES.get(NetworkType.none) self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_strength = 0 self._net_strength = 0
self._temp_status = MetricData("TEMP", "GOOD", Colors.GOOD) self._temp_status = MetricData(tr("TEMP"), tr("GOOD"), Colors.GOOD)
self._panda_status = MetricData("VEHICLE", "ONLINE", Colors.GOOD) self._panda_status = MetricData(tr("VEHICLE"), tr("ONLINE"), Colors.GOOD)
self._connect_status = MetricData("CONNECT", "OFFLINE", Colors.WARNING) self._connect_status = MetricData(tr("CONNECT"), tr("OFFLINE"), Colors.WARNING)
self._recording_audio = False self._recording_audio = False
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height) self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
@ -113,7 +114,7 @@ class Sidebar(Widget):
self._update_panda_status() self._update_panda_status()
def _update_network_status(self, device_state): def _update_network_status(self, device_state):
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, "Unknown") self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr("Unknown"))
strength = device_state.networkStrength strength = device_state.networkStrength
self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0 self._net_strength = max(0, min(5, strength.raw + 1)) if strength > 0 else 0
@ -121,26 +122,26 @@ class Sidebar(Widget):
thermal_status = device_state.thermalStatus thermal_status = device_state.thermalStatus
if thermal_status == ThermalStatus.green: if thermal_status == ThermalStatus.green:
self._temp_status.update("TEMP", "GOOD", Colors.GOOD) self._temp_status.update(tr("TEMP"), tr("GOOD"), Colors.GOOD)
elif thermal_status == ThermalStatus.yellow: elif thermal_status == ThermalStatus.yellow:
self._temp_status.update("TEMP", "OK", Colors.WARNING) self._temp_status.update(tr("TEMP"), tr("OK"), Colors.WARNING)
else: else:
self._temp_status.update("TEMP", "HIGH", Colors.DANGER) self._temp_status.update(tr("TEMP"), tr("HIGH"), Colors.DANGER)
def _update_connection_status(self, device_state): def _update_connection_status(self, device_state):
last_ping = device_state.lastAthenaPingTime last_ping = device_state.lastAthenaPingTime
if last_ping == 0: if last_ping == 0:
self._connect_status.update("CONNECT", "OFFLINE", Colors.WARNING) self._connect_status.update(tr("CONNECT"), tr("OFFLINE"), Colors.WARNING)
elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds elif time.monotonic_ns() - last_ping < 80_000_000_000: # 80 seconds in nanoseconds
self._connect_status.update("CONNECT", "ONLINE", Colors.GOOD) self._connect_status.update(tr("CONNECT"), tr("ONLINE"), Colors.GOOD)
else: else:
self._connect_status.update("CONNECT", "ERROR", Colors.DANGER) self._connect_status.update(tr("CONNECT"), tr("ERROR"), Colors.DANGER)
def _update_panda_status(self): def _update_panda_status(self):
if ui_state.panda_type == log.PandaState.PandaType.unknown: if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._panda_status.update("NO", "PANDA", Colors.DANGER) self._panda_status.update(tr("NO"), tr("PANDA"), Colors.DANGER)
else: else:
self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) self._panda_status.update(tr("VEHICLE"), tr("ONLINE"), Colors.GOOD)
def _handle_mouse_release(self, mouse_pos: MousePos): def _handle_mouse_release(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):

@ -5,6 +5,7 @@ from cereal import messaging, log
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import TICI from openpilot.system.hardware import TICI
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.widgets.label import Label
@ -47,22 +48,22 @@ class Alert:
# Pre-defined alert instances # Pre-defined alert instances
ALERT_STARTUP_PENDING = Alert( ALERT_STARTUP_PENDING = Alert(
text1="openpilot Unavailable", text1=tr("openpilot Unavailable"),
text2="Waiting to start", text2=tr("Waiting to start"),
size=AlertSize.mid, size=AlertSize.mid,
status=AlertStatus.normal, status=AlertStatus.normal,
) )
ALERT_CRITICAL_TIMEOUT = Alert( ALERT_CRITICAL_TIMEOUT = Alert(
text1="TAKE CONTROL IMMEDIATELY", text1=tr("TAKE CONTROL IMMEDIATELY"),
text2="System Unresponsive", text2=tr("System Unresponsive"),
size=AlertSize.full, size=AlertSize.full,
status=AlertStatus.critical, status=AlertStatus.critical,
) )
ALERT_CRITICAL_REBOOT = Alert( ALERT_CRITICAL_REBOOT = Alert(
text1="System Unresponsive", text1=tr("System Unresponsive"),
text2="Reboot Device", text2=tr("Reboot Device"),
size=AlertSize.mid, size=AlertSize.mid,
status=AlertStatus.normal, status=AlertStatus.normal,
) )

@ -5,6 +5,7 @@ from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
@ -30,7 +31,7 @@ class DriverCameraDialog(CameraView):
if not self.frame: if not self.frame:
gui_label( gui_label(
rect, rect,
"camera starting", tr("camera starting"),
font_size=100, font_size=100,
font_weight=FontWeight.BOLD, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,

@ -4,6 +4,7 @@ from openpilot.common.constants import CV
from openpilot.selfdrive.ui.onroad.exp_button import ExpButton from openpilot.selfdrive.ui.onroad.exp_button import ExpButton
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -144,7 +145,7 @@ class HudRenderer(Widget):
elif ui_state.status == UIStatus.OVERRIDE: elif ui_state.status == UIStatus.OVERRIDE:
max_color = COLORS.override max_color = COLORS.override
max_text = "MAX" max_text = tr("MAX")
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
rl.draw_text_ex( rl.draw_text_ex(
self._font_semi_bold, self._font_semi_bold,
@ -173,7 +174,7 @@ class HudRenderer(Widget):
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
unit_text = "km/h" if ui_state.is_metric else "mph" unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent) rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)

@ -4,10 +4,8 @@ import json
import os import os
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.multilang import UI_DIR, TRANSLATIONS_DIR, LANGUAGES_FILE
UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui")
TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations")
LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h") TRANSLATIONS_INCLUDE_FILE = os.path.join(TRANSLATIONS_DIR, "alerts_generated.h")
PLURAL_ONLY = ["en"] # base language, only create entries for strings with plural forms PLURAL_ONLY = ["en"] # base language, only create entries for strings with plural forms

@ -1,6 +1,7 @@
import pyray as rl import pyray as rl
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -46,7 +47,7 @@ class ExperimentalModeButton(Widget):
rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color) rl.draw_line_ex(rl.Vector2(line_x, rect.y), rl.Vector2(line_x, rect.y + rect.height), 3, separator_color)
# Draw text label (left aligned) # Draw text label (left aligned)
text = "EXPERIMENTAL MODE ON" if self.experimental_mode else "CHILL MODE ON" text = tr("EXPERIMENTAL MODE ON") if self.experimental_mode else tr("CHILL MODE ON")
text_x = rect.x + self.horizontal_padding text_x = rect.x + self.horizontal_padding
text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically text_y = rect.y + rect.height / 2 - 45 * FONT_SCALE // 2 # Center vertically

@ -6,6 +6,7 @@ from dataclasses import dataclass
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
@ -14,7 +15,7 @@ from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
NO_RELEASE_NOTES = "<h2>No release notes available.</h2>" NO_RELEASE_NOTES = tr("<h2>No release notes available.</h2>")
class AlertColors: class AlertColors:
@ -101,15 +102,15 @@ class AbstractAlert(Widget, ABC):
if self.dismiss_callback: if self.dismiss_callback:
self.dismiss_callback() self.dismiss_callback()
self.dismiss_btn = ActionButton("Close") self.dismiss_btn = ActionButton(tr("Close"))
self.snooze_btn = ActionButton("Snooze Update", style=ButtonStyle.DARK) self.snooze_btn = ActionButton(tr("Snooze Update"), style=ButtonStyle.DARK)
self.snooze_btn.set_click_callback(snooze_callback) self.snooze_btn.set_click_callback(snooze_callback)
self.excessive_actuation_btn = ActionButton("Acknowledge Excessive Actuation", style=ButtonStyle.DARK, min_width=800) self.excessive_actuation_btn = ActionButton(tr("Acknowledge Excessive Actuation"), style=ButtonStyle.DARK, min_width=800)
self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback) self.excessive_actuation_btn.set_click_callback(excessive_actuation_callback)
self.reboot_btn = ActionButton("Reboot and Update", min_width=600) self.reboot_btn = ActionButton(tr("Reboot and Update"), min_width=600)
self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot()) self.reboot_btn.set_click_callback(lambda: HARDWARE.reboot())
# TODO: just use a Scroller? # TODO: just use a Scroller?

@ -8,6 +8,7 @@ from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
@ -99,7 +100,7 @@ class PairingDialog(Widget):
y += close_size + 40 y += close_size + 40
# Title # Title
title = "Pair your device to your comma account" title = tr("Pair your device to your comma account")
title_font = gui_app.font(FontWeight.NORMAL) title_font = gui_app.font(FontWeight.NORMAL)
left_width = int(content_rect.width * 0.5 - 15) left_width = int(content_rect.width * 0.5 - 15)
@ -124,9 +125,9 @@ class PairingDialog(Widget):
def _render_instructions(self, rect: rl.Rectangle) -> None: def _render_instructions(self, rect: rl.Rectangle) -> None:
instructions = [ instructions = [
"Go to https://connect.comma.ai on your phone", tr("Go to https://connect.comma.ai on your phone"),
"Click \"add new device\" and scan the QR code on the right", tr("Click \"add new device\" and scan the QR code on the right"),
"Bookmark connect.comma.ai to your home screen to use it like an app", tr("Bookmark connect.comma.ai to your home screen to use it like an app"),
] ]
font = gui_app.font(FontWeight.BOLD) font = gui_app.font(FontWeight.BOLD)
@ -157,7 +158,7 @@ class PairingDialog(Widget):
rl.draw_rectangle_rounded(rect, 0.1, 20, rl.Color(240, 240, 240, 255)) rl.draw_rectangle_rounded(rect, 0.1, 20, rl.Color(240, 240, 240, 255))
error_font = gui_app.font(FontWeight.BOLD) error_font = gui_app.font(FontWeight.BOLD)
rl.draw_text_ex( rl.draw_text_ex(
error_font, "QR Code Error", rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED error_font, tr("QR Code Error"), rl.Vector2(rect.x + 20, rect.y + rect.height // 2 - 15), 30, 0.0, rl.RED
) )
return return

@ -2,6 +2,7 @@ import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -29,21 +30,21 @@ class PrimeWidget(Widget):
w = rect.width - 160 w = rect.width - 160
# Title # Title
gui_label(rl.Rectangle(x, y, w, 90), "Upgrade Now", 75, font_weight=FontWeight.BOLD) gui_label(rl.Rectangle(x, y, w, 90), tr("Upgrade Now"), 75, font_weight=FontWeight.BOLD)
# Description with wrapping # Description with wrapping
desc_y = y + 140 desc_y = y + 140
font = gui_app.font(FontWeight.LIGHT) font = gui_app.font(FontWeight.LIGHT)
wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w))) wrapped_text = "\n".join(wrap_text(font, tr("Become a comma prime member at connect.comma.ai"), 56, int(w)))
text_size = measure_text_cached(font, wrapped_text, 56) text_size = measure_text_cached(font, wrapped_text, 56)
rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE) rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE)
# Features section # Features section
features_y = desc_y + text_size.y + 50 features_y = desc_y + text_size.y + 50
gui_label(rl.Rectangle(x, features_y, w, 50), "PRIME FEATURES:", 41, font_weight=FontWeight.BOLD) gui_label(rl.Rectangle(x, features_y, w, 50), tr("PRIME FEATURES:"), 41, font_weight=FontWeight.BOLD)
# Feature list # Feature list
features = ["Remote access", "24/7 LTE connectivity", "1 year of drive storage", "Remote snapshots"] features = [tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Remote snapshots")]
for i, feature in enumerate(features): for i, feature in enumerate(features):
item_y = features_y + 80 + i * 65 item_y = features_y + 80 + i * 65
gui_label(rl.Rectangle(x, item_y, 100, 60), "", 50, color=rl.Color(70, 91, 234, 255)) gui_label(rl.Rectangle(x, item_y, 100, 60), "", 50, color=rl.Color(70, 91, 234, 255))
@ -58,5 +59,5 @@ class PrimeWidget(Widget):
y = rect.y + 40 y = rect.y + 40
font = gui_app.font(FontWeight.BOLD) font = gui_app.font(FontWeight.BOLD)
rl.draw_text_ex(font, "✓ SUBSCRIBED", rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255)) rl.draw_text_ex(font, tr("✓ SUBSCRIBED"), rl.Vector2(x, y), 41, 0, rl.Color(134, 255, 78, 255))
rl.draw_text_ex(font, "comma prime", rl.Vector2(x, y + 61), 75, 0, rl.WHITE) rl.draw_text_ex(font, tr("comma prime"), rl.Vector2(x, y + 61), 75, 0, rl.WHITE)

@ -3,6 +3,7 @@ from openpilot.common.time_helpers import system_time_valid
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog from openpilot.system.ui.widgets.confirm_dialog import alert_dialog
@ -15,10 +16,10 @@ class SetupWidget(Widget):
super().__init__() super().__init__()
self._open_settings_callback = None self._open_settings_callback = None
self._pairing_dialog: PairingDialog | None = None self._pairing_dialog: PairingDialog | None = None
self._pair_device_btn = Button("Pair device", self._show_pairing, button_style=ButtonStyle.PRIMARY) self._pair_device_btn = Button(tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY)
self._open_settings_btn = Button("Open", lambda: self._open_settings_callback() if self._open_settings_callback else None, self._open_settings_btn = Button(tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY) button_style=ButtonStyle.PRIMARY)
self._firehose_label = Label("🔥 Firehose Mode 🔥", font_weight=FontWeight.MEDIUM, font_size=64) self._firehose_label = Label(tr("🔥 Firehose Mode 🔥"), font_weight=FontWeight.MEDIUM, font_size=64)
def set_open_settings_callback(self, callback): def set_open_settings_callback(self, callback):
self._open_settings_callback = callback self._open_settings_callback = callback
@ -40,11 +41,11 @@ class SetupWidget(Widget):
# Title # Title
font = gui_app.font(FontWeight.BOLD) font = gui_app.font(FontWeight.BOLD)
rl.draw_text_ex(font, "Finish Setup", rl.Vector2(x, y), 75, 0, rl.WHITE) rl.draw_text_ex(font, tr("Finish Setup"), rl.Vector2(x, y), 75, 0, rl.WHITE)
y += 113 # 75 + 38 spacing y += 113 # 75 + 38 spacing
# Description # Description
desc = "Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer." desc = tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")
light_font = gui_app.font(FontWeight.LIGHT) light_font = gui_app.font(FontWeight.LIGHT)
wrapped = wrap_text(light_font, desc, 50, int(w)) wrapped = wrap_text(light_font, desc, 50, int(w))
for line in wrapped: for line in wrapped:
@ -71,7 +72,7 @@ class SetupWidget(Widget):
# Description # Description
desc_font = gui_app.font(FontWeight.NORMAL) desc_font = gui_app.font(FontWeight.NORMAL)
desc_text = "Maximize your training data uploads to improve openpilot's driving models." desc_text = tr("Maximize your training data uploads to improve openpilot's driving models.")
wrapped_desc = wrap_text(desc_font, desc_text, 40, int(w)) wrapped_desc = wrap_text(desc_font, desc_text, 40, int(w))
for line in wrapped_desc: for line in wrapped_desc:
@ -87,7 +88,7 @@ class SetupWidget(Widget):
def _show_pairing(self): def _show_pairing(self):
if not system_time_valid(): if not system_time_valid():
dlg = alert_dialog("Please connect to Wi-Fi to complete initial pairing") dlg = alert_dialog(tr("Please connect to Wi-Fi to complete initial pairing"))
gui_app.set_modal_overlay(dlg) gui_app.set_modal_overlay(dlg)
return return

@ -7,6 +7,7 @@ from enum import Enum
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
@ -25,9 +26,9 @@ VALUE_FONT_SIZE = 48
class SshKeyActionState(Enum): class SshKeyActionState(Enum):
LOADING = "LOADING" LOADING = tr("LOADING")
ADD = "ADD" ADD = tr("ADD")
REMOVE = "REMOVE" REMOVE = tr("REMOVE")
class SshKeyAction(ItemAction): class SshKeyAction(ItemAction):
@ -85,7 +86,7 @@ class SshKeyAction(ItemAction):
def _handle_button_click(self): def _handle_button_click(self):
if self._state == SshKeyActionState.ADD: if self._state == SshKeyActionState.ADD:
self._keyboard.reset() self._keyboard.reset()
self._keyboard.set_title("Enter your GitHub username") self._keyboard.set_title(tr("Enter your GitHub username"))
gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit) gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit)
elif self._state == SshKeyActionState.REMOVE: elif self._state == SshKeyActionState.REMOVE:
self._params.remove("GithubUsername") self._params.remove("GithubUsername")
@ -110,7 +111,7 @@ class SshKeyAction(ItemAction):
response.raise_for_status() response.raise_for_status()
keys = response.text.strip() keys = response.text.strip()
if not keys: if not keys:
raise requests.exceptions.HTTPError("No SSH keys found") raise requests.exceptions.HTTPError(tr("No SSH keys found"))
# Success - save keys # Success - save keys
self._params.put("GithubUsername", username) self._params.put("GithubUsername", username)
@ -119,10 +120,10 @@ class SshKeyAction(ItemAction):
self._username = username self._username = username
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
self._error_message = "Request timed out" self._error_message = tr("Request timed out")
self._state = SshKeyActionState.ADD self._state = SshKeyActionState.ADD
except Exception: except Exception:
self._error_message = f"No SSH keys found for user '{username}'" self._error_message = tr("No SSH keys found for user '{}'").format(username)
self._state = SshKeyActionState.ADD self._state = SshKeyActionState.ADD

@ -0,0 +1,10 @@
import os
import gettext
from openpilot.common.basedir import BASEDIR
UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui")
TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations")
LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json")
tr = gettext.gettext
trn = gettext.ngettext

@ -1,5 +1,6 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.widgets.label import Label
@ -16,8 +17,10 @@ BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
class ConfirmDialog(Widget): class ConfirmDialog(Widget):
def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel", rich: bool = False): def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False):
super().__init__() super().__init__()
if cancel_text is None:
cancel_text = tr("Cancel")
self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255)) self._label = Label(text, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255))
self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50}, center_text=True) self._html_renderer = HtmlRenderer(text=text, text_size={ElementType.P: 50}, center_text=True)
self._cancel_button = Button(cancel_text, self._cancel_button_callback) self._cancel_button = Button(cancel_text, self._cancel_button_callback)
@ -85,5 +88,7 @@ class ConfirmDialog(Widget):
return self._dialog_result return self._dialog_result
def alert_dialog(message: str, button_text: str = "Ok"): def alert_dialog(message: str, button_text: str | None = None):
if button_text is None:
button_text = tr("OK")
return ConfirmDialog(message, button_text, cancel_text="") return ConfirmDialog(message, button_text, cancel_text="")

@ -4,6 +4,7 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -243,7 +244,7 @@ class HtmlModal(Widget):
super().__init__() super().__init__()
self._content = HtmlRenderer(file_path=file_path, text=text) self._content = HtmlRenderer(file_path=file_path, text=text)
self._scroll_panel = GuiScrollPanel() self._scroll_panel = GuiScrollPanel()
self._ok_button = Button("OK", click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY) self._ok_button = Button(tr("OK"), click_callback=lambda: gui_app.set_modal_overlay(None), button_style=ButtonStyle.PRIMARY)
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
margin = 50 margin = 50

@ -5,6 +5,7 @@ from typing import Literal
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.button import ButtonStyle, Button
from openpilot.system.ui.widgets.inputbox import InputBox from openpilot.system.ui.widgets.inputbox import InputBox
@ -77,7 +78,7 @@ class Keyboard(Widget):
self._backspace_last_repeat: float = 0.0 self._backspace_last_repeat: float = 0.0
self._render_return_status = -1 self._render_return_status = -1
self._cancel_button = Button("Cancel", self._cancel_button_callback) self._cancel_button = Button(tr("Cancel"), self._cancel_button_callback)
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
@ -98,7 +99,7 @@ class Keyboard(Widget):
if key in self._key_icons: if key in self._key_icons:
texture = self._key_icons[key] texture = self._key_icons[key]
self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture,
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True)
else: else:
self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True)
self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY],

@ -3,6 +3,7 @@ import pyray as rl
from collections.abc import Callable from collections.abc import Callable
from abc import ABC from abc import ABC
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
@ -111,7 +112,7 @@ class ButtonAction(ItemAction):
@property @property
def text(self): def text(self):
return _resolve_value(self._text_source, "Error") return _resolve_value(self._text_source, tr("Error"))
@property @property
def value(self): def value(self):
@ -149,7 +150,7 @@ class TextAction(ItemAction):
@property @property
def text(self): def text(self):
return _resolve_value(self._text_source, "Error") return _resolve_value(self._text_source, tr("Error"))
def get_width_hint(self) -> float: def get_width_hint(self) -> float:
text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x text_width = measure_text_cached(self._font, self.text, ITEM_TEXT_FONT_SIZE).x

@ -4,6 +4,7 @@ from typing import cast
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@ -70,7 +71,7 @@ class NetworkUI(Widget):
self._current_panel: PanelType = PanelType.WIFI self._current_panel: PanelType = PanelType.WIFI
self._wifi_panel = WifiManagerUI(wifi_manager) self._wifi_panel = WifiManagerUI(wifi_manager)
self._advanced_panel = AdvancedNetworkSettings(wifi_manager) self._advanced_panel = AdvancedNetworkSettings(wifi_manager)
self._nav_button = NavButton("Advanced") self._nav_button = NavButton(tr("Advanced"))
self._nav_button.set_click_callback(self._cycle_panel) self._nav_button.set_click_callback(self._cycle_panel)
def show_event(self): def show_event(self):
@ -91,11 +92,11 @@ class NetworkUI(Widget):
content_rect = rl.Rectangle(self._rect.x, self._rect.y + self._nav_button.rect.height + 40, content_rect = rl.Rectangle(self._rect.x, self._rect.y + self._nav_button.rect.height + 40,
self._rect.width, self._rect.height - self._nav_button.rect.height - 40) self._rect.width, self._rect.height - self._nav_button.rect.height - 40)
if self._current_panel == PanelType.WIFI: if self._current_panel == PanelType.WIFI:
self._nav_button.text = "Advanced" self._nav_button.text = tr("Advanced")
self._nav_button.set_position(self._rect.x + self._rect.width - self._nav_button.rect.width, self._rect.y + 20) self._nav_button.set_position(self._rect.x + self._rect.width - self._nav_button.rect.width, self._rect.y + 20)
self._wifi_panel.render(content_rect) self._wifi_panel.render(content_rect)
else: else:
self._nav_button.text = "Back" self._nav_button.text = tr("Back")
self._nav_button.set_position(self._rect.x, self._rect.y + 20) self._nav_button.set_position(self._rect.x, self._rect.y + 20)
self._advanced_panel.render(content_rect) self._advanced_panel.render(content_rect)
@ -116,40 +117,40 @@ class AdvancedNetworkSettings(Widget):
# Tethering # Tethering
self._tethering_action = ToggleAction(initial_state=False) self._tethering_action = ToggleAction(initial_state=False)
tethering_btn = ListItem(title="Enable Tethering", action_item=self._tethering_action, callback=self._toggle_tethering) tethering_btn = ListItem(title=tr("Enable Tethering"), action_item=self._tethering_action, callback=self._toggle_tethering)
# Edit tethering password # Edit tethering password
self._tethering_password_action = ButtonAction(text="EDIT") self._tethering_password_action = ButtonAction(text=tr("EDIT"))
tethering_password_btn = ListItem(title="Tethering Password", action_item=self._tethering_password_action, callback=self._edit_tethering_password) tethering_password_btn = ListItem(title=tr("Tethering Password"), action_item=self._tethering_password_action, callback=self._edit_tethering_password)
# Roaming toggle # Roaming toggle
roaming_enabled = self._params.get_bool("GsmRoaming") roaming_enabled = self._params.get_bool("GsmRoaming")
self._roaming_action = ToggleAction(initial_state=roaming_enabled) self._roaming_action = ToggleAction(initial_state=roaming_enabled)
self._roaming_btn = ListItem(title="Enable Roaming", action_item=self._roaming_action, callback=self._toggle_roaming) self._roaming_btn = ListItem(title=tr("Enable Roaming"), action_item=self._roaming_action, callback=self._toggle_roaming)
# Cellular metered toggle # Cellular metered toggle
cellular_metered = self._params.get_bool("GsmMetered") cellular_metered = self._params.get_bool("GsmMetered")
self._cellular_metered_action = ToggleAction(initial_state=cellular_metered) self._cellular_metered_action = ToggleAction(initial_state=cellular_metered)
self._cellular_metered_btn = ListItem(title="Cellular Metered", description="Prevent large data uploads when on a metered cellular connection", self._cellular_metered_btn = ListItem(title=tr("Cellular Metered"), description=tr("Prevent large data uploads when on a metered cellular connection"),
action_item=self._cellular_metered_action, callback=self._toggle_cellular_metered) action_item=self._cellular_metered_action, callback=self._toggle_cellular_metered)
# APN setting # APN setting
self._apn_btn = button_item("APN Setting", "EDIT", callback=self._edit_apn) self._apn_btn = button_item(tr("APN Setting"), tr("EDIT"), callback=self._edit_apn)
# Wi-Fi metered toggle # Wi-Fi metered toggle
self._wifi_metered_action = MultipleButtonAction(["default", "metered", "unmetered"], 255, 0, callback=self._toggle_wifi_metered) self._wifi_metered_action = MultipleButtonAction([tr("default"), tr("metered"), tr("unmetered")], 255, 0, callback=self._toggle_wifi_metered)
wifi_metered_btn = ListItem(title="Wi-Fi Network Metered", description="Prevent large data uploads when on a metered Wi-Fi connection", wifi_metered_btn = ListItem(title=tr("Wi-Fi Network Metered"), description=tr("Prevent large data uploads when on a metered Wi-Fi connection"),
action_item=self._wifi_metered_action) action_item=self._wifi_metered_action)
items: list[Widget] = [ items: list[Widget] = [
tethering_btn, tethering_btn,
tethering_password_btn, tethering_password_btn,
text_item("IP Address", lambda: self._wifi_manager.ipv4_address), text_item(tr("IP Address"), lambda: self._wifi_manager.ipv4_address),
self._roaming_btn, self._roaming_btn,
self._apn_btn, self._apn_btn,
self._cellular_metered_btn, self._cellular_metered_btn,
wifi_metered_btn, wifi_metered_btn,
button_item("Hidden Network", "CONNECT", callback=self._connect_to_hidden_network), button_item(tr("Hidden Network"), tr("CONNECT"), callback=self._connect_to_hidden_network),
] ]
self._scroller = Scroller(items, line_separator=True, spacing=0) self._scroller = Scroller(items, line_separator=True, spacing=0)
@ -198,7 +199,7 @@ class AdvancedNetworkSettings(Widget):
current_apn = self._params.get("GsmApn") or "" current_apn = self._params.get("GsmApn") or ""
self._keyboard.reset(min_text_size=0) self._keyboard.reset(min_text_size=0)
self._keyboard.set_title("Enter APN", "leave blank for automatic configuration") self._keyboard.set_title(tr("Enter APN"), tr("leave blank for automatic configuration"))
self._keyboard.set_text(current_apn) self._keyboard.set_text(current_apn)
gui_app.set_modal_overlay(self._keyboard, update_apn) gui_app.set_modal_overlay(self._keyboard, update_apn)
@ -231,11 +232,11 @@ class AdvancedNetworkSettings(Widget):
self._wifi_manager.connect_to_network(ssid, password, hidden=True) self._wifi_manager.connect_to_network(ssid, password, hidden=True)
self._keyboard.reset(min_text_size=0) self._keyboard.reset(min_text_size=0)
self._keyboard.set_title("Enter password", f"for \"{ssid}\"") self._keyboard.set_title(tr("Enter password"), tr("for \"{}\"").format(ssid))
gui_app.set_modal_overlay(self._keyboard, enter_password) gui_app.set_modal_overlay(self._keyboard, enter_password)
self._keyboard.reset(min_text_size=1) self._keyboard.reset(min_text_size=1)
self._keyboard.set_title("Enter SSID", "") self._keyboard.set_title(tr("Enter SSID"), "")
gui_app.set_modal_overlay(self._keyboard, connect_hidden) gui_app.set_modal_overlay(self._keyboard, connect_hidden)
def _edit_tethering_password(self): def _edit_tethering_password(self):
@ -248,7 +249,7 @@ class AdvancedNetworkSettings(Widget):
self._tethering_password_action.set_enabled(False) self._tethering_password_action.set_enabled(False)
self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) self._keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
self._keyboard.set_title("Enter new tethering password", "") self._keyboard.set_title(tr("Enter new tethering password"), "")
self._keyboard.set_text(self._wifi_manager.tethering_password) self._keyboard.set_text(self._wifi_manager.tethering_password)
gui_app.set_modal_overlay(self._keyboard, update_password) gui_app.set_modal_overlay(self._keyboard, update_password)
@ -281,7 +282,7 @@ class WifiManagerUI(Widget):
self._networks: list[Network] = [] self._networks: list[Network] = []
self._networks_buttons: dict[str, Button] = {} self._networks_buttons: dict[str, Button] = {}
self._forget_networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {}
self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self._confirm_dialog = ConfirmDialog("", tr("Forget"), tr("Cancel"))
self._wifi_manager.set_callbacks(need_auth=self._on_need_auth, self._wifi_manager.set_callbacks(need_auth=self._on_need_auth,
activated=self._on_activated, activated=self._on_activated,
@ -305,15 +306,15 @@ class WifiManagerUI(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if not self._networks: if not self._networks:
gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
return return
if self.state == UIState.NEEDS_AUTH and self._state_network: if self.state == UIState.NEEDS_AUTH and self._state_network:
self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") self.keyboard.set_title(tr("Wrong password") if self._password_retry else tr("Enter password"), tr("for \"{}\"").format(self._state_network.ssid))
self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH) self.keyboard.reset(min_text_size=MIN_PASSWORD_LENGTH)
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result))
elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') self._confirm_dialog.set_text(tr("Forget Wi-Fi Network \"{}\"?").format(self._state_network.ssid))
self._confirm_dialog.reset() self._confirm_dialog.reset()
gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
else: else:
@ -363,11 +364,11 @@ class WifiManagerUI(Widget):
if self.state == UIState.CONNECTING and self._state_network: if self.state == UIState.CONNECTING and self._state_network:
if self._state_network.ssid == network.ssid: if self._state_network.ssid == network.ssid:
self._networks_buttons[network.ssid].set_enabled(False) self._networks_buttons[network.ssid].set_enabled(False)
status_text = "CONNECTING..." status_text = tr("CONNECTING...")
elif self.state == UIState.FORGETTING and self._state_network: elif self.state == UIState.FORGETTING and self._state_network:
if self._state_network.ssid == network.ssid: if self._state_network.ssid == network.ssid:
self._networks_buttons[network.ssid].set_enabled(False) self._networks_buttons[network.ssid].set_enabled(False)
status_text = "FORGETTING..." status_text = tr("FORGETTING...")
elif network.security_type == SecurityType.UNSUPPORTED: elif network.security_type == SecurityType.UNSUPPORTED:
self._networks_buttons[network.ssid].set_enabled(False) self._networks_buttons[network.ssid].set_enabled(False)
else: else:
@ -445,7 +446,7 @@ class WifiManagerUI(Widget):
self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55,
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
font_size=45) font_size=45)
self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())

@ -1,5 +1,6 @@
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
@ -30,8 +31,8 @@ class MultiOptionDialog(Widget):
text_padding=50) for option in options] text_padding=50) for option in options]
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING) self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
self.cancel_button = Button("Cancel", click_callback=lambda: self._set_result(DialogResult.CANCEL)) self.cancel_button = Button(tr("Cancel"), click_callback=lambda: self._set_result(DialogResult.CANCEL))
self.select_button = Button("Select", click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY) self.select_button = Button(tr("Select"), click_callback=lambda: self._set_result(DialogResult.CONFIRM), button_style=ButtonStyle.PRIMARY)
def _set_result(self, result: DialogResult): def _set_result(self, result: DialogResult):
self._result = result self._result = result

Loading…
Cancel
Save