From 2e58281d827ab02e570f1cd55f5b9617bdb9b880 Mon Sep 17 00:00:00 2001 From: SakuraRC_Yang <35626190+BloodSakura3774@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:06:25 +0800 Subject: [PATCH] FEATURE: i18n markup added Both logging and output/display strings are marked, please double check before merging --- .../ardupilot_methodic_configurator.py | 16 +- MethodicConfigurator/argparse_check_range.py | 6 +- MethodicConfigurator/backend_filesystem.py | 50 +-- .../backend_filesystem_configuration_steps.py | 39 +-- .../backend_filesystem_program_settings.py | 15 +- .../backend_filesystem_vehicle_components.py | 10 +- .../backend_flightcontroller.py | 71 ++--- MethodicConfigurator/common_arguments.py | 6 +- MethodicConfigurator/frontend_tkinter_base.py | 22 +- .../frontend_tkinter_component_editor.py | 60 ++-- .../frontend_tkinter_component_editor_base.py | 30 +- .../frontend_tkinter_connection_selection.py | 80 ++--- .../frontend_tkinter_directory_selection.py | 125 ++++---- .../frontend_tkinter_entry_dynamic.py | 4 +- .../frontend_tkinter_flightcontroller_info.py | 20 +- .../frontend_tkinter_parameter_editor.py | 289 +++++++++--------- ...frontend_tkinter_parameter_editor_table.py | 107 +++---- .../frontend_tkinter_template_overview.py | 10 +- MethodicConfigurator/internationalization.py | 22 ++ 19 files changed, 521 insertions(+), 461 deletions(-) create mode 100644 MethodicConfigurator/internationalization.py diff --git a/MethodicConfigurator/ardupilot_methodic_configurator.py b/MethodicConfigurator/ardupilot_methodic_configurator.py index 2206b9a3..4c29fb65 100755 --- a/MethodicConfigurator/ardupilot_methodic_configurator.py +++ b/MethodicConfigurator/ardupilot_methodic_configurator.py @@ -34,6 +34,8 @@ from MethodicConfigurator.common_arguments import add_common_arguments_and_parse +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.version import VERSION @@ -46,7 +48,7 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = argparse.ArgumentParser(description='ArduPilot methodic configurator is a simple GUI with a table that lists ' + parser = argparse.ArgumentParser(description=_('ArduPilot methodic configurator is a simple GUI with a table that lists ' 'parameters. The GUI reads intermediate parameter files from a directory and ' 'displays their parameters in a table. Each row displays the parameter name, ' 'its current value on the flight controller, its new value from the selected ' @@ -55,7 +57,7 @@ def argument_parser(): 'When "Upload Selected to FC" is clicked, it uploads the selected parameters to the ' 'flight controller. ' 'When "Skip" is pressed, it skips to the next intermediate parameter file. ' - 'The process gets repeated for each intermediate parameter file.') + 'The process gets repeated for each intermediate parameter file.')) parser = FlightController.add_argparse_arguments(parser) parser = LocalFilesystem.add_argparse_arguments(parser) parser = ComponentEditorWindow.add_argparse_arguments(parser) @@ -68,7 +70,7 @@ def connect_to_fc_and_read_parameters(args): error_str = flight_controller.connect(args.device, log_errors=False) if error_str: - if args.device and "No serial ports found" not in error_str: + if args.device and _("No serial ports found") not in error_str: logging_error(error_str) conn_sel_window = ConnectionSelectionWindow(flight_controller, error_str) conn_sel_window.root.mainloop() @@ -77,9 +79,9 @@ def connect_to_fc_and_read_parameters(args): if vehicle_type == "": # not explicitly set, to try to guess it if flight_controller.info.vehicle_type is not None: vehicle_type = flight_controller.info.vehicle_type - logging_debug("Vehicle type not set explicitly, auto-detected %s.", vehicle_type) + logging_debug(_("Vehicle type not set explicitly, auto-detected %s."), vehicle_type) else: - logging_info("Vehicle type explicitly set to %s.", vehicle_type) + logging_info(_("Vehicle type explicitly set to %s."), vehicle_type) return flight_controller,vehicle_type @@ -110,7 +112,7 @@ def component_editor(args, flight_controller, vehicle_type, local_filesystem, ve flight_controller.fc_parameters) if error_message: logging_error(error_message) - show_error_message("Error in derived parameters", error_message) + show_error_message(_("Error in derived parameters"), error_message) sys_exit(1) @@ -131,7 +133,7 @@ def main(): local_filesystem = LocalFilesystem(args.vehicle_dir, vehicle_type, flight_controller.info.flight_sw_version, args.allow_editing_template_files) except SystemExit as exp: - show_error_message("Fatal error reading parameter files", f"{exp}") + show_error_message(_("Fatal error reading parameter files"), f"{exp}") raise param_default_values_dirty = False diff --git a/MethodicConfigurator/argparse_check_range.py b/MethodicConfigurator/argparse_check_range.py index 849e15d9..4ae2ba90 100644 --- a/MethodicConfigurator/argparse_check_range.py +++ b/MethodicConfigurator/argparse_check_range.py @@ -16,7 +16,7 @@ from operator import ge from operator import lt from operator import le - +from MethodicConfigurator.internationalization import _ class CheckRange(Action): ''' @@ -29,9 +29,9 @@ class CheckRange(Action): def __init__(self, *args, **kwargs): if "min" in kwargs and "inf" in kwargs: - raise ValueError("either min or inf, but not both") + raise ValueError(_("either min or inf, but not both")) if "max" in kwargs and "sup" in kwargs: - raise ValueError("either max or sup, but not both") + raise ValueError(_("either max or sup, but not both")) for name in self.ops: if name in kwargs: diff --git a/MethodicConfigurator/backend_filesystem.py b/MethodicConfigurator/backend_filesystem.py index f6c167e0..7352c049 100644 --- a/MethodicConfigurator/backend_filesystem.py +++ b/MethodicConfigurator/backend_filesystem.py @@ -47,6 +47,8 @@ from MethodicConfigurator.backend_filesystem_configuration_steps import ConfigurationSteps from MethodicConfigurator.backend_filesystem_program_settings import ProgramSettings +from MethodicConfigurator.internationalization import _ + TOOLTIP_MAX_LENGTH = 105 @@ -111,7 +113,7 @@ def re_init(self, vehicle_dir: str, vehicle_type: str): vehicle_type = self.get_fc_fw_type_from_vehicle_components_json() if vehicle_type == "": vehicle_type = "ArduCopter" - logging_warning("Could not detect vehicle type. Defaulting to %s.", vehicle_type) + logging_warning(_("Could not detect vehicle type. Defaulting to %s."), vehicle_type) self.vehicle_type = vehicle_type ConfigurationSteps.re_init(self, vehicle_dir, vehicle_type) @@ -232,7 +234,7 @@ def read_params_from_files(self): continue parameters[filename] = Par.load_param_file_into_dict(os_path.join(self.vehicle_dir, filename)) else: - logging_error("Error: %s is not a directory.", self.vehicle_dir) + logging_error(_("Error: %s is not a directory."), self.vehicle_dir) return parameters @staticmethod @@ -419,7 +421,7 @@ def zip_files(self, files_to_zip: List[Tuple[bool, str]]): if wrote: self.add_configuration_file_to_zip(zipf, filename) - logging_info("Intermediate parameter files and summary files zipped to %s", zip_file_path) + logging_info(_("Intermediate parameter files and summary files zipped to %s"), zip_file_path) def vehicle_image_filepath(self): return os_path.join(self.vehicle_dir, 'vehicle.jpg') @@ -464,7 +466,7 @@ def copy_fc_values_to_file(self, selected_file: str, params: Dict[str, float]): v.value = params[param] ret += 1 else: - logging_warning("Parameter %s not found in the current parameter file", param) + logging_warning(_("Parameter %s not found in the current parameter file"), param) return ret def write_last_uploaded_filename(self, current_file: str): @@ -472,16 +474,16 @@ def write_last_uploaded_filename(self, current_file: str): with open(os_path.join(self.vehicle_dir, 'last_uploaded_filename.txt'), 'w', encoding='utf-8') as file: file.write(current_file) except Exception as e: # pylint: disable=broad-except - logging_error("Error writing last uploaded filename: %s", e) + logging_error(_("Error writing last uploaded filename: %s"), e) def __read_last_uploaded_filename(self) -> str: try: with open(os_path.join(self.vehicle_dir, 'last_uploaded_filename.txt'), 'r', encoding='utf-8') as file: return file.read().strip() except FileNotFoundError as e: - logging_debug("last_uploaded_filename.txt not found: %s", e) + logging_debug(_("last_uploaded_filename.txt not found: %s"), e) except Exception as e: # pylint: disable=broad-except - logging_error("Error reading last uploaded filename: %s", e) + logging_error(_("Error reading last uploaded filename: %s"), e) return "" def get_start_file(self, explicit_index: int, tcal_available: bool) -> str: @@ -496,27 +498,27 @@ def get_start_file(self, explicit_index: int, tcal_available: bool) -> str: start_file_index = explicit_index # Ensure the index is within the range of available files if start_file_index >= len(files): start_file_index = len(files) - 1 - logging_warning("Starting file index %s is out of range. Starting with file %s instead.", + logging_warning(_("Starting file index %s is out of range. Starting with file %s instead."), explicit_index, files[start_file_index]) return files[start_file_index] if tcal_available: start_file = files[0] - info_msg = "Starting with the first file." + info_msg = _("Starting with the first file.") else: start_file = files[2] - info_msg = "Starting with the first non-tcal file." + info_msg = _("Starting with the first non-tcal file.") last_uploaded_filename = self.__read_last_uploaded_filename() if last_uploaded_filename: - logging_info("Last uploaded file was %s.", last_uploaded_filename) + logging_info(_("Last uploaded file was %s."), last_uploaded_filename) else: - logging_info("No last uploaded file found. %s.", info_msg) + logging_info(_("No last uploaded file found. %s."), info_msg) return start_file if last_uploaded_filename not in files: # Handle the case where last_uploaded_filename is not found in the list - logging_warning("Last uploaded file not found in the list of files. %s.", info_msg) + logging_warning(_("Last uploaded file not found in the list of files. %s."), info_msg) return start_file # Find the index of last_uploaded_filename in files @@ -525,7 +527,7 @@ def get_start_file(self, explicit_index: int, tcal_available: bool) -> str: start_file_index = last_uploaded_index + 1 if start_file_index >= len(files): # Handle the case where last_uploaded_filename is the last file in the list - logging_warning("Last uploaded file is the last file in the list. Starting from there.") + logging_warning(_("Last uploaded file is the last file in the list. Starting from there.")) start_file_index = len(files) - 1 return files[start_file_index] @@ -589,9 +591,9 @@ def get_upload_local_and_remote_filenames(self, selected_file: str) -> Tuple[str @staticmethod def download_file_from_url(url: str, local_filename: str, timeout: int=5) -> bool: if not url or not local_filename: - logging_error("URL or local filename not provided.") + logging_error(_("URL or local filename not provided.")) return False - logging_info("Downloading %s from %s", local_filename, url) + logging_info(_("Downloading %s from %s"), local_filename, url) response = requests_get(url, timeout=timeout) if response.status_code == 200: @@ -599,7 +601,7 @@ def download_file_from_url(url: str, local_filename: str, timeout: int=5) -> boo file.write(response.content) return True - logging_error("Failed to download the file") + logging_error(_("Failed to download the file")) return False @staticmethod @@ -607,20 +609,20 @@ def add_argparse_arguments(parser): parser.add_argument('-t', '--vehicle-type', choices=VehicleComponents.supported_vehicles(), default='', - help='The type of the vehicle. Defaults to ArduCopter') + help=_('The type of the vehicle. Defaults to ArduCopter')) parser.add_argument('--vehicle-dir', type=str, default=os_getcwd(), - help='Directory containing vehicle-specific intermediate parameter files. ' - 'Defaults to the current working directory') + help=_('Directory containing vehicle-specific intermediate parameter files. ' + 'Defaults to the current working directory')) parser.add_argument('--n', type=int, default=-1, - help='Start directly on the nth intermediate parameter file (skips previous files). ' + help=_('Start directly on the nth intermediate parameter file (skips previous files). ' 'Default is to start on the file next to the last that you wrote to the flight controller.' - 'If the file does not exist, it will start on the first file.') + 'If the file does not exist, it will start on the first file.')) parser.add_argument('--allow-editing-template-files', action='store_true', - help='Allow opening and editing template files directly. ' - 'Only for software developers that know what they are doing.') + help=_('Allow opening and editing template files directly. ' + 'Only for software developers that know what they are doing.')) return parser diff --git a/MethodicConfigurator/backend_filesystem_configuration_steps.py b/MethodicConfigurator/backend_filesystem_configuration_steps.py index b3d7525a..2f2ecf5d 100644 --- a/MethodicConfigurator/backend_filesystem_configuration_steps.py +++ b/MethodicConfigurator/backend_filesystem_configuration_steps.py @@ -18,11 +18,12 @@ from json import load as json_load from json import JSONDecodeError - from typing import Tuple from MethodicConfigurator.annotate_params import Par +from MethodicConfigurator.internationalization import _ + class ConfigurationSteps: """ @@ -56,24 +57,24 @@ def re_init(self, vehicle_dir: str, vehicle_type: str): file_found = True if self.log_loaded_file: if i == 0: - logging_warning("Configuration steps '%s' loaded from %s " \ - "(overwriting default configuration steps).", + logging_warning(_("Configuration steps '%s' loaded from %s " \ + "(overwriting default configuration steps)."), self.configuration_steps_filename, directory) if i == 1: - logging_info("Configuration steps '%s' loaded from %s.", + logging_info(_("Configuration steps '%s' loaded from %s."), self.configuration_steps_filename, directory) break except FileNotFoundError: pass except JSONDecodeError as e: - logging_error("Error in file '%s': %s", self.configuration_steps_filename, e) + logging_error(_("Error in file '%s': %s"), self.configuration_steps_filename, e) break if file_found: for filename, file_info in self.configuration_steps.items(): self.__validate_parameters_in_configuration_steps(filename, file_info, 'forced') self.__validate_parameters_in_configuration_steps(filename, file_info, 'derived') else: - logging_warning("No configuration steps documentation and no forced and derived parameters will be available.") + logging_warning(_("No configuration steps documentation and no forced and derived parameters will be available.")) self.log_loaded_file = True def __validate_parameters_in_configuration_steps(self, filename: str, file_info: dict, parameter_type: str) -> None: @@ -85,17 +86,17 @@ def __validate_parameters_in_configuration_steps(self, filename: str, file_info: """ if parameter_type + '_parameters' in file_info: if not isinstance(file_info[parameter_type + '_parameters'], dict): - logging_error("Error in file '%s': '%s' %s parameter is not a dictionary", + logging_error(_("Error in file '%s': '%s' %s parameter is not a dictionary"), self.configuration_steps_filename, filename, parameter_type) return for parameter, parameter_info in file_info[parameter_type + '_parameters'].items(): if "New Value" not in parameter_info: - logging_error("Error in file '%s': '%s' %s parameter '%s'" - " 'New Value' attribute not found.", + logging_error(_("Error in file '%s': '%s' %s parameter '%s'" + " 'New Value' attribute not found."), self.configuration_steps_filename, filename, parameter_type, parameter) if "Change Reason" not in parameter_info: - logging_error("Error in file '%s': '%s' %s parameter '%s'" - " 'Change Reason' attribute not found.", + logging_error(_("Error in file '%s': '%s' %s parameter '%s'" + " 'Change Reason' attribute not found."), self.configuration_steps_filename, filename, parameter_type, parameter) def compute_parameters(self, filename: str, file_info: dict, parameter_type: str, variables: dict) -> str: @@ -112,8 +113,8 @@ def compute_parameters(self, filename: str, file_info: dict, parameter_type: str try: if ('fc_parameters' in str(parameter_info["New Value"])) and \ ('fc_parameters' not in variables or variables['fc_parameters'] == {}): - error_msg = f"In file '{self.configuration_steps_filename}': '{filename}' {parameter_type} " \ - f"parameter '{parameter}' could not be computed: 'fc_parameters' not found, is an FC connected?" + error_msg = _(f"In file '{self.configuration_steps_filename}': '{filename}' {parameter_type} " \ + f"parameter '{parameter}' could not be computed: 'fc_parameters' not found, is an FC connected?") if parameter_type == 'forced': logging_error(error_msg) return error_msg @@ -136,8 +137,8 @@ def compute_parameters(self, filename: str, file_info: dict, parameter_type: str destination[filename] = {} destination[filename][parameter] = Par(float(result), parameter_info["Change Reason"]) except (SyntaxError, NameError, KeyError, StopIteration) as e: - error_msg = f"In file '{self.configuration_steps_filename}': '{filename}' {parameter_type} " \ - f"parameter '{parameter}' could not be computed: {e}" + error_msg = _(f"In file '{self.configuration_steps_filename}': '{filename}' {parameter_type} " \ + f"parameter '{parameter}' could not be computed: {e}") if parameter_type == 'forced': logging_error(error_msg) return error_msg @@ -158,11 +159,11 @@ def get_documentation_text_and_url(self, selected_file: str, prefix_key: str) -> documentation = self.configuration_steps.get(selected_file, {}) if \ self.configuration_steps else None if documentation is None: - text = f"File '{self.configuration_steps_filename}' not found. " \ - "No intermediate parameter configuration steps available" + text = _(f"File '{self.configuration_steps_filename}' not found. " \ + "No intermediate parameter configuration steps available") url = None else: - text = documentation.get(prefix_key + "_text", f"No documentation available for {selected_file} in the " - f"{self.configuration_steps_filename} file") + text = documentation.get(prefix_key + "_text", _(f"No documentation available for {selected_file} in the " + f"{self.configuration_steps_filename} file")) url = documentation.get(prefix_key + "_url", None) return text, url diff --git a/MethodicConfigurator/backend_filesystem_program_settings.py b/MethodicConfigurator/backend_filesystem_program_settings.py index f1fb9a90..c169aa0c 100644 --- a/MethodicConfigurator/backend_filesystem_program_settings.py +++ b/MethodicConfigurator/backend_filesystem_program_settings.py @@ -27,6 +27,8 @@ from platformdirs import site_config_dir from platformdirs import user_config_dir +from MethodicConfigurator.internationalization import _ + class ProgramSettings: """ @@ -53,13 +55,12 @@ def application_logo_filepath(): def create_new_vehicle_dir(new_vehicle_dir: str): # Check if the new vehicle directory already exists if os_path.exists(new_vehicle_dir): - return "Directory already exists, choose a different one" - + return _("Directory already exists, choose a different one") try: # Create the new vehicle directory os_makedirs(new_vehicle_dir, exist_ok=True) except OSError as e: - logging_error("Error creating new vehicle directory: %s", e) + logging_error(_("Error creating new vehicle directory: %s"), e) return str(e) return "" @@ -88,9 +89,9 @@ def __user_config_dir(): user_config_directory = user_config_dir(".ardupilot_methodic_configurator", False, roaming=True, ensure_exists=True) if not os_path.exists(user_config_directory): - raise FileNotFoundError(f"The user configuration directory '{user_config_directory}' does not exist.") + raise FileNotFoundError(_(f"The user configuration directory '{user_config_directory}' does not exist.")) if not os_path.isdir(user_config_directory): - raise NotADirectoryError(f"The path '{user_config_directory}' is not a directory.") + raise NotADirectoryError(_(f"The path '{user_config_directory}' is not a directory.")) return user_config_directory @@ -100,9 +101,9 @@ def __site_config_dir(): ensure_exists=True) if not os_path.exists(site_config_directory): - raise FileNotFoundError(f"The site configuration directory '{site_config_directory}' does not exist.") + raise FileNotFoundError(_(f"The site configuration directory '{site_config_directory}' does not exist.")) if not os_path.isdir(site_config_directory): - raise NotADirectoryError(f"The path '{site_config_directory}' is not a directory.") + raise NotADirectoryError(_(f"The path '{site_config_directory}' is not a directory.")) return site_config_directory diff --git a/MethodicConfigurator/backend_filesystem_vehicle_components.py b/MethodicConfigurator/backend_filesystem_vehicle_components.py index 72740924..ed098c8a 100644 --- a/MethodicConfigurator/backend_filesystem_vehicle_components.py +++ b/MethodicConfigurator/backend_filesystem_vehicle_components.py @@ -27,6 +27,8 @@ from MethodicConfigurator.middleware_template_overview import TemplateOverview +from MethodicConfigurator.internationalization import _ + class VehicleComponents: """ @@ -45,9 +47,9 @@ def load_vehicle_components_json_data(self, vehicle_dir: str): data = json_load(file) except FileNotFoundError: # Normal users do not need this information - logging_debug("File '%s' not found in %s.", self.vehicle_components_json_filename, vehicle_dir) + logging_debug(_("File '%s' not found in %s."), self.vehicle_components_json_filename, vehicle_dir) except JSONDecodeError: - logging_error("Error decoding JSON data from file '%s'.", filepath) + logging_error(_("Error decoding JSON data from file '%s'."), filepath) self.vehicle_components = data return data @@ -57,7 +59,7 @@ def save_vehicle_components_json_data(self, data, vehicle_dir: str) -> bool: with open(filepath, 'w', encoding='utf-8') as file: json_dump(data, file, indent=4) except Exception as e: # pylint: disable=broad-except - logging_error("Error saving JSON data to file '%s': %s", filepath, e) + logging_error(_("Error saving JSON data to file '%s': %s"), filepath, e) return True return False @@ -83,7 +85,7 @@ def get_fc_fw_version_from_vehicle_components_json(self) -> str: version_str = version_str.lstrip().split(' ')[0] if version_str else '' if re_match(r'^\d+\.\d+\.\d+$', version_str): return version_str - logging_error(f"FW version string {version_str} on {self.vehicle_components_json_filename} is invalid") + logging_error(_(f"FW version string {version_str} on {self.vehicle_components_json_filename} is invalid")) return None @staticmethod diff --git a/MethodicConfigurator/backend_flightcontroller.py b/MethodicConfigurator/backend_flightcontroller.py index 6bc96568..71770c55 100644 --- a/MethodicConfigurator/backend_flightcontroller.py +++ b/MethodicConfigurator/backend_flightcontroller.py @@ -34,6 +34,8 @@ from MethodicConfigurator.argparse_check_range import CheckRange +from MethodicConfigurator.internationalization import _ + # adding all this allows pyinstaller to build a working windows executable # note that using --hidden-import does not work for these modules @@ -85,7 +87,7 @@ def __init__(self, reboot_time: int): """ # warn people about ModemManager which interferes badly with ArduPilot if os_path.exists("/usr/sbin/ModemManager"): - logging_warning("You should uninstall ModemManager as it conflicts with ArduPilot") + logging_warning(_("You should uninstall ModemManager as it conflicts with ArduPilot")) self.__reboot_time = reboot_time self.__connection_tuples = [] @@ -101,11 +103,11 @@ def discover_connections(self): # list of tuples with the first element being the port name and the second element being the port description self.__connection_tuples = [(port.device, port.description) for port in comports] + \ [(port, port) for port in netports] - logging_info('Available connection ports are:') + logging_info(_('Available connection ports are:')) for port in self.__connection_tuples: logging_info("%s - %s", port[0], port[1]) # now that it is logged, add the 'Add another' tuple - self.__connection_tuples += [tuple(['Add another', 'Add another'])] + self.__connection_tuples += [tuple([_('Add another'), _('Add another')])] def disconnect(self): """ @@ -125,11 +127,11 @@ def add_connection(self, connection_string: str): # Check if connection_string is not the first element of any tuple in self.other_connection_tuples if all(connection_string != t[0] for t in self.__connection_tuples): self.__connection_tuples.insert(-1, (connection_string, connection_string)) - logging_debug("Added connection %s", connection_string) + logging_debug(_("Added connection %s"), connection_string) return True - logging_debug("Did not add duplicated connection %s", connection_string) + logging_debug(_("Did not add duplicated connection %s"), connection_string) else: - logging_debug("Did not add empty connection") + logging_debug(_("Did not add empty connection")) return False def connect(self, device: str, progress_callback=None, log_errors: bool=True) -> str: @@ -163,13 +165,13 @@ def connect(self, device: str, progress_callback=None, log_errors: bool=True) -> if os_name == 'posix': try: dev = autodetect_serial[0].device - logging_debug("Auto-detected device %s", dev) + logging_debug(_("Auto-detected device %s"), dev) # Get the directory part of the soft link softlink_dir = os_path.dirname(dev) # Resolve the soft link and join it with the directory part resolved_path = os_path.abspath(os_path.join(softlink_dir, os_readlink(dev))) autodetect_serial[0].device = resolved_path - logging_debug("Resolved soft link %s to %s", dev, resolved_path) + logging_debug(_("Resolved soft link %s to %s"), dev, resolved_path) except OSError: pass # Not a soft link, proceed with the original device path self.comport = autodetect_serial[0] @@ -177,7 +179,7 @@ def connect(self, device: str, progress_callback=None, log_errors: bool=True) -> if self.comport.device not in [t[0] for t in self.__connection_tuples]: self.__connection_tuples.insert(-1, (self.comport.device, self.comport.description)) else: - return "No serial ports found. Please connect a flight controller and try again." + return _("No serial ports found. Please connect a flight controller and try again.") return self.__create_connection_with_retry(progress_callback=progress_callback, log_errors=log_errors) def __request_banner(self): @@ -236,28 +238,28 @@ def __create_connection_with_retry(self, progress_callback, retries: int = 3, """ if self.comport is None or self.comport.device == 'test': # FIXME for testing only pylint: disable=fixme return "" - logging_info("Will connect to %s", self.comport.device) + logging_info(_("Will connect to %s"), self.comport.device) try: # Create the connection self.master = mavutil.mavlink_connection(device=self.comport.device, timeout=timeout, retries=retries, progress_callback=progress_callback) - logging_debug("Waiting for MAVLink heartbeat") + logging_debug(_("Waiting for MAVLink heartbeat")) m = self.master.wait_heartbeat(timeout=timeout) if m is None: - return "No MAVLink heartbeat received, connection failed." + return _("No MAVLink heartbeat received, connection failed.") self.info.set_system_id_and_component_id(m.get_srcSystem(), m.get_srcComponent()) - logging_debug("Connection established with systemID %d, componentID %d.", + logging_debug(_("Connection established with systemID %d, componentID %d."), self.info.system_id, self.info.component_id) self.info.set_autopilot(m.autopilot) if self.info.is_supported: - logging_info(f"Autopilot type {self.info.autopilot}") + logging_info(_(f"Autopilot type {self.info.autopilot}")) else: - return f"Unsupported autopilot type {self.info.autopilot}" + return _(f"Unsupported autopilot type {self.info.autopilot}") self.info.set_type(m.type) - logging_info(f"Vehicle type: {self.info.mav_type} running {self.info.vehicle_type} firmware") + logging_info(_(f"Vehicle type: {self.info.mav_type} running {self.info.vehicle_type} firmware")) self.__request_banner() banner_msgs = self.__receive_banner_text() @@ -267,15 +269,15 @@ def __create_connection_with_retry(self, progress_callback, retries: int = 3, return self.__process_autopilot_version(m, banner_msgs) except (ConnectionError, SerialException, PermissionError, ConnectionRefusedError) as e: if log_errors: - logging_warning("Connection failed: %s", e) - logging_error("Failed to connect after %d attempts.", retries) + logging_warning(_("Connection failed: %s"), e) + logging_error(_("Failed to connect after %d attempts."), retries) return str(e) def __process_autopilot_version(self, m, banner_msgs) -> str: if m is None: - return "No AUTOPILOT_VERSION MAVLink message received, connection failed.\n" \ + return _("No AUTOPILOT_VERSION MAVLink message received, connection failed.\n" \ "Only ArduPilot versions newer than 4.3.8 are supported.\n" \ - "Make sure parameter SERIAL0_PROTOCOL is set to 2" + "Make sure parameter SERIAL0_PROTOCOL is set to 2") self.info.set_capabilities(m.capabilities) self.info.set_flight_sw_version(m.flight_sw_version) self.info.set_board_version(m.board_version) @@ -289,7 +291,7 @@ def __process_autopilot_version(self, m, banner_msgs) -> str: if 'ChibiOS:' in msg: os_custom_version = msg.split(' ')[1].strip() if os_custom_version != self.info.os_custom_version: - logging_warning("ChibiOS version missmatch: %s (BANNER) != % s (AUTOPILOT_VERSION)", os_custom_version, + logging_warning(_("ChibiOS version missmatch: %s (BANNER) != % s (AUTOPILOT_VERSION)"), os_custom_version, self.info.os_custom_version) os_custom_version_index = i continue @@ -302,7 +304,7 @@ def __process_autopilot_version(self, m, banner_msgs) -> str: if len(fc_product_banner_substrings) >= 3: fc_product = fc_product_banner_substrings[0] if fc_product != self.info.product: - logging_warning("FC product mismatch: %s (BANNER) != %s (AUTOPILOT_VERSION)", fc_product, self.info.product) + logging_warning(_("FC product mismatch: %s (BANNER) != %s (AUTOPILOT_VERSION)"), fc_product, self.info.product) self.info.product = fc_product # force the one from the banner because it is more reliable return "" @@ -317,7 +319,7 @@ def download_params(self, progress_callback=None) -> Tuple[Dict[str, float], Dic # FIXME this entire if statement is for testing only, remove it later pylint: disable=fixme if self.master is None and self.comport is not None and self.comport.device == 'test': filename = 'params.param' - logging_warning("Testing active, will load all parameters from the %s file", filename) + logging_warning(_("Testing active, will load all parameters from the %s file"), filename) par_dict_with_comments = Par.load_param_file_into_dict(filename) return {k: v.value for k, v in par_dict_with_comments.items()}, {} @@ -326,16 +328,16 @@ def download_params(self, progress_callback=None) -> Tuple[Dict[str, float], Dic # Check if MAVFTP is supported if self.info.is_mavftp_supported: - logging_info("MAVFTP is supported by the %s flight controller", + logging_info(_("MAVFTP is supported by the %s flight controller"), self.comport.device) return self.download_params_via_mavftp(progress_callback) - logging_info("MAVFTP is not supported by the %s flight controller, fallback to MAVLink", self.comport.device) + logging_info(_("MAVFTP is not supported by the %s flight controller, fallback to MAVLink"), self.comport.device) return self.__download_params_via_mavlink(progress_callback), {} def __download_params_via_mavlink(self, progress_callback=None) -> Dict[str, float]: - logging_debug("Will fetch all parameters from the %s flight controller", self.comport.device) + logging_debug(_("Will fetch all parameters from the %s flight controller"), self.comport.device) # Request all parameters self.master.mav.param_request_list_send( self.master.target_system, self.master.target_component @@ -354,16 +356,16 @@ def __download_params_via_mavlink(self, progress_callback=None) -> Dict[str, flo param_id = message['param_id'] # .decode("utf-8") param_value = message['param_value'] parameters[param_id] = param_value - logging_debug('Received parameter: %s = %s', param_id, param_value) + logging_debug(_('Received parameter: %s = %s'), param_id, param_value) # Call the progress callback with the current progress if progress_callback: progress_callback(len(parameters), m.param_count) if m.param_count == len(parameters): - logging_debug("Fetched %d parameter values from the %s flight controller", + logging_debug(_("Fetched %d parameter values from the %s flight controller"), m.param_count, self.comport.device) break except Exception as error: # pylint: disable=broad-except - logging_error('Error: %s', error) + logging_error(_('Error: %s'), error) break return parameters @@ -418,7 +420,7 @@ def reset_and_reconnect(self, reset_progress_callback=None, connection_progress_ return "" # Issue a reset self.master.reboot_autopilot() - logging_info("Reset command sent to ArduPilot.") + logging_info(_("Reset command sent to ArduPilot.")) time_sleep(0.3) self.disconnect() @@ -529,8 +531,9 @@ def add_argparse_arguments(parser): parser.add_argument('--device', type=str, default="", - help='MAVLink connection string to the flight controller. If set to "none" no connection is made.' - ' Defaults to autodetection' + help=_('MAVLink connection string to the flight controller. ' + 'If set to "none" no connection is made.' + ' Defaults to autodetection') ) parser.add_argument('-r', '--reboot-time', type=int, @@ -538,6 +541,6 @@ def add_argparse_arguments(parser): max=50, action=CheckRange, default=7, - help='Flight controller reboot time. ' - 'Default is %(default)s') + help=_('Flight controller reboot time. ' + 'Default is %(default)s')) return parser diff --git a/MethodicConfigurator/common_arguments.py b/MethodicConfigurator/common_arguments.py index 47b04a9c..638b1b57 100644 --- a/MethodicConfigurator/common_arguments.py +++ b/MethodicConfigurator/common_arguments.py @@ -9,16 +9,16 @@ ''' from MethodicConfigurator.version import VERSION - +from MethodicConfigurator.internationalization import _ def add_common_arguments_and_parse(parser): parser.add_argument('--loglevel', type=str, default='INFO', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], - help='Logging level (default is INFO).') + help=_('Logging level (default is INFO).')) parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {VERSION}', - help='Display version information and exit.') + help=_('Display version information and exit.')) return parser.parse_args() diff --git a/MethodicConfigurator/frontend_tkinter_base.py b/MethodicConfigurator/frontend_tkinter_base.py index 15be1c35..28939d29 100644 --- a/MethodicConfigurator/frontend_tkinter_base.py +++ b/MethodicConfigurator/frontend_tkinter_base.py @@ -27,6 +27,8 @@ from MethodicConfigurator.backend_filesystem import LocalFilesystem +from MethodicConfigurator.internationalization import _ + def show_error_message(title: str, message: str): root = tk.Tk() @@ -39,16 +41,16 @@ def show_error_message(title: str, message: str): def show_no_param_files_error(dirname: str): - error_message = f"No intermediate parameter files found in the selected '{dirname}' vehicle directory.\n" \ + error_message = _(f"No intermediate parameter files found in the selected '{dirname}' vehicle directory.\n" \ "Please select and step inside a vehicle directory containing valid ArduPilot intermediate parameter files.\n\n" \ - "Make sure to step inside the directory (double-click) and not just select it." - show_error_message("No Parameter Files Found", error_message) + "Make sure to step inside the directory (double-click) and not just select it.") + show_error_message(_("No Parameter Files Found"), error_message) def show_no_connection_error(error_string: str): - error_message = f"{error_string}\n\nPlease connect a flight controller to the PC,\n" \ - "wait at least 7 seconds and retry." - show_error_message("No Connection to the Flight Controller", error_message) + error_message = _(f"{error_string}\n\nPlease connect a flight controller to the PC,\n" \ + "wait at least 7 seconds and retry.") + show_error_message(_("No Connection to the Flight Controller"), error_message) def show_tooltip(widget, text): @@ -106,10 +108,10 @@ def set_entries_tupple(self, values, selected_element, tooltip=None): if selected_element in values: self.set(selected_element) else: - logging_error("param_file combobox selected string '%s' not in list %s", selected_element, values) + logging_error(_("param_file combobox selected string '%s' not in list %s"), selected_element, values) else: if values: - logging_warning("No param_file combobox element selected") + logging_warning(_("No param_file combobox element selected")) if values: update_combobox_width(self) if tooltip: @@ -243,7 +245,7 @@ def __create_progress_window(self, title: str, message, width, height): self.progress_bar.update() def update_progress_bar_300_pct(self, percent: int): - self.message = "Please be patient, {:.1f}% of {}% complete" + self.message = _("Please be patient, {:.1f}% of {}% complete") self.update_progress_bar(percent/3, max_value=100) def update_progress_bar(self, current_value: int, max_value: int): @@ -257,7 +259,7 @@ def update_progress_bar(self, current_value: int, max_value: int): try: self.progress_window.lift() except tk.TclError as e: - logging_error(f"Lifting window: {e} on file {__file__}") + logging_error(_(f"Lifting window: {e} on file {__file__}")) return self.progress_bar['value'] = current_value diff --git a/MethodicConfigurator/frontend_tkinter_component_editor.py b/MethodicConfigurator/frontend_tkinter_component_editor.py index 60aa377b..1b6a38c7 100644 --- a/MethodicConfigurator/frontend_tkinter_component_editor.py +++ b/MethodicConfigurator/frontend_tkinter_component_editor.py @@ -33,6 +33,8 @@ #from MethodicConfigurator.frontend_tkinter_base import show_tooltip from MethodicConfigurator.frontend_tkinter_base import show_error_message +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.version import VERSION @@ -45,8 +47,8 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = ArgumentParser(description='A GUI for editing JSON files that contain vehicle component configurations. ' - 'Not to be used directly, but through the main ArduPilot methodic configurator script.') + parser = ArgumentParser(description=_('A GUI for editing JSON files that contain vehicle component configurations. ' + 'Not to be used directly, but through the main ArduPilot methodic configurator script.')) parser = LocalFilesystem.add_argparse_arguments(parser) parser = ComponentEditorWindow.add_argparse_arguments(parser) return add_common_arguments_and_parse(parser) @@ -266,10 +268,10 @@ def set_vehicle_configuration_template(self, configuration_template: str): def reverse_key_search(doc: dict, param_name: str, values: list, fallbacks: list) -> list: retv = [int(key) for key, value in doc[param_name]["values"].items() if value in values] if len(values) != len(fallbacks): - logging_error("Length of values %u and fallbacks %u differ for %s", len(values), len(fallbacks), param_name) + logging_error(_("Length of values %u and fallbacks %u differ for %s"), len(values), len(fallbacks), param_name) if retv: return retv - logging_error("No values found for %s in the metadata", param_name) + logging_error(_("No values found for %s in the metadata"), param_name) return fallbacks def __assert_dict_is_uptodate(self, doc: dict, dict_to_check: dict, doc_key: str, doc_dict: str): @@ -279,9 +281,9 @@ def __assert_dict_is_uptodate(self, doc: dict, dict_to_check: dict, doc_key: str if key in dict_to_check: code_protocol = dict_to_check[key].get('protocol', None) if code_protocol != doc_protocol: - logging_error("Protocol %s does not match %s in %s metadata", code_protocol, doc_protocol, doc_key) + logging_error(_("Protocol %s does not match %s in %s metadata"), code_protocol, doc_protocol, doc_key) else: - logging_error("Protocol %s not found in %s metadata", doc_protocol, doc_key) + logging_error(_("Protocol %s not found in %s metadata"), doc_protocol, doc_key) def set_values_from_fc_parameters(self, fc_parameters: dict, doc: dict): self.__assert_dict_is_uptodate(doc, serial_protocols_dict, 'SERIAL1_PROTOCOL', 'values') @@ -302,7 +304,7 @@ def __set_gnss_type_and_protocol_from_fc_parameters(self, fc_parameters: dict): try: gps1_type = int(gps1_type) except ValueError: - logging_error("Invalid non-integer value for GPS_TYPE %f", gps1_type) + logging_error(_("Invalid non-integer value for GPS_TYPE %f"), gps1_type) gps1_type = 0 if str(gps1_type) in gnss_receiver_connection: gps1_connection_type = gnss_receiver_connection[str(gps1_type)].get('type') @@ -321,7 +323,7 @@ def __set_gnss_type_and_protocol_from_fc_parameters(self, fc_parameters: dict): 'CAN_P2_DRIVER' in fc_parameters and fc_parameters['CAN_P2_DRIVER'] == 2: self.data['Components']['GNSS Receiver']['FC Connection']['Type'] = "CAN2" else: - logging_error("Invalid CAN_Dx_PROTOCOL %s and CAN_Px_DRIVER %s for GNSS Receiver", + logging_error(_("Invalid CAN_Dx_PROTOCOL %s and CAN_Px_DRIVER %s for GNSS Receiver"), fc_parameters.get('CAN_D1_PROTOCOL'), fc_parameters.get('CAN_P1_DRIVER')) self.data['Components']['GNSS Receiver']['FC Connection']['Type'] = "None" self.data['Components']['GNSS Receiver']['FC Connection']['Protocol'] = gps1_connection_protocol @@ -353,7 +355,7 @@ def __set_serial_type_and_protocol_from_fc_parameters(self, fc_parameters: dict) try: serial_protocol_nr = int(serial_protocol_nr) except ValueError: - logging_error("Invalid non-integer value for %s_PROTOCOL %f", serial, serial_protocol_nr) + logging_error(_("Invalid non-integer value for %s_PROTOCOL %f"), serial, serial_protocol_nr) serial_protocol_nr = 0 component = serial_protocols_dict[str(serial_protocol_nr)].get('component') protocol = serial_protocols_dict[str(serial_protocol_nr)].get('protocol') @@ -381,7 +383,7 @@ def __set_esc_type_and_protocol_from_fc_parameters(self, fc_parameters: dict, do try: mot_pwm_type = int(mot_pwm_type) except ValueError: - logging_error("Invalid non-integer value for MOT_PWM_TYPE %f", mot_pwm_type) + logging_error(_("Invalid non-integer value for MOT_PWM_TYPE %f"), mot_pwm_type) mot_pwm_type = 0 main_out_functions = [fc_parameters.get('SERVO' + str(i) + '_FUNCTION', 0) for i in range(1, 9)] @@ -447,10 +449,10 @@ def get_combobox_values(param_name: str) -> list: return list(param_metadata[param_name]["values"].values()) if "Bitmask" in param_metadata[param_name] and param_metadata[param_name]["Bitmask"]: return list(param_metadata[param_name]["Bitmask"].values()) - logging_error("No values found for %s in the metadata", param_name) + logging_error(_("No values found for %s in the metadata"), param_name) if param_name in fallbacks: return fallbacks[param_name] - logging_error("No fallback values found for %s", param_name) + logging_error(_("No fallback values found for %s"), param_name) return [] combobox_config = { @@ -553,8 +555,8 @@ def validate_combobox(self, event, path) -> bool: if value not in allowed_values: if event.type == "10": # FocusOut events - show_error_message("Error", f"Invalid value '{value}' for {'>'.join(list(path))}\n" - f"Allowed values are: {', '.join(allowed_values)}") + show_error_message(_("Error"), _(f"Invalid value '{value}' for {'>'.join(list(path))}\n" + f"Allowed values are: {', '.join(allowed_values)}")) combobox.configure(style="comb_input_invalid.TCombobox") return False @@ -574,7 +576,7 @@ def validate_entry_limits(self, event, entry, data_type, limits, name, path): # raise ValueError(f"{name} must be a {data_type.__name__} between {limits[0]} and {limits[1]}") except ValueError as e: if is_focusout_event: - show_error_message("Error", f"Invalid value '{value}' for {'>'.join(list(path))}\n{e}") + show_error_message(_("Error"), _(f"Invalid value '{value}' for {'>'.join(list(path))}\n{e}")) return False entry.configure(style="entry_input_valid.TEntry") return True @@ -585,7 +587,7 @@ def validate_cell_voltage(self, event, entry, path): # pylint: disable=too-many """ chemistry_path = ('Battery', 'Specifications', 'Chemistry') if chemistry_path not in self.entry_widgets: - show_error_message("Error", "Battery Chemistry not set. Will default to Lipo.") + show_error_message(_("Error"), _("Battery Chemistry not set. Will default to Lipo.")) chemistry = "Lipo" else: chemistry = self.entry_widgets[chemistry_path].get() @@ -597,25 +599,25 @@ def validate_cell_voltage(self, event, entry, path): # pylint: disable=too-many if is_focusout_event: entry.delete(0, tk.END) entry.insert(0, BatteryCell.limit_min_voltage(chemistry)) - raise VoltageTooLowError(f"is below the {chemistry} minimum limit of " - f"{BatteryCell.limit_min_voltage(chemistry)}") + raise VoltageTooLowError(_(f"is below the {chemistry} minimum limit of " + f"{BatteryCell.limit_min_voltage(chemistry)}")) if voltage > BatteryCell.limit_max_voltage(chemistry): if is_focusout_event: entry.delete(0, tk.END) entry.insert(0, BatteryCell.limit_max_voltage(chemistry)) - raise VoltageTooHighError(f"is above the {chemistry} maximum limit of " - f"{BatteryCell.limit_max_voltage(chemistry)}") + raise VoltageTooHighError(_(f"is above the {chemistry} maximum limit of " + f"{BatteryCell.limit_max_voltage(chemistry)}")) except (VoltageTooLowError, VoltageTooHighError) as e: if is_focusout_event: - show_error_message("Error", f"Invalid value '{value}' for {'>'.join(list(path))}\n" - f"{e}") + show_error_message(_("Error"), _(f"Invalid value '{value}' for {'>'.join(list(path))}\n" + f"{e}")) else: entry.configure(style="entry_input_invalid.TEntry") return False except ValueError as e: if is_focusout_event: - show_error_message("Error", f"Invalid value '{value}' for {'>'.join(list(path))}\n" - f"{e}\nWill be set to the recommended value.") + show_error_message(_("Error"), _(f"Invalid value '{value}' for {'>'.join(list(path))}\n" + f"{e}\nWill be set to the recommended value.")) entry.delete(0, tk.END) if path[-1] == "Volt per cell max": entry.insert(0, str(BatteryCell.recommended_max_voltage(chemistry))) @@ -647,8 +649,8 @@ def validate_data(self): # pylint: disable=too-many-branches if path == ('ESC', 'FC Connection', 'Type'): self.update_esc_protocol_combobox_entries(value) if value not in entry.cget("values"): - show_error_message("Error", f"Invalid value '{value}' for {'>'.join(list(path))}\n" - f"Allowed values are: {', '.join(entry.cget('values'))}") + show_error_message(_("Error"), _(f"Invalid value '{value}' for {'>'.join(list(path))}\n" + f"Allowed values are: {', '.join(entry.cget('values'))}")) entry.configure(style="comb_input_invalid.TCombobox") invalid_values = True continue @@ -663,7 +665,7 @@ def validate_data(self): # pylint: disable=too-many-branches fc_serial_connection[value] in ['Battery Monitor', 'ESC']: entry.configure(style="comb_input_valid.TCombobox") continue # Allow 'Battery Monitor' and 'ESC' connections using the same SERIAL port - show_error_message("Error", f"Duplicate FC connection type '{value}' for {'>'.join(list(path))}") + show_error_message(_("Error"), _(f"Duplicate FC connection type '{value}' for {'>'.join(list(path))}")) entry.configure(style="comb_input_invalid.TCombobox") duplicated_connections = True continue @@ -681,12 +683,12 @@ def validate_data(self): # pylint: disable=too-many-branches invalid_values = True if path == ('Battery', 'Specifications', 'Volt per cell low'): if value >= self.entry_widgets[('Battery', 'Specifications', 'Volt per cell max')].get(): - show_error_message("Error", "Battery Cell Low voltage must be lower than max voltage") + show_error_message(_("Error"), _("Battery Cell Low voltage must be lower than max voltage")) entry.configure(style="entry_input_invalid.TEntry") invalid_values = True if path == ('Battery', 'Specifications', 'Volt per cell crit'): if value >= self.entry_widgets[('Battery', 'Specifications', 'Volt per cell low')].get(): - show_error_message("Error", "Battery Cell Crit voltage must be lower than low voltage") + show_error_message(_("Error"), _("Battery Cell Crit voltage must be lower than low voltage")) entry.configure(style="entry_input_invalid.TEntry") invalid_values = True diff --git a/MethodicConfigurator/frontend_tkinter_component_editor_base.py b/MethodicConfigurator/frontend_tkinter_component_editor_base.py index 711c905b..6cd76638 100644 --- a/MethodicConfigurator/frontend_tkinter_component_editor_base.py +++ b/MethodicConfigurator/frontend_tkinter_component_editor_base.py @@ -27,6 +27,8 @@ from MethodicConfigurator.frontend_tkinter_base import ScrollFrame from MethodicConfigurator.frontend_tkinter_base import BaseWindow +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.version import VERSION @@ -39,8 +41,8 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = ArgumentParser(description='A GUI for editing JSON files that contain vehicle component configurations. ' - 'Not to be used directly, but through the main ArduPilot methodic configurator script.') + parser = ArgumentParser(description=_('A GUI for editing JSON files that contain vehicle component configurations. ' + 'Not to be used directly, but through the main ArduPilot methodic configurator script.')) parser = LocalFilesystem.add_argparse_arguments(parser) parser = ComponentEditorWindowBase.add_argparse_arguments(parser) return add_common_arguments_and_parse(parser) @@ -58,7 +60,7 @@ def __init__(self, version, local_filesystem: LocalFilesystem=None): super().__init__() self.local_filesystem = local_filesystem - self.root.title("Amilcar Lucas's - ArduPilot methodic configurator " + version + " - Vehicle Component Editor") + self.root.title(_("Amilcar Lucas's - ArduPilot methodic configurator ") + version + _(" - Vehicle Component Editor")) self.root.geometry("880x600") # Set the window width self.data = local_filesystem.load_vehicle_components_json_data(local_filesystem.vehicle_dir) @@ -79,9 +81,9 @@ def __init__(self, version, local_filesystem: LocalFilesystem=None): style.configure("entry_input_invalid.TEntry", fieldbackground="red") style.configure("entry_input_valid.TEntry", fieldbackground="white") - explanation_text = "Please configure all vehicle component properties in this window.\n" - explanation_text += "Scroll down and make sure you do not miss a property.\n" - explanation_text += "Saving the result will write to the vehicle_components.json file." + explanation_text = _("Please configure all vehicle component properties in this window.\n") + explanation_text += _("Scroll down and make sure you do not miss a property.\n") + explanation_text += _("Saving the result will write to the vehicle_components.json file.") explanation_label = ttk.Label(intro_frame, text=explanation_text, wraplength=800, justify=tk.LEFT) explanation_label.configure(style="bigger.TLabel") explanation_label.pack(side=tk.LEFT, padx=(10, 10), pady=(10, 0), anchor=tk.NW) @@ -90,9 +92,9 @@ def __init__(self, version, local_filesystem: LocalFilesystem=None): if local_filesystem.vehicle_image_exists(): image_label = self.put_image_in_label(intro_frame, local_filesystem.vehicle_image_filepath(), 100) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) - show_tooltip(image_label, "Replace the vehicle.jpg file in the vehicle directory to change the vehicle image.") + show_tooltip(image_label, _("Replace the vehicle.jpg file in the vehicle directory to change the vehicle image.")) else: - image_label = ttk.Label(intro_frame, text="Add a 'vehicle.jpg' image file to the vehicle directory.") + image_label = ttk.Label(intro_frame, text=_("Add a 'vehicle.jpg' image file to the vehicle directory.")) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) self.scroll_frame = ScrollFrame(self.main_frame) @@ -102,8 +104,8 @@ def __init__(self, version, local_filesystem: LocalFilesystem=None): save_frame = ttk.Frame(self.main_frame) save_frame.pack(side=tk.TOP, fill="x", expand=False) - self.save_button = ttk.Button(save_frame, text="Save data and start configuration", command=self.save_data) - show_tooltip(self.save_button, "Save component data and start parameter value configuration and tuning.") + self.save_button = ttk.Button(save_frame, text=_("Save data and start configuration"), command=self.save_data) + show_tooltip(self.save_button, _("Save component data and start parameter value configuration and tuning.")) self.save_button.pack(pady=7) def update_json_data(self): # should be overwritten in child classes @@ -186,9 +188,9 @@ def save_data(self): # Save the updated data back to the JSON file if self.local_filesystem.save_vehicle_components_json_data(self.data, self.local_filesystem.vehicle_dir): - show_error_message("Error", "Failed to save data to file. Is the destination write protected?") + show_error_message(_("Error"), _("Failed to save data to file. Is the destination write protected?")) else: - logging_info("Vehicle component data saved successfully.") + logging_info(_("Vehicle component data saved successfully.")) self.root.destroy() # This function will be overwritten in child classes @@ -201,8 +203,8 @@ def add_entry_or_combobox(self, value, entry_frame, path): # pylint: disable=un def add_argparse_arguments(parser): parser.add_argument('--skip-component-editor', action='store_true', - help='Skip the component editor window. Only use this if all components have been configured. ' - 'Default to false') + help=_('Skip the component editor window. Only use this if all components have been configured. ' + 'Default to false')) return parser diff --git a/MethodicConfigurator/frontend_tkinter_connection_selection.py b/MethodicConfigurator/frontend_tkinter_connection_selection.py index ffc774d9..fdfcfa22 100644 --- a/MethodicConfigurator/frontend_tkinter_connection_selection.py +++ b/MethodicConfigurator/frontend_tkinter_connection_selection.py @@ -32,6 +32,8 @@ from MethodicConfigurator.frontend_tkinter_base import ProgressWindow from MethodicConfigurator.frontend_tkinter_base import BaseWindow +from MethodicConfigurator.internationalization import _ + # https://dev.to/geraldew/python-tkinter-an-exercise-in-wrapping-the-combobox-ndb class PairTupleCombobox(ttk.Combobox): # pylint: disable=too-many-ancestors @@ -52,7 +54,7 @@ def process_list_pair_tuple(self, list_pair_tuple): r_list_keys.append(key) r_list_shows.append(value) else: - logging_critical("list_pair_tuple must be a tuple or a dictionary, not %s", type(list_pair_tuple)) + logging_critical(_("list_pair_tuple must be a tuple or a dictionary, not %s"), type(list_pair_tuple)) sys_exit(1) return r_list_keys, r_list_shows @@ -70,17 +72,17 @@ def set_entries_tupple(self, list_pair_tuple, selected_element): default_key_index = self.list_keys.index(selected_element) self.current(default_key_index) except IndexError: - logging_critical("%s combobox selected string '%s' not in list %s", + logging_critical(_("%s combobox selected string '%s' not in list %s"), self.cb_name, selected_element, self.list_keys) sys_exit(1) except ValueError: - logging_critical("%s combobox selected string '%s' not in list %s", + logging_critical(_("%s combobox selected string '%s' not in list %s"), self.cb_name, selected_element, self.list_keys) sys_exit(1) update_combobox_width(self) else: # Normal users do not need this information - logging_debug("No %s combobox element selected", self.cb_name) + logging_debug(_("No %s combobox element selected"), self.cb_name) def get_selected_key(self): try: @@ -111,7 +113,7 @@ def __init__(self, parent, parent_frame, flight_controller: FlightController, # self.container_frame = ttk.Frame(parent_frame) # Create a description label for the flight controller connection selection - conn_selection_label = ttk.Label(self.container_frame, text="flight controller connection:") + conn_selection_label = ttk.Label(self.container_frame, text=_("flight controller connection:")) conn_selection_label.pack(side=tk.TOP) # Add the label to the top of the conn_selection_frame # Create a read-only combobox for flight controller connection selection @@ -121,12 +123,12 @@ def __init__(self, parent, parent_frame, flight_controller: FlightController, # state='readonly') self.conn_selection_combobox.bind("<>", self.on_select_connection_combobox_change) self.conn_selection_combobox.pack(side=tk.TOP, pady=(4, 0)) - show_tooltip(self.conn_selection_combobox, "Select the flight controller connection\nYou can add a custom connection " - "to the existing ones") + show_tooltip(self.conn_selection_combobox, _("Select the flight controller connection\nYou can add a custom " + "connection to the existing ones")) def on_select_connection_combobox_change(self, _event): selected_connection = self.conn_selection_combobox.get_selected_key() - logging_debug(f"Connection combobox changed to: {selected_connection}") + logging_debug(_(f"Connection combobox changed to: {selected_connection}")) if self.flight_controller.master is None or selected_connection != self.flight_controller.comport.device: if selected_connection == 'Add another': if not self.add_connection() and self.previous_selection: @@ -137,26 +139,26 @@ def on_select_connection_combobox_change(self, _event): def add_connection(self): # Open the connection selection dialog - selected_connection = simpledialog.askstring("Flight Controller Connection", - "Enter the connection string to the flight controller. " + selected_connection = simpledialog.askstring(_("Flight Controller Connection"), + _("Enter the connection string to the flight controller. " "Examples are:\n\nCOM4 (on windows)\n" "/dev/serial/by-id/usb-xxx (on linux)\n" "tcp:127.0.0.1:5761\n" - "udp:127.0.0.1:14551") + "udp:127.0.0.1:14551")) if selected_connection: - logging_debug(f"Will add new connection: {selected_connection} if not duplicated") + logging_debug(_(f"Will add new connection: {selected_connection} if not duplicated")) self.flight_controller.add_connection(selected_connection) connection_tuples = self.flight_controller.get_connection_tuples() - logging_debug(f"Updated connection tuples: {connection_tuples} with selected connection: {selected_connection}") + logging_debug(_(f"Updated connection tuples: {connection_tuples} with selected connection: {selected_connection}")) self.conn_selection_combobox.set_entries_tupple(connection_tuples, selected_connection) self.reconnect(selected_connection) else: - logging_debug(f"Add connection canceled or string empty {selected_connection}") + logging_debug(_(f"Add connection canceled or string empty {selected_connection}")) return selected_connection def reconnect(self, selected_connection: str = ""): # defaults to auto-connect - self.connection_progress_window = ProgressWindow(self.parent.root, "Connecting with the FC", - "Connection step {} of {}") + self.connection_progress_window = ProgressWindow(self.parent.root, _("Connecting with the FC"), + _("Connection step {} of {}")) error_message = self.flight_controller.connect(selected_connection, self.connection_progress_window.update_progress_bar) if error_message: @@ -167,7 +169,7 @@ def reconnect(self, selected_connection: str = ""): # defaults to auto-connect self.previous_selection = self.flight_controller.comport.device if self.destroy_parent_on_connect: self.parent.root.destroy() - if self.download_params_on_connect and hasattr(self.parent, "download_flight_controller_parameters"): + if self.download_params_on_connect and hasattr(self.parent, _("download_flight_controller_parameters")): self.parent.download_flight_controller_parameters(redownload=False) return False @@ -182,42 +184,42 @@ class ConnectionSelectionWindow(BaseWindow): """ def __init__(self, flight_controller: FlightController, connection_result_string: str): super().__init__() - self.root.title("Flight controller connection") + self.root.title(_("Flight controller connection")) self.root.geometry("460x450") # Set the window size # Explain why we are here if flight_controller.comport is None: - introduction_text = "No ArduPilot flight controller was auto-detected detected yet." + introduction_text = _("No ArduPilot flight controller was auto-detected detected yet.") else: if ":" in connection_result_string: introduction_text = connection_result_string.replace(":", ":\n") else: introduction_text = connection_result_string self.introduction_label = ttk.Label(self.main_frame, anchor=tk.CENTER, justify=tk.CENTER, - text=introduction_text + "\nChoose one of the following three options:") + text=introduction_text + _("\nChoose one of the following three options:")) self.introduction_label.pack(expand=False, fill=tk.X, padx=6, pady=6) # Option 1 - Auto-connect - option1_label = ttk.Label(text="Auto connection", style="Bold.TLabel") + option1_label = ttk.Label(text=_("Auto connection"), style="Bold.TLabel") option1_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option1_label, borderwidth=2, relief="solid") option1_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) option1_label = ttk.Label(option1_label_frame, anchor=tk.CENTER, justify=tk.CENTER, - text="Connect a flight controller to the PC,\n" + text=_("Connect a flight controller to the PC,\n" "wait 7 seconds for it to fully boot and\n" - "press the Auto-connect button below to connect to it") + "press the Auto-connect button below to connect to it")) option1_label.pack(expand=False, fill=tk.X, padx=6) - autoconnect_button = ttk.Button(option1_label_frame, text="Auto-connect", command=self.fc_autoconnect) + autoconnect_button = ttk.Button(option1_label_frame, text=_("Auto-connect"), command=self.fc_autoconnect) autoconnect_button.pack(expand=False, fill=tk.X, padx=100, pady=6) - show_tooltip(autoconnect_button, "Auto-connect to a 'Mavlink'-talking serial device") + show_tooltip(autoconnect_button, _("Auto-connect to a 'Mavlink'-talking serial device")) # Option 2 - Manually select the flight controller connection or add a new one - option2_label = ttk.Label(text="Manual connection", style="Bold.TLabel") + option2_label = ttk.Label(text=_("Manual connection"), style="Bold.TLabel") option2_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option2_label, borderwidth=2, relief="solid") option2_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) option2_label = ttk.Label(option2_label_frame, anchor=tk.CENTER, justify=tk.CENTER, - text="Connect a flight controller to the PC,\n" + text=_("Connect a flight controller to the PC,\n" "wait 7 seconds for it to fully boot and\n" - "manually select the fight controller connection or add a new one") + "manually select the fight controller connection or add a new one")) option2_label.pack(expand=False, fill=tk.X, padx=6) self.connection_selection_widgets = ConnectionSelectionWidgets(self, option2_label_frame, flight_controller, destroy_parent_on_connect=True, @@ -225,24 +227,24 @@ def __init__(self, flight_controller: FlightController, connection_result_string self.connection_selection_widgets.container_frame.pack(expand=False, fill=tk.X, padx=80, pady=6) # Option 3 - Skip FC connection, just edit the .param files on disk - option3_label = ttk.Label(text="No connection", style="Bold.TLabel") + option3_label = ttk.Label(text=_("No connection"), style="Bold.TLabel") option3_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option3_label, borderwidth=2, relief="solid") option3_label_frame.pack(expand=False, fill=tk.X, padx=6, pady=6) #option3_label = ttk.Label(option3_label_frame, anchor=tk.CENTER, justify=tk.CENTER, - # text="Skip the flight controller connection,\n" + # text=_("Skip the flight controller connection,\n") # "no default parameter values will be fetched from the FC,\n" # "default parameter values from disk will be used instead\n" # "(if '00_default.param' file is present)\n" # "and just edit the intermediate '.param' files on disk") #option3_label.pack(expand=False, fill=tk.X, padx=6) skip_fc_connection_button = ttk.Button(option3_label_frame, - text="Skip FC connection, just edit the .param files on disk", + text=_("Skip FC connection, just edit the .param files on disk"), command=lambda flight_controller=flight_controller: self.skip_fc_connection(flight_controller)) skip_fc_connection_button.pack(expand=False, fill=tk.X, padx=15, pady=6) show_tooltip(skip_fc_connection_button, - "No parameter values will be fetched from the FC, default parameter values from disk will be used\n" - "instead (if '00_default.param' file is present) and just edit the intermediate '.param' files on disk") + _("No parameter values will be fetched from the FC, default parameter values from disk will be used\n" + "instead (if '00_default.param' file is present) and just edit the intermediate '.param' files on disk")) # Bind the close_connection_and_quit function to the window close event self.root.protocol("WM_DELETE_WINDOW", self.close_and_quit) @@ -254,8 +256,8 @@ def fc_autoconnect(self): self.connection_selection_widgets.reconnect() def skip_fc_connection(self, flight_controller: FlightController): - logging_warning("Will proceed without FC connection. FC parameters will not be downloaded nor uploaded") - logging_warning("Only the intermediate '.param' files on the PC disk will be edited") + logging_warning(_("Will proceed without FC connection. FC parameters will not be downloaded nor uploaded")) + logging_warning(_("Only the intermediate '.param' files on the PC disk will be edited")) flight_controller.disconnect() self.root.destroy() @@ -269,8 +271,8 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = ArgumentParser(description='This main is for testing and development only. ' - 'Usually, the ConnectionSelectionWidgets is called from another script') + parser = ArgumentParser(description=_('This main is for testing and development only. ' + 'Usually, the ConnectionSelectionWidgets is called from another script')) parser = FlightController.add_argparse_arguments(parser) return add_common_arguments_and_parse(parser) @@ -280,8 +282,8 @@ def main(): logging_basicConfig(level=logging_getLevelName(args.loglevel), format='%(asctime)s - %(levelname)s - %(message)s') - logging_warning("This main is for testing and development only, usually the ConnectionSelectionWindow is called from " - "another script") + logging_warning(_("This main is for testing and development only, usually the ConnectionSelectionWindow is called from " + "another script")) flight_controller = FlightController(args.reboot_time) # Initialize your FlightController instance result = flight_controller.connect(device=args.device) # Connect to the flight controller diff --git a/MethodicConfigurator/frontend_tkinter_directory_selection.py b/MethodicConfigurator/frontend_tkinter_directory_selection.py index 4282a873..10fbef8e 100644 --- a/MethodicConfigurator/frontend_tkinter_directory_selection.py +++ b/MethodicConfigurator/frontend_tkinter_directory_selection.py @@ -39,6 +39,8 @@ from MethodicConfigurator.frontend_tkinter_template_overview import TemplateOverviewWindow +from MethodicConfigurator.internationalization import _ + class DirectorySelectionWidgets(): """ @@ -90,7 +92,7 @@ def on_select_directory(self): if self.is_template_selection: TemplateOverviewWindow(self.parent.root) selected_directory = ProgramSettings.get_recently_used_dirs()[0] - logging_info("Selected template directory: %s", selected_directory) + logging_info(_("Selected template directory: %s"), selected_directory) else: selected_directory = filedialog.askdirectory(initialdir=self.directory, title=f"Select {self.label_text}") @@ -150,12 +152,12 @@ def __init__(self, parent: ttk, parent_frame: ttk.Frame, # pylint: disable=too- local_filesystem: LocalFilesystem, initial_dir: str, destroy_parent_on_open: bool) -> None: # Call the parent constructor with the necessary arguments - super().__init__(parent, parent_frame, initial_dir, "Vehicle configuration directory:", + super().__init__(parent, parent_frame, initial_dir, _("Vehicle configuration directory:"), False, - "Vehicle-specific directory containing the intermediate\n" - "parameter files to be uploaded to the flight controller", - "Select the vehicle-specific configuration directory containing the\n" - "intermediate parameter files to be uploaded to the flight controller" \ + _("Vehicle-specific directory containing the intermediate\n" + "parameter files to be uploaded to the flight controller"), + _("Select the vehicle-specific configuration directory containing the\n" + "intermediate parameter files to be uploaded to the flight controller") \ if destroy_parent_on_open else '', False) self.local_filesystem = local_filesystem @@ -165,22 +167,22 @@ def on_select_directory(self): # Call the base class method to open the directory selection dialog if super().on_select_directory(): if "vehicle_templates" in self.directory and not self.local_filesystem.allow_editing_template_files: - messagebox.showerror("Invalid Vehicle Directory Selected", - "Please do not edit the files provided 'vehicle_templates' directory\n" - "as those are used as a template for new vehicles") + messagebox.showerror(_("Invalid Vehicle Directory Selected"), + _("Please do not edit the files provided 'vehicle_templates' directory\n" + "as those are used as a template for new vehicles")) return self.local_filesystem.vehicle_dir = self.directory if not self.local_filesystem.vehicle_configuration_files_exist(self.directory): - messagebox.showerror("Invalid Vehicle Directory Selected", - "Selected directory must contain files matching \\d\\d_*\\.param " \ - f"and a {self.local_filesystem.vehicle_components_json_filename} file") + messagebox.showerror(_("Invalid Vehicle Directory Selected"), + _("Selected directory must contain files matching \\d\\d_*\\.param " \ + f"and a {self.local_filesystem.vehicle_components_json_filename} file")) return try: self.local_filesystem.re_init(self.directory, self.local_filesystem.vehicle_type) except SystemExit as exp: - messagebox.showerror("Fatal error reading parameter files", f"{exp}") + messagebox.showerror(_("Fatal error reading parameter files"), f"{exp}") raise if self.local_filesystem.file_parameters: @@ -207,24 +209,24 @@ class VehicleDirectorySelectionWindow(BaseWindow): def __init__(self, local_filesystem: LocalFilesystem, fc_connected: bool = False): super().__init__() self.local_filesystem = local_filesystem - self.root.title("Amilcar Lucas's - ArduPilot methodic configurator " + VERSION + \ - " - Select vehicle configuration directory") + self.root.title(_("Amilcar Lucas's - ArduPilot methodic configurator ") + VERSION + \ + _(" - Select vehicle configuration directory")) self.root.geometry("800x625") # Set the window size self.use_fc_params = tk.BooleanVar(value=False) self.configuration_template = None # will be set to a string if a template was used # Explain why we are here if local_filesystem.vehicle_dir == LocalFilesystem.getcwd(): - introduction_text = "No intermediate parameter files found\nin current working directory." + introduction_text = _("No intermediate parameter files found\nin current working directory.") else: - introduction_text = "No intermediate parameter files found\nin the --vehicle-dir specified directory." + introduction_text = _("No intermediate parameter files found\nin the --vehicle-dir specified directory.") introduction_label = ttk.Label(self.main_frame, anchor=tk.CENTER, justify=tk.CENTER, - text=introduction_text + "\nChoose one of the following three options:") + text=introduction_text + _("\nChoose one of the following three options:")) introduction_label.pack(expand=False, fill=tk.X, padx=6, pady=6) template_dir, new_base_dir, vehicle_dir = LocalFilesystem.get_recently_used_dirs() self.create_option1_widgets(template_dir, new_base_dir, - "MyVehicleName", + _("MyVehicleName"), fc_connected) self.create_option2_widgets(vehicle_dir) self.create_option3_widgets(vehicle_dir) @@ -238,15 +240,15 @@ def close_and_quit(self): def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: str, initial_new_dir: str, fc_connected: bool): # Option 1 - Create a new vehicle configuration directory based on an existing template - option1_label = ttk.Label(text="New", style="Bold.TLabel") + option1_label = ttk.Label(text=_("New"), style="Bold.TLabel") option1_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option1_label, borderwidth=2, relief="solid") option1_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) - template_dir_edit_tooltip = "Existing vehicle template directory containing the intermediate\n" \ - "parameter files to be copied to the new vehicle configuration directory" - template_dir_btn_tooltip = "Select the existing vehicle template directory containing the intermediate\n" \ - "parameter files to be copied to the new vehicle configuration directory" + template_dir_edit_tooltip = _("Existing vehicle template directory containing the intermediate\n" \ + "parameter files to be copied to the new vehicle configuration directory") + template_dir_btn_tooltip = _("Select the existing vehicle template directory containing the intermediate\n" \ + "parameter files to be copied to the new vehicle configuration directory") self.template_dir = DirectorySelectionWidgets(self, option1_label_frame, initial_template_dir, - "Source Template directory:", + _("Source Template directory:"), False, template_dir_edit_tooltip, template_dir_btn_tooltip, @@ -254,48 +256,49 @@ def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: st self.template_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) use_fc_params_checkbox = ttk.Checkbutton(option1_label_frame, variable=self.use_fc_params, - text="Use parameter values from connected FC, not from template files") + text=_("Use parameter values from connected FC, not from template files")) use_fc_params_checkbox.pack(anchor=tk.NW) show_tooltip(use_fc_params_checkbox, - "Use the parameter values from the connected flight controller instead of the\n" \ + _("Use the parameter values from the connected flight controller instead of the\n" \ "template files when creating a new vehicle configuration directory from a template.\n" \ - "This option is only available when a flight controller is connected") + "This option is only available when a flight controller is connected")) if not fc_connected: self.use_fc_params.set(False) use_fc_params_checkbox.config(state=tk.DISABLED) - new_base_dir_edit_tooltip = "Existing directory where the new vehicle configuration directory will be created" - new_base_dir_btn_tooltip = "Select the directory where the new vehicle configuration directory will be created" + new_base_dir_edit_tooltip = _("Existing directory where the new vehicle configuration directory will be created") + new_base_dir_btn_tooltip = _("Select the directory where the new vehicle configuration directory will be created") self.new_base_dir = DirectorySelectionWidgets(self, option1_label_frame, initial_base_dir, - "Destination base directory:", + _("Destination base directory:"), False, new_base_dir_edit_tooltip, new_base_dir_btn_tooltip, False) self.new_base_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) - new_dir_edit_tooltip = "A new vehicle configuration directory with this name will be created at the " \ - "(destination) base directory" + new_dir_edit_tooltip = _("A new vehicle configuration directory with this name will be created at the " \ + "(destination) base directory") self.new_dir = DirectoryNameWidgets(option1_label_frame, initial_new_dir, - "Destination new vehicle name:", + _("Destination new vehicle name:"), new_dir_edit_tooltip) self.new_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) create_vehicle_directory_from_template_button = ttk.Button(option1_label_frame, - text="Create vehicle configuration directory from template", + text=_("Create vehicle configuration directory from " + "template"), command=self.create_new_vehicle_from_template) create_vehicle_directory_from_template_button.pack(expand=False, fill=tk.X, padx=20, pady=5, anchor=tk.CENTER) show_tooltip(create_vehicle_directory_from_template_button, - "Create a new vehicle configuration directory on the (destination) base directory,\n" + _("Create a new vehicle configuration directory on the (destination) base directory,\n" "copy the template files from the (source) template directory to it and\n" - "load the newly created files into the application") + "load the newly created files into the application")) def create_option2_widgets(self, initial_dir: str): # Option 2 - Use an existing vehicle configuration directory - option2_label = ttk.Label(text="Open", style="Bold.TLabel") + option2_label = ttk.Label(text=_("Open"), style="Bold.TLabel") option2_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option2_label, borderwidth=2, relief="solid") option2_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) option2_label = ttk.Label(option2_label_frame, anchor=tk.CENTER, justify=tk.CENTER, - text="Use an existing vehicle configuration directory with\n" \ - "intermediate parameter files, apm.pdef.xml and vehicle_components.json") + text=_("Use an existing vehicle configuration directory with\n" \ + "intermediate parameter files, apm.pdef.xml and vehicle_components.json")) option2_label.pack(expand=False, fill=tk.X, padx=6) self.connection_selection_widgets = VehicleDirectorySelectionWidgets(self, option2_label_frame, self.local_filesystem, @@ -305,14 +308,14 @@ def create_option2_widgets(self, initial_dir: str): def create_option3_widgets(self, last_vehicle_dir: str): # Option 3 - Open the last used vehicle configuration directory - option3_label = ttk.Label(text="Re-Open", style="Bold.TLabel") + option3_label = ttk.Label(text=_("Re-Open"), style="Bold.TLabel") option3_label_frame = ttk.LabelFrame(self.main_frame, labelwidget=option3_label, borderwidth=2, relief="solid") option3_label_frame.pack(expand=True, fill=tk.X, padx=6, pady=6) last_dir = DirectorySelectionWidgets(self, option3_label_frame, last_vehicle_dir if last_vehicle_dir else '', - "Last used vehicle configuration directory:", + _("Last used vehicle configuration directory:"), False, - "Last used vehicle configuration directory", + _("Last used vehicle configuration directory"), "", False) last_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) @@ -321,13 +324,13 @@ def create_option3_widgets(self, last_vehicle_dir: str): button_state = tk.NORMAL if last_vehicle_dir and self.local_filesystem.directory_exists(last_vehicle_dir) else \ tk.DISABLED open_last_vehicle_directory_button = ttk.Button(option3_label_frame, - text="Open Last Used Vehicle Configuration Directory", + text=_("Open Last Used Vehicle Configuration Directory"), command=lambda last_vehicle_dir=last_vehicle_dir: \ self.open_last_vehicle_directory(last_vehicle_dir), state=button_state) open_last_vehicle_directory_button.pack(expand=False, fill=tk.X, padx=20, pady=5, anchor=tk.CENTER) show_tooltip(open_last_vehicle_directory_button, - "Directly open the last used vehicle configuration directory for configuring and tuning the vehicle") + _("Directly open the last used vehicle configuration directory for configuring and tuning the vehicle")) def create_new_vehicle_from_template(self): # Get the selected template directory and new vehicle configuration directory name @@ -336,28 +339,28 @@ def create_new_vehicle_from_template(self): new_vehicle_name = self.new_dir.get_selected_directory() if template_dir == "": - messagebox.showerror("Vehicle template directory", "Vehicle template directory cannot be empty") + messagebox.showerror(_("Vehicle template directory"), _("Vehicle template directory cannot be empty")) return if not LocalFilesystem.directory_exists(template_dir): - messagebox.showerror("Vehicle template directory", "Vehicle template directory does not exist") + messagebox.showerror(_("Vehicle template directory"), _("Vehicle template directory does not exist")) return if new_vehicle_name == "": - messagebox.showerror("New vehicle directory", "New vehicle name cannot be empty") + messagebox.showerror(_("New vehicle directory"), _("New vehicle name cannot be empty")) return if not LocalFilesystem.valid_directory_name(new_vehicle_name): - messagebox.showerror("New vehicle directory", "New vehicle name must not contain invalid characters") + messagebox.showerror(_("New vehicle directory"), _("New vehicle name must not contain invalid characters")) return new_vehicle_dir = LocalFilesystem.new_vehicle_dir(new_base_dir, new_vehicle_name) error_msg = self.local_filesystem.create_new_vehicle_dir(new_vehicle_dir) if error_msg: - messagebox.showerror("New vehicle directory", error_msg) + messagebox.showerror(_("New vehicle directory"), error_msg) return error_msg = self.local_filesystem.copy_template_files_to_new_vehicle_dir(template_dir, new_vehicle_dir) if error_msg: - messagebox.showerror("Copying template files", error_msg) + messagebox.showerror(_("Copying template files"), error_msg) return # Update the local_filesystem with the new vehicle configuration directory @@ -366,7 +369,7 @@ def create_new_vehicle_from_template(self): try: self.local_filesystem.re_init(new_vehicle_dir, self.local_filesystem.vehicle_type) except SystemExit as exp: - messagebox.showerror("Fatal error reading parameter files", f"{exp}") + messagebox.showerror(_("Fatal error reading parameter files"), f"{exp}") raise files = list(self.local_filesystem.file_parameters.keys()) @@ -387,7 +390,7 @@ def open_last_vehicle_directory(self, last_vehicle_dir: str): try: self.local_filesystem.re_init(last_vehicle_dir, self.local_filesystem.vehicle_type) except SystemExit as exp: - messagebox.showerror("Fatal error reading parameter files", f"{exp}") + messagebox.showerror(_("Fatal error reading parameter files"), f"{exp}") raise if self.local_filesystem.file_parameters: @@ -400,9 +403,9 @@ def open_last_vehicle_directory(self, last_vehicle_dir: str): show_no_param_files_error(last_vehicle_dir) else: # If no last opened directory is found, display a message to the user - messagebox.showerror("No Last Vehicle Directory Found", - "No last opened vehicle configuration directory was found.\n" \ - "Please select a directory manually.") + messagebox.showerror(_("No Last Vehicle Directory Found"), + _("No last opened vehicle configuration directory was found.\n" \ + "Please select a directory manually.")) def argument_parser(): """ @@ -413,8 +416,8 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = ArgumentParser(description='This main is for testing and development only. ' - 'Usually, the VehicleDirectorySelectionWindow is called from another script') + parser = ArgumentParser(description=_('This main is for testing and development only. ' + 'Usually, the VehicleDirectorySelectionWindow is called from another script')) parser = LocalFilesystem.add_argparse_arguments(parser) return add_common_arguments_and_parse(parser) @@ -424,8 +427,8 @@ def main(): logging_basicConfig(level=logging_getLevelName(args.loglevel), format='%(asctime)s - %(levelname)s - %(message)s') - logging_warning("This main is for testing and development only, usually the VehicleDirectorySelectionWindow is" - " called from another script") + logging_warning(_("This main is for testing and development only, usually the VehicleDirectorySelectionWindow is" + " called from another script")) local_filesystem = LocalFilesystem(args.vehicle_dir, args.vehicle_type, None, args.allow_editing_template_files) @@ -433,7 +436,7 @@ def main(): files = list(local_filesystem.file_parameters.keys()) if not files: - logging_error("No intermediate parameter files found in %s.", args.vehicle_dir) + logging_error(_("No intermediate parameter files found in %s."), args.vehicle_dir) window = VehicleDirectorySelectionWindow(local_filesystem) window.root.mainloop() diff --git a/MethodicConfigurator/frontend_tkinter_entry_dynamic.py b/MethodicConfigurator/frontend_tkinter_entry_dynamic.py index 875c363a..8173aab8 100644 --- a/MethodicConfigurator/frontend_tkinter_entry_dynamic.py +++ b/MethodicConfigurator/frontend_tkinter_entry_dynamic.py @@ -17,6 +17,8 @@ import tkinter as tk from tkinter import ttk +from MethodicConfigurator.internationalization import _ + def autoscroll(sbar, first, last): """Hide and show scrollbar as needed.""" @@ -35,7 +37,7 @@ class EntryWithDynamicalyFilteredListbox(Entry): # pylint: disable=too-many-anc def __init__(self, master, list_of_items=None, custom_filter_function=None, listbox_width=None, listbox_height=12, # pylint: disable=too-many-arguments ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs): if list_of_items is None: - raise ValueError("List_of_items can't be 'None'") + raise ValueError(_("List_of_items can't be 'None'")) self._list_of_items = list_of_items self.filter_function = custom_filter_function if custom_filter_function else self.default_filter_function diff --git a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py index b795a9cb..85fb11ad 100644 --- a/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py +++ b/MethodicConfigurator/frontend_tkinter_flightcontroller_info.py @@ -21,6 +21,8 @@ from MethodicConfigurator.frontend_tkinter_base import ProgressWindow from MethodicConfigurator.frontend_tkinter_base import BaseWindow +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.version import VERSION @@ -30,7 +32,7 @@ class FlightControllerInfoWindow(BaseWindow): """ def __init__(self, flight_controller: FlightController): super().__init__() - self.root.title("ArduPilot methodic configurator " + VERSION + " - Flight Controller Info") + self.root.title(_("ArduPilot methodic configurator ") + VERSION + _(" - Flight Controller Info")) self.root.geometry("500x350") # Adjust the window size as needed self.flight_controller = flight_controller self.param_default_values = {} @@ -59,19 +61,19 @@ def __init__(self, flight_controller: FlightController): self.info_frame.columnconfigure(1, weight=1) - logging_info("Firmware Version: %s", flight_controller.info.flight_sw_version_and_type) - logging_info(f"Firmware first 8 hex bytes of the FC git hash: {flight_controller.info.flight_custom_version}") - logging_info(f"Firmware first 8 hex bytes of the ChibiOS git hash: {flight_controller.info.os_custom_version}") - logging_info(f"Flight Controller HW / board version: {flight_controller.info.board_version}") - logging_info(f"Flight Controller USB vendor ID: {flight_controller.info.vendor}") - logging_info(f"Flight Controller USB product ID: {flight_controller.info.product}") + logging_info(_("Firmware Version: %s"), flight_controller.info.flight_sw_version_and_type) + logging_info(_(f"Firmware first 8 hex bytes of the FC git hash: {flight_controller.info.flight_custom_version}")) + logging_info(_(f"Firmware first 8 hex bytes of the ChibiOS git hash: {flight_controller.info.os_custom_version}")) + logging_info(_(f"Flight Controller HW / board version: {flight_controller.info.board_version}")) + logging_info(_(f"Flight Controller USB vendor ID: {flight_controller.info.vendor}")) + logging_info(_(f"Flight Controller USB product ID: {flight_controller.info.product}")) self.root.after(50, self.download_flight_controller_parameters()) # 50 milliseconds self.root.mainloop() def download_flight_controller_parameters(self): - param_download_progress_window = ProgressWindow(self.root, "Downloading FC parameters", - "Downloaded {} of {} parameters") + param_download_progress_window = ProgressWindow(self.root, _("Downloading FC parameters"), + _("Downloaded {} of {} parameters")) self.flight_controller.fc_parameters, self.param_default_values = self.flight_controller.download_params( param_download_progress_window.update_progress_bar) param_download_progress_window.destroy() # for the case that '--device test' and there is no real FC connected diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor.py b/MethodicConfigurator/frontend_tkinter_parameter_editor.py index 735387a6..3bec2646 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor.py @@ -48,6 +48,8 @@ from MethodicConfigurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.tempcal_imu import IMUfit from MethodicConfigurator.version import VERSION @@ -71,19 +73,19 @@ def __init__(self, root: tk.Tk, local_filesystem, current_file: str): self.__create_documentation_frame() def __create_documentation_frame(self): - self.documentation_frame = ttk.LabelFrame(self.root, text="Documentation") + self.documentation_frame = ttk.LabelFrame(self.root, text=_("Documentation")) self.documentation_frame.pack(side=tk.TOP, fill="x", expand=False, pady=(4, 4), padx=(4, 4)) # Create a grid structure within the documentation_frame documentation_grid = ttk.Frame(self.documentation_frame) documentation_grid.pack(fill="both", expand=True) - descriptive_texts = ["Forum Blog:", "Wiki:", "External tool:", "Mandatory:"] - descriptive_tooltips = ["ArduPilot's forum Methodic configuration Blog post relevant for the current file", - "ArduPilot's wiki page relevant for the current file", - "External tool or documentation relevant for the current file", - "Mandatory level of the current file,\n 100% you MUST use this file to configure the " - "vehicle,\n 0% you can ignore this file if it does not apply to your vehicle"] + descriptive_texts = [_("Forum Blog:"), _("Wiki:"), _("External tool:"), _("Mandatory:")] + descriptive_tooltips = [_("ArduPilot's forum Methodic configuration Blog post relevant for the current file"), + _("ArduPilot's wiki page relevant for the current file"), + _("External tool or documentation relevant for the current file"), + _("Mandatory level of the current file,\n 100% you MUST use this file to configure the " + "vehicle,\n 0% you can ignore this file if it does not apply to your vehicle")] for i, text in enumerate(descriptive_texts): # Create labels for the first column with static descriptive text label = ttk.Label(documentation_grid, text=text) @@ -100,21 +102,21 @@ def __create_documentation_frame(self): def update_documentation_labels(self, current_file: str): self.current_file = current_file if current_file: - frame_title = f"{current_file} Documentation" + frame_title = _(f"{current_file} Documentation") else: - frame_title = "Documentation" + frame_title = _("Documentation") self.documentation_frame.config(text=frame_title) blog_text, blog_url = self.local_filesystem.get_documentation_text_and_url(current_file, 'blog') - self.__update_documentation_label('Forum Blog:', blog_text, blog_url) + self.__update_documentation_label(_('Forum Blog:'), blog_text, blog_url) wiki_text, wiki_url = self.local_filesystem.get_documentation_text_and_url(current_file, 'wiki') - self.__update_documentation_label('Wiki:', wiki_text, wiki_url) + self.__update_documentation_label(_('Wiki:'), wiki_text, wiki_url) external_tool_text, external_tool_url = self.local_filesystem.get_documentation_text_and_url(current_file, 'external_tool') - self.__update_documentation_label('External tool:', external_tool_text, external_tool_url) + self.__update_documentation_label(_('External tool:'), external_tool_text, external_tool_url) mandatory_text, mandatory_url = self.local_filesystem.get_documentation_text_and_url(current_file, 'mandatory') - self.__update_documentation_label('Mandatory:', mandatory_text, mandatory_url, False) + self.__update_documentation_label(_('Mandatory:'), mandatory_text, mandatory_url, False) def __update_documentation_label(self, label_key, text, url, url_expected=True): label = self.documentation_labels[label_key] @@ -126,40 +128,40 @@ def __update_documentation_label(self, label_key, text, url, url_expected=True): label.config(text=text, foreground="black", cursor="arrow", underline=False) label.bind("", lambda event: None) if url_expected: - show_tooltip(label, "Documentation URL not available") + show_tooltip(label, _("Documentation URL not available")) def show_about_window(root, version: str): # Create a new window for the custom "About" message about_window = tk.Toplevel(root) - about_window.title("About") + about_window.title(_("About")) about_window.geometry("650x220") main_frame = ttk.Frame(about_window) main_frame.pack(expand=True, fill=tk.BOTH) # Add the "About" message - about_message = f"ArduPilot Methodic Configurator Version: {version}\n\n" \ + about_message = _(f"ArduPilot Methodic Configurator Version: {version}\n\n" \ "A clear configuration sequence for ArduPilot vehicles.\n\n" \ "Copyright © 2024 Amilcar do Carmo Lucas and ArduPilot.org\n\n" \ - "Licensed under the GNU General Public License v3.0" + "Licensed under the GNU General Public License v3.0") about_label = ttk.Label(main_frame, text=about_message, wraplength=450) about_label.grid(column=0, row=0, padx=10, pady=10, columnspan=5) # Span across all columns # Create buttons for each action - user_manual_button = ttk.Button(main_frame, text="User Manual", + user_manual_button = ttk.Button(main_frame, text=_("User Manual"), command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/blob/master/USERMANUAL.md")) - support_forum_button = ttk.Button(main_frame, text="Support Forum", + support_forum_button = ttk.Button(main_frame, text=_("Support Forum"), command=lambda: webbrowser_open( "http://discuss.ardupilot.org/t/new-ardupilot-methodic-configurator-gui/115038/1")) - report_bug_button = ttk.Button(main_frame, text="Report a Bug", + report_bug_button = ttk.Button(main_frame, text=_("Report a Bug"), command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/issues/new")) - licenses_button = ttk.Button(main_frame, text="Licenses", + licenses_button = ttk.Button(main_frame, text=_("Licenses"), command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator/blob/master/credits/CREDITS.md")) - source_button = ttk.Button(main_frame, text="Source Code", + source_button = ttk.Button(main_frame, text=_("Source Code"), command=lambda: webbrowser_open( "https://github.com/ArduPilot/MethodicConfigurator")) @@ -198,8 +200,8 @@ def __init__(self, current_file: str, flight_controller: FlightController, self.tempcal_imu_progress_window = None self.file_upload_progress_window = None - self.root.title("Amilcar Lucas's - ArduPilot methodic configurator " + VERSION + \ - " - Parameter file editor and uploader") + self.root.title(_("Amilcar Lucas's - ArduPilot methodic configurator ") + VERSION + \ + _(" - Parameter file editor and uploader")) self.root.geometry("990x550") # Set the window width # Bind the close_connection_and_quit function to the window close event @@ -248,17 +250,17 @@ def __create_conf_widgets(self, version: str): file_selection_frame.pack(side=tk.LEFT, fill="x", expand=False, padx=(6, 6)) # Create a label for the Combobox - file_selection_label = ttk.Label(file_selection_frame, text="Current intermediate parameter file:") + file_selection_label = ttk.Label(file_selection_frame, text=_("Current intermediate parameter file:")) file_selection_label.pack(side=tk.TOP, anchor=tk.NW) # Add the label to the top of the file_selection_frame # Create Combobox for intermediate parameter file selection self.file_selection_combobox = AutoResizeCombobox(file_selection_frame, list(self.local_filesystem.file_parameters.keys()), self.current_file, - "Select the intermediate parameter file from the list of available " - "files in the selected vehicle directory\nIt will automatically " + _("Select the intermediate parameter file from the list of available" + " files in the selected vehicle directory\nIt will automatically " "advance to the next file once the current file is uploaded to the " - "fight controller", + "fight controller"), state='readonly', width=45, style='readonly.TCombobox') self.file_selection_combobox.bind("<>", self.on_param_file_combobox_change) self.file_selection_combobox.pack(side=tk.TOP, anchor=tk.NW, pady=(4, 0)) @@ -268,31 +270,31 @@ def __create_conf_widgets(self, version: str): image_label = BaseWindow.put_image_in_label(config_frame, LocalFilesystem.application_logo_filepath()) image_label.pack(side=tk.RIGHT, anchor=tk.NE, padx=(4, 4), pady=(4, 0)) image_label.bind("", lambda event: show_about_window(self.main_frame, version)) - show_tooltip(image_label, "User Manual, Support Forum, Report a Bug, Licenses, Source Code") + show_tooltip(image_label, _("User Manual, Support Forum, Report a Bug, Licenses, Source Code")) def legend_frame(self, config_subframe: ttk.Frame, font_family: str): style = ttk.Style() style.configure('Legend.TLabelframe', font=(font_family, 9)) - legend_frame = ttk.LabelFrame(config_subframe, text="Legend", style='Legend.TLabelframe') + legend_frame = ttk.LabelFrame(config_subframe, text=_("Legend"), style='Legend.TLabelframe') legend_left = ttk.Frame(legend_frame) legend_left.pack(side=tk.LEFT, anchor=tk.NW) - show_tooltip(legend_frame, "the meaning of the text background colors") + show_tooltip(legend_frame, _("the meaning of the text background colors")) font_size = 8 font = (font_family, font_size) - np_label = ttk.Label(legend_left, text="Normal parameter", font=font) + np_label = ttk.Label(legend_left, text=_("Normal parameter"), font=font) np_label.pack(side=tk.TOP, anchor=tk.NW) - cal_label = ttk.Label(legend_left, text="Calibration param", background="yellow", font=font) + cal_label = ttk.Label(legend_left, text=_("Calibration param"), background="yellow", font=font) cal_label.pack(side=tk.TOP, anchor=tk.NW) - readonly_label = ttk.Label(legend_left, text="Read-only param", background="red", font=font) + readonly_label = ttk.Label(legend_left, text=_("Read-only param"), background="red", font=font) readonly_label.pack(side=tk.TOP, anchor=tk.NW) legend_right = ttk.Frame(legend_frame) legend_right.pack(side=tk.RIGHT, anchor=tk.NE) - default_label = ttk.Label(legend_right, text="Default value", background="lightblue", font=font) + default_label = ttk.Label(legend_right, text=_("Default value"), background="lightblue", font=font) default_label.pack(side=tk.TOP, anchor=tk.NW) - na_label = ttk.Label(legend_right, text="Not available", background="orange", font=font) + na_label = ttk.Label(legend_right, text=_("Not available"), background="orange", font=font) na_label.pack(side=tk.TOP, anchor=tk.NW) - ne_label = ttk.Label(legend_right, text="Not editable", font=font) + ne_label = ttk.Label(legend_right, text=_("Not editable"), font=font) ne_label.configure(state='disabled') ne_label.pack(side=tk.TOP, anchor=tk.NW) legend_frame.pack(side=tk.LEFT, fill="x", expand=False, padx=(2, 2)) @@ -315,40 +317,41 @@ def __create_parameter_area_widgets(self): checkboxes_frame.pack(side=tk.LEFT, padx=(8, 8)) # Create a checkbox for toggling parameter display - only_changed_checkbox = ttk.Checkbutton(checkboxes_frame, text="See only changed parameters", + only_changed_checkbox = ttk.Checkbutton(checkboxes_frame, text=_("See only changed parameters"), variable=self.show_only_differences, command=self.on_show_only_changed_checkbox_change) only_changed_checkbox.pack(side=tk.TOP, anchor=tk.NW) - show_tooltip(only_changed_checkbox, "Toggle to show only parameters that will change if/when uploaded to the flight " - "controller") + show_tooltip(only_changed_checkbox, _("Toggle to show only parameters that will change if/when uploaded to the flight " + "controller")) - annotate_params_checkbox = ttk.Checkbutton(checkboxes_frame, text="Annotate docs into .param files", + annotate_params_checkbox = ttk.Checkbutton(checkboxes_frame, text=_("Annotate docs into .param files"), state='normal' if self.local_filesystem.doc_dict else 'disabled', variable=self.annotate_params_into_files) annotate_params_checkbox.pack(side=tk.TOP, anchor=tk.NW) - show_tooltip(annotate_params_checkbox, "Annotate ArduPilot parameter documentation metadata into the intermediate " + show_tooltip(annotate_params_checkbox, _("Annotate ArduPilot parameter documentation metadata into the intermediate " "parameter files\n" - "The files will be bigger, but all the existing parameter documentation will be included inside") + "The files will be bigger, but all the existing parameter documentation will be included inside")) # Create upload button - upload_selected_button = ttk.Button(buttons_frame, text="Upload selected params to FC, and advance to next param file", + upload_selected_button = ttk.Button(buttons_frame, text=_("Upload selected params to FC, " + "and advance to next param file"), command=self.on_upload_selected_click) upload_selected_button.configure(state='normal' if self.flight_controller.master else 'disabled') upload_selected_button.pack(side=tk.LEFT, padx=(8, 8)) # Add padding on both sides of the upload selected button - show_tooltip(upload_selected_button, "Upload selected parameters to the flight controller and advance to the next " + show_tooltip(upload_selected_button, _("Upload selected parameters to the flight controller and advance to the next " "intermediate parameter file\nIf changes have been made to the current file it will ask if you want " - "to save them\nIt will reset the FC if necessary, re-download all parameters and validate their value") + "to save them\nIt will reset the FC if necessary, re-download all parameters and validate their value")) # Create skip button - skip_button = ttk.Button(buttons_frame, text="Skip parameter file", command=self.on_skip_click) + skip_button = ttk.Button(buttons_frame, text=_("Skip parameter file"), command=self.on_skip_click) skip_button.pack(side=tk.RIGHT, padx=(8, 8)) # Add right padding to the skip button - show_tooltip(skip_button, "Skip to the next intermediate parameter file without uploading any changes to the flight " - "controller\nIf changes have been made to the current file it will ask if you want to save them") + show_tooltip(skip_button, _("Skip to the next intermediate parameter file without uploading any changes to the flight " + "controller\nIf changes have been made to the current file it will ask if you want to save them")) @staticmethod def __please_read_the_docs(parent: tk.Tk): welcome_window = BaseWindow(parent) - welcome_window.root.title("Welcome to the ArduPilot Methodic Configurator") + welcome_window.root.title(_("Welcome to the ArduPilot Methodic Configurator")) welcome_window.root.geometry("690x170") style = ttk.Style() @@ -356,26 +359,27 @@ def __please_read_the_docs(parent: tk.Tk): instructions_text = RichText(welcome_window.main_frame, wrap=tk.WORD, height=5, bd=0, background=style.lookup("TLabel", "background")) instructions_text.pack(padx=10, pady=10) - instructions_text.insert(tk.END, "1. Read ") - instructions_text.insert(tk.END, "all", "bold") - instructions_text.insert(tk.END, " the documentation on top of the parameter table\n") - instructions_text.insert(tk.END, "2. Edit the parameter ") - instructions_text.insert(tk.END, "New Values", "italic") - instructions_text.insert(tk.END, " and", "bold") - instructions_text.insert(tk.END, " their ") - instructions_text.insert(tk.END, "Change Reason\n", "italic") - instructions_text.insert(tk.END, "3. Use ") - instructions_text.insert(tk.END, "Del", "italic") - instructions_text.insert(tk.END, " and ") - instructions_text.insert(tk.END, "Add", "italic") - instructions_text.insert(tk.END, " buttons to delete and add parameters if necessary\n") - instructions_text.insert(tk.END, "4. Press the ") - instructions_text.insert(tk.END, "Upload selected params to FC, and advance to next param file", "italic") - instructions_text.insert(tk.END, " button\n") - instructions_text.insert(tk.END, "5. Repeat from the top until the program automatically closes") + instructions_text.insert(tk.END, _("1. Read ")) + instructions_text.insert(tk.END, _("all"), "bold") + instructions_text.insert(tk.END, _(" the documentation on top of the parameter table\n")) + instructions_text.insert(tk.END, _("2. Edit the parameter ")) + instructions_text.insert(tk.END, _("New Values"), "italic") + instructions_text.insert(tk.END, _(" and"), "bold") + instructions_text.insert(tk.END, _(" their ")) + instructions_text.insert(tk.END, _("Change Reason\n"), "italic") + instructions_text.insert(tk.END, _("3. Use ")) + instructions_text.insert(tk.END, _("Del"), "italic") + instructions_text.insert(tk.END, _(" and ")) + instructions_text.insert(tk.END, _("Add"), "italic") + instructions_text.insert(tk.END, _(" buttons to delete and add parameters if necessary\n")) + instructions_text.insert(tk.END, _("4. Press the ")) + instructions_text.insert(tk.END, _("Upload selected params to FC, and advance to next param file"), "italic") + instructions_text.insert(tk.END, _(" button\n")) + instructions_text.insert(tk.END, _("5. Repeat from the top until the program automatically closes")) + instructions_text.config(state=tk.DISABLED) - dismiss_button = ttk.Button(welcome_window.main_frame, text="Dismiss", + dismiss_button = ttk.Button(welcome_window.main_frame, text=_("Dismiss"), command=lambda: ParameterEditorWindow.__close_instructions_window(welcome_window, parent)) dismiss_button.pack(pady=10) @@ -396,18 +400,18 @@ def __do_tempcal_imu(self, selected_file:str): tempcal_imu_result_param_filename, tempcal_imu_result_param_fullpath = \ self.local_filesystem.tempcal_imu_result_param_tuple() if selected_file == tempcal_imu_result_param_filename: - if messagebox.askyesno("IMU temperature calibration", - f"If you proceed the {tempcal_imu_result_param_filename}\n" + if messagebox.askyesno(_("IMU temperature calibration"), + _(f"If you proceed the {tempcal_imu_result_param_filename}\n" "will be overwritten with the new calibration results.\n" "Do you want to provide a .bin log file and\n" - "run the IMU temperature calibration using it?"): + "run the IMU temperature calibration using it?")): # file selection dialog to select the *.bin file to be used in the IMUfit temperature calibration - filename = filedialog.askopenfilename(filetypes=[("ArduPilot binary log files", ["*.bin", "*.BIN"])]) + filename = filedialog.askopenfilename(filetypes=[(_("ArduPilot binary log files", ["*.bin", "*.BIN"]))]) if filename: - messagebox.showwarning("IMU temperature calibration", "Please wait, this can take a really long time and\n" - "the GUI will be unresponsive until it finishes.") - self.tempcal_imu_progress_window = ProgressWindow(self.main_frame, "Reading IMU calibration messages", - "Please wait, this can take a long time") + messagebox.showwarning(_("IMU temperature calibration"), _("Please wait, this can take a really long time " + "and\nthe GUI will be unresponsive until it finishes.")) + self.tempcal_imu_progress_window = ProgressWindow(self.main_frame, _("Reading IMU calibration messages"), + _("Please wait, this can take a long time")) # Pass the selected filename to the IMUfit class IMUfit(filename, tempcal_imu_result_param_fullpath, False, False, False, False, self.local_filesystem.vehicle_dir, self.tempcal_imu_progress_window.update_progress_bar_300_pct) @@ -415,18 +419,18 @@ def __do_tempcal_imu(self, selected_file:str): try: self.local_filesystem.file_parameters = self.local_filesystem.read_params_from_files() except SystemExit as exp: - messagebox.showerror("Fatal error reading parameter files", f"{exp}") + messagebox.showerror(_("Fatal error reading parameter files"), f"{exp}") raise self.parameter_editor_table.set_at_least_one_param_edited(True) # force writing doc annotations to file def __should_copy_fc_values_to_file(self, selected_file: str): auto_changed_by = self.local_filesystem.auto_changed_by(selected_file) if auto_changed_by and self.flight_controller.fc_parameters: - if messagebox.askyesno("Update file with values from FC?", - "This configuration step should be performed outside this tool by\n" + if messagebox.askyesno(_("Update file with values from FC?"), + _("This configuration step should be performed outside this tool by\n" f"{auto_changed_by}\n" "and that should have changed the parameters on the FC.\n\n" - f"Should the FC values now be copied to the {selected_file} file?"): + f"Should the FC values now be copied to the {selected_file} file?")): relevant_fc_params = {key: value for key, value in self.flight_controller.fc_parameters.items() \ if key in self.local_filesystem.file_parameters[selected_file]} params_copied = self.local_filesystem.copy_fc_values_to_file(selected_file, relevant_fc_params) @@ -436,7 +440,7 @@ def __should_copy_fc_values_to_file(self, selected_file: str): def __should_jump_to_file(self, selected_file: str) -> str: jump_possible = self.local_filesystem.jump_possible(selected_file) for dest_file, msg in jump_possible.items(): - if messagebox.askyesno("Skip some steps?", msg): + if messagebox.askyesno(_("Skip some steps?"), msg): self.file_selection_combobox.set(dest_file) return dest_file return selected_file @@ -446,32 +450,32 @@ def __should_download_file_from_url(self, selected_file: str): if url and local_filename: if self.local_filesystem.vehicle_configuration_file_exists(local_filename): return # file already exists in the vehicle directory, no need to download it - if messagebox.askyesno("Download file from URL", - f"Should the {local_filename} file be downloaded from the URL\n{url}?"): + if messagebox.askyesno(_("Download file from URL"), + _(f"Should the {local_filename} file be downloaded from the URL\n{url}?")): if not self.local_filesystem.download_file_from_url(url, local_filename): - messagebox.showerror("Download failed", - f"Failed to download {local_filename} from {url}, please download it manually") + messagebox.showerror(_("Download failed"), + _(f"Failed to download {local_filename} from {url}, please download it manually")) def __should_upload_file_to_fc(self, selected_file: str): local_filename, remote_filename = self.local_filesystem.get_upload_local_and_remote_filenames(selected_file) if local_filename and remote_filename: if not self.local_filesystem.vehicle_configuration_file_exists(local_filename): - messagebox.showerror("Will not upload any file", f"Local file {local_filename} does not exist") + messagebox.showerror(_("Will not upload any file"), _(f"Local file {local_filename} does not exist")) return if self.flight_controller.master: - if messagebox.askyesno("Upload file to FC", - f"Should the {local_filename} file be uploaded to the flight controller as {remote_filename}?"): - self.file_upload_progress_window = ProgressWindow(self.main_frame, "Uploading file", - "Uploaded {} of {} %") + if messagebox.askyesno(_("Upload file to FC"), + _(f"Should the {local_filename} file be uploaded to the flight controller as {remote_filename}?")): + self.file_upload_progress_window = ProgressWindow(self.main_frame, _("Uploading file"), + _("Uploaded {} of {} %")) if not self.flight_controller.upload_file(local_filename, remote_filename, self.file_upload_progress_window.update_progress_bar): - messagebox.showerror("Upload failed", - f"Failed to upload {local_filename} to {remote_filename}," \ - " please upload it manually") + messagebox.showerror(_("Upload failed"), + _(f"Failed to upload {local_filename} to {remote_filename}," \ + " please upload it manually")) self.file_upload_progress_window.destroy() else: - logging_warning("No flight controller connection, will not upload any file") - messagebox.showwarning("Will not upload any file", "No flight controller connection") + logging_warning(_("No flight controller connection, will not upload any file")) + messagebox.showwarning(_("Will not upload any file"), _("No flight controller connection")) def on_param_file_combobox_change(self, _event, forced: bool = False): if not self.file_selection_combobox['values']: @@ -493,8 +497,9 @@ def on_param_file_combobox_change(self, _event, forced: bool = False): self.repopulate_parameter_table(selected_file) def download_flight_controller_parameters(self, redownload: bool = False): - self.param_download_progress_window = ProgressWindow(self.main_frame, ("Re-d" if redownload else "D") + \ - "ownloading FC parameters", "Downloaded {} of {} parameters") + operation_string = _("Re-downloading FC parameters") if redownload else _("Downloading FC parameters") + self.param_download_progress_window = ProgressWindow(self.main_frame, operation_string, + _("Downloaded {} of {} parameters")) # Download all parameters from the flight controller self.flight_controller.fc_parameters, param_default_values = self.flight_controller.download_params( self.param_download_progress_window.update_progress_bar) @@ -529,7 +534,7 @@ def upload_params_that_require_reset(self, selected_params: dict): # Write each selected parameter to the flight controller for param_name, param in selected_params.items(): try: - logging_info("Parameter %s set to %f", param_name, param.value) + logging_info(_("Parameter %s set to %f"), param_name, param.value) if param_name not in self.flight_controller.fc_parameters or \ not is_within_tolerance(self.flight_controller.fc_parameters[param_name], param.value): param_metadata = self.local_filesystem.doc_dict.get(param_name, None) @@ -537,24 +542,24 @@ def upload_params_that_require_reset(self, selected_params: dict): self.flight_controller.set_param(param_name, float(param.value)) self.at_least_one_changed_parameter_written = True if param_name in self.flight_controller.fc_parameters: - logging_info("Parameter %s changed from %f to %f, reset required", param_name, + logging_info(_("Parameter %s changed from %f to %f, reset required"), param_name, self.flight_controller.fc_parameters[param_name], param.value) else: - logging_info("Parameter %s changed to %f, reset required", param_name, param.value) + logging_info(_("Parameter %s changed to %f, reset required"), param_name, param.value) fc_reset_required = True # Check if any of the selected parameters have a _TYPE, _EN, or _ENABLE suffix elif param_name.endswith(('_TYPE', '_EN', '_ENABLE', 'SID_AXIS')): self.flight_controller.set_param(param_name, float(param.value)) self.at_least_one_changed_parameter_written = True if param_name in self.flight_controller.fc_parameters: - logging_info("Parameter %s changed from %f to %f, possible reset required", param_name, + logging_info(_("Parameter %s changed from %f to %f, possible reset required"), param_name, self.flight_controller.fc_parameters[param_name], param.value) else: - logging_info("Parameter %s changed to %f, possible reset required", param_name, param.value) + logging_info(_("Parameter %s changed to %f, possible reset required"), param_name, param.value) fc_reset_unsure.append(param_name) except ValueError as e: - logging_error("Failed to set parameter %s: %s", param_name, e) - messagebox.showerror("ArduPilot methodic configurator", f"Failed to set parameter {param_name}: {e}") + logging_error(_("Failed to set parameter %s: %s"), param_name, e) + messagebox.showerror(_("ArduPilot methodic configurator"), _(f"Failed to set parameter {param_name}: {e}")) self.__reset_and_reconnect(fc_reset_required, fc_reset_unsure) @@ -562,12 +567,13 @@ def __reset_and_reconnect(self, fc_reset_required, fc_reset_unsure): if not fc_reset_required: if fc_reset_unsure: # Ask the user if they want to reset the ArduPilot - fc_reset_required = messagebox.askyesno("Possible reset required", f"{(', ').join(fc_reset_unsure)} parameter" - "(s) potentially require a reset\nDo you want to reset the ArduPilot?") + fc_reset_required = messagebox.askyesno(_("Possible reset required"), + _(f"{(', ').join(fc_reset_unsure)} parameter(s) potentially require" + " a reset\nDo you want to reset the ArduPilot?")) if fc_reset_required: - self.reset_progress_window = ProgressWindow(self.main_frame, "Resetting Flight Controller", - "Waiting for {} of {} seconds") + self.reset_progress_window = ProgressWindow(self.main_frame, _("Resetting Flight Controller"), + _("Waiting for {} of {} seconds")) filesystem_boot_delay = self.local_filesystem.file_parameters[self.current_file].get('BRD_BOOT_DELAY', Par(0.0)) flightcontroller_boot_delay = self.flight_controller.fc_parameters.get('BRD_BOOT_DELAY', 0) extra_sleep_time = max(filesystem_boot_delay.value, flightcontroller_boot_delay) // 1000 + 1 # round up @@ -576,7 +582,7 @@ def __reset_and_reconnect(self, fc_reset_required, fc_reset_unsure): extra_sleep_time) if error_message: logging_error(error_message) - messagebox.showerror("ArduPilot methodic configurator", error_message) + messagebox.showerror(_("ArduPilot methodic configurator"), error_message) self.reset_progress_window.destroy() # for the case that we are doing a test and there is no real FC connected def on_upload_selected_click(self): @@ -588,17 +594,18 @@ def on_upload_selected_click(self): if hasattr(self.flight_controller, 'fc_parameters') and self.flight_controller.fc_parameters: self.upload_selected_params(selected_params) else: - logging_warning("No parameters were yet downloaded from the flight controller, will not upload any parameter") - messagebox.showwarning("Will not upload any parameter", "No flight controller connection") + logging_warning(_("No parameters were yet downloaded from the flight controller, " + "will not upload any parameter")) + messagebox.showwarning(_("Will not upload any parameter"), _("No flight controller connection")) else: - logging_warning("No parameter was selected for upload, will not upload any parameter") - messagebox.showwarning("Will not upload any parameter", "No parameter was selected for upload") + logging_warning(_("No parameter was selected for upload, will not upload any parameter")) + messagebox.showwarning(_("Will not upload any parameter"), _("No parameter was selected for upload")) # Delete the parameter table and create a new one with the next file if available self.on_skip_click(force_focus_out_event=False) # This function can recurse multiple times if there is an upload error def upload_selected_params(self, selected_params): - logging_info("Uploading %d selected %s parameters to flight controller...", len(selected_params), self.current_file) + logging_info(_("Uploading %d selected %s parameters to flight controller..."), len(selected_params), self.current_file) self.upload_params_that_require_reset(selected_params) @@ -606,18 +613,18 @@ def upload_selected_params(self, selected_params): for param_name, param in selected_params.items(): try: self.flight_controller.set_param(param_name, param.value) - logging_info("Parameter %s set to %f", param_name, param.value) + logging_info(_("Parameter %s set to %f"), param_name, param.value) if param_name not in self.flight_controller.fc_parameters or \ not is_within_tolerance(self.flight_controller.fc_parameters[param_name], param.value): self.at_least_one_changed_parameter_written = True except ValueError as e: - logging_error("Failed to set parameter %s: %s", param_name, e) - messagebox.showerror("ArduPilot methodic configurator", f"Failed to set parameter {param_name}: {e}") + logging_error(_("Failed to set parameter %s: %s"), param_name, e) + messagebox.showerror(_("ArduPilot methodic configurator"), _(f"Failed to set parameter {param_name}: {e}")) if self.at_least_one_changed_parameter_written: # Re-download all parameters, in case one of them changed, and validate that all uploads were successful self.download_flight_controller_parameters(True) - logging_info("Re-download all parameters from the flight controller") + logging_info(_("Re-download all parameters from the flight controller")) # Validate that the read parameters are the same as the ones in the current_file param_upload_error = [] @@ -625,21 +632,21 @@ def upload_selected_params(self, selected_params): if param_name in self.flight_controller.fc_parameters and \ param is not None and \ not is_within_tolerance(self.flight_controller.fc_parameters[param_name], float(param.value)): - logging_error("Parameter %s upload to the flight controller failed. Expected: %f, Actual: %f", + logging_error(_("Parameter %s upload to the flight controller failed. Expected: %f, Actual: %f"), param_name, param.value, self.flight_controller.fc_parameters[param_name]) param_upload_error.append(param_name) if param_name not in self.flight_controller.fc_parameters: - logging_error("Parameter %s upload to the flight controller failed. Expected: %f, Actual: N/A", + logging_error(_("Parameter %s upload to the flight controller failed. Expected: %f, Actual: N/A"), param_name, param.value) param_upload_error.append(param_name) if param_upload_error: - if messagebox.askretrycancel("Parameter upload error", - "Failed to upload the following parameters to the flight controller:\n" - f"{(', ').join(param_upload_error)}"): + if messagebox.askretrycancel(_("Parameter upload error"), + _("Failed to upload the following parameters to the flight controller:\n" + f"{(', ').join(param_upload_error)}")): self.upload_selected_params(selected_params) else: - logging_info("All parameters uploaded to the flight controller successfully") + logging_info(_("All parameters uploaded to the flight controller successfully")) self.local_filesystem.write_last_uploaded_filename(self.current_file) def on_skip_click(self, _event=None, force_focus_out_event=True): @@ -664,14 +671,14 @@ def on_skip_click(self, _event=None, force_focus_out_event=True): self.on_param_file_combobox_change(None) except ValueError: # If the current file is not found in the list, present a message box - messagebox.showerror("ArduPilot methodic configurator", "Current file not found in the list of files") + messagebox.showerror(_("ArduPilot methodic configurator"), _("Current file not found in the list of files")) # Close the application and the connection self.close_connection_and_quit() def write_changes_to_intermediate_parameter_file(self): if self.parameter_editor_table.get_at_least_one_param_edited(): - if messagebox.askyesno("One or more parameters have been edited", - f"Do you want to write the changes to the {self.current_file} file?"): + if messagebox.askyesno(_("One or more parameters have been edited"), + _(f"Do you want to write the changes to the {self.current_file} file?")): self.local_filesystem.export_to_param(self.local_filesystem.file_parameters[self.current_file], self.current_file, annotate_doc=self.annotate_params_into_files.get()) self.parameter_editor_table.set_at_least_one_param_edited(False) @@ -687,15 +694,15 @@ def write_summary_files(self): nr_unchanged_params = len(annotated_fc_parameters) - len(non_default__read_only_params) - \ len(non_default__writable_calibrations) - len(non_default__writable_non_calibrations) # If there are no more files, present a summary message box - summary_message = f"Methodic configuration of {len(annotated_fc_parameters)} parameters complete:\n\n" \ + summary_message = _(f"Methodic configuration of {len(annotated_fc_parameters)} parameters complete:\n\n" \ f"{nr_unchanged_params} kept their default value\n\n" \ f"{len(non_default__read_only_params)} non-default read-only parameters - " \ "ignore these, you can not change them\n\n" \ f"{len(non_default__writable_calibrations)} non-default writable sensor-calibrations - " \ "non-reusable between vehicles\n\n" \ f"{len(non_default__writable_non_calibrations)} non-default writable non-sensor-calibrations - " \ - "these can be reused between similar vehicles" - messagebox.showinfo("Last parameter file processed", summary_message) + "these can be reused between similar vehicles") + messagebox.showinfo(_("Last parameter file processed"), summary_message) wrote_complete = self.write_summary_file(annotated_fc_parameters, "complete.param", False) wrote_read_only = self.write_summary_file(non_default__read_only_params, @@ -715,24 +722,24 @@ def write_summary_file(self, param_dict: dict, filename: str, annotate_doc: bool should_write_file = True if param_dict: if self.local_filesystem.vehicle_configuration_file_exists(filename): - should_write_file = messagebox.askyesno("Overwrite existing file", - f"{filename} file already exists.\nDo you want to overwrite it?") + should_write_file = messagebox.askyesno(_("Overwrite existing file"), + _(f"{filename} file already exists.\nDo you want to overwrite it?")) if should_write_file: self.local_filesystem.export_to_param(param_dict, filename, annotate_doc) - logging_info("Summary file %s written", filename) + logging_info(_("Summary file %s written"), filename) return should_write_file def write_zip_file(self, files_to_zip: List[Tuple[bool, str]]): should_write_file = True zip_file_path = self.local_filesystem.zip_file_path() if self.local_filesystem.zip_file_exists(): - should_write_file = messagebox.askyesno("Overwrite existing file", - f"{zip_file_path} file already exists.\nDo you want to overwrite it?") + should_write_file = messagebox.askyesno(_("Overwrite existing file"), + _(f"{zip_file_path} file already exists.\nDo you want to overwrite it?")) if should_write_file: self.local_filesystem.zip_files(files_to_zip) - messagebox.showinfo("Parameter files zipped", "All relevant files have been zipped into the \n" + messagebox.showinfo(_("Parameter files zipped"), _("All relevant files have been zipped into the \n" f"{zip_file_path} file.\n\nYou can now upload this file to the ArduPilot Methodic\n" - "Configuration Blog post on discuss.ardupilot.org.") + "Configuration Blog post on discuss.ardupilot.org.")) return should_write_file def close_connection_and_quit(self): @@ -744,8 +751,8 @@ def close_connection_and_quit(self): def add_argparse_arguments(parser): parser.add_argument('--skip-welcome-popup', action='store_true', - help='Skip the welcome popup window. Only use this if you already know how to use the software. ' - 'Default to false') + help=_('Skip the welcome popup window. Only use this if you already know how to use the software. ' + 'Default to false')) return parser @@ -758,8 +765,8 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = ArgumentParser(description='A GUI for editing ArduPilot param files. ' - 'Not to be used directly, but through the main ArduPilot methodic configurator script.') + parser = ArgumentParser(description=_('A GUI for editing ArduPilot param files. ' + 'Not to be used directly, but through the main ArduPilot methodic configurator script.')) parser = FlightController.add_argparse_arguments(parser) parser = LocalFilesystem.add_argparse_arguments(parser) parser = ParameterEditorWindow.add_argparse_arguments(parser) diff --git a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py index 735e62ab..6742418c 100644 --- a/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py +++ b/MethodicConfigurator/frontend_tkinter_parameter_editor_table.py @@ -37,6 +37,8 @@ from MethodicConfigurator.frontend_tkinter_entry_dynamic import EntryWithDynamicalyFilteredListbox +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.annotate_params import Par @@ -70,7 +72,7 @@ def compute_forced_and_derived_parameters(self): for filename, file_info in self.local_filesystem.configuration_steps.items(): error_msg = self.local_filesystem.compute_parameters(filename, file_info, 'forced', self.variables) if error_msg: - messagebox.showerror("Error in forced parameters", error_msg) + messagebox.showerror(_("Error in forced parameters"), error_msg) else: self.add_forced_or_derived_parameters(filename, self.local_filesystem.forced_parameters) #error_msg = self.local_filesystem.compute_parameters(filename, file_info, 'derived', self.variables) @@ -93,14 +95,15 @@ def repopulate(self, selected_file: str, fc_parameters: dict, show_only_differen self.current_file = selected_file # Create labels for table headers - headers = ["-/+", "Parameter", "Current Value", "New Value", "Unit", "Upload", "Change Reason"] - tooltips = ["Delete or add a parameter", - "Parameter name must be ^[A-Z][A-Z_0-9]* and most 16 characters long", - "Current value on the flight controller ", - "New value from the above selected intermediate parameter file", - "Parameter Unit", - "When selected, upload the new value to the flight controller", - "Reason why respective parameter changed"] + headers = [_("-/+"), _("Parameter"), _("Current Value"), _("New Value"), _("Unit"), _("Upload"), _("Change Reason")] + tooltips = [_("Delete or add a parameter"), + _("Parameter name must be ^[A-Z][A-Z_0-9]* and most 16 characters long"), + _("Current value on the flight controller "), + _("New value from the above selected intermediate parameter file"), + _("Parameter Unit"), + _("When selected, upload the new value to the flight controller"), + _("Reason why respective parameter changed")] + for i, header in enumerate(headers): label = ttk.Label(self.view_port, text=header) label.grid(row=0, column=i, sticky="ew") # Use sticky="ew" to make the label stretch horizontally @@ -115,7 +118,7 @@ def repopulate(self, selected_file: str, fc_parameters: dict, show_only_differen self.local_filesystem.configuration_steps[selected_file], 'derived', self.variables) if error_msg: - messagebox.showerror("Error in derived parameters", error_msg) + messagebox.showerror(_("Error in derived parameters"), error_msg) else: # Add derived parameters not yet in the parameter list to the parameter list self.add_forced_or_derived_parameters(selected_file, self.local_filesystem.derived_parameters, fc_parameters) @@ -130,9 +133,9 @@ def repopulate(self, selected_file: str, fc_parameters: dict, show_only_differen not is_within_tolerance(fc_parameters[param_name], float(file_value.value)))} self.__update_table(different_params, fc_parameters) if not different_params: - logging_info("No different parameters found in %s. Skipping...", selected_file) - messagebox.showinfo("ArduPilot methodic configurator", - f"No different parameters found in {selected_file}. Skipping...") + logging_info(_("No different parameters found in %s. Skipping..."), selected_file) + messagebox.showinfo(_("ArduPilot methodic configurator"), + _(f"No different parameters found in {selected_file}. Skipping...")) self.parameter_editor.on_skip_click(force_focus_out_event=False) return else: @@ -145,7 +148,7 @@ def rename_fc_connection(self, selected_file): if "rename_connection" in self.local_filesystem.configuration_steps[selected_file]: new_connection_prefix = self.local_filesystem.configuration_steps[selected_file]["rename_connection"] new_connection_prefix = eval(str(new_connection_prefix), {}, self.variables) # pylint: disable=eval-used - for param_name, _ in self.local_filesystem.file_parameters[selected_file].items(): + for param_name, _param_value in self.local_filesystem.file_parameters[selected_file].items(): new_prefix = new_connection_prefix old_prefix = param_name.split("_")[0] @@ -164,13 +167,13 @@ def rename_fc_connection(self, selected_file): for old_name, new_name in renames.items(): if new_name in new_names: self.local_filesystem.file_parameters[selected_file].pop(old_name) - logging_info("Removing duplicate parameter %s", old_name) + logging_info(_("Removing duplicate parameter %s"), old_name) else: new_names.add(new_name) if new_name != old_name: self.local_filesystem.file_parameters[selected_file][new_name] = \ self.local_filesystem.file_parameters[selected_file].pop(old_name) - logging_info("Renaming parameter %s to %s", old_name, new_name) + logging_info(_("Renaming parameter %s to %s"), old_name, new_name) def __update_table(self, params, fc_parameters): try: @@ -178,7 +181,7 @@ def __update_table(self, params, fc_parameters): param_metadata = self.local_filesystem.doc_dict.get(param_name, None) param_default = self.local_filesystem.param_default_dict.get(param_name, None) doc_tooltip = param_metadata.get('doc_tooltip') if param_metadata else \ - "No documentation available in apm.pdef.xml for this parameter" + _("No documentation available in apm.pdef.xml for this parameter") column = [] column.append(self.__create_delete_button(param_name)) @@ -198,14 +201,14 @@ def __update_table(self, params, fc_parameters): column[6].grid(row=i, column=6, sticky="ew", padx=(0, 5)) # Add the "Add" button at the bottom of the table - add_button = ttk.Button(self.view_port, text="Add", style='narrow.TButton', + add_button = ttk.Button(self.view_port, text=_("Add"), style='narrow.TButton', command=lambda: self.__on_parameter_add(fc_parameters)) - show_tooltip(add_button, f"Add a parameter to the {self.current_file} file") + show_tooltip(add_button, _(f"Add a parameter to the {self.current_file} file")) add_button.grid(row=len(params)+2, column=0, sticky="w", padx=0) except KeyError as e: - logging_critical("Parameter %s not found in the %s file: %s", param_name, self.current_file, e, exc_info=True) + logging_critical(_("Parameter %s not found in the %s file: %s"), param_name, self.current_file, e, exc_info=True) sys_exit(1) # Configure the table_frame to stretch columns @@ -218,9 +221,9 @@ def __update_table(self, params, fc_parameters): self.view_port.columnconfigure(6, weight=1) # Change Reason def __create_delete_button(self, param_name): - delete_button = ttk.Button(self.view_port, text="Del", style='narrow.TButton', + delete_button = ttk.Button(self.view_port, text=_("Del"), style='narrow.TButton', command=lambda: self.__on_parameter_delete(param_name)) - show_tooltip(delete_button, f"Delete {param_name} from the {self.current_file} file") + show_tooltip(delete_button, _(f"Delete {param_name} from the {self.current_file} file")) return delete_button def __create_parameter_name(self, param_name, param_metadata, doc_tooltip): @@ -244,7 +247,7 @@ def __create_flightcontroller_value(self, fc_parameters, param_name, param_defau # Otherwise, set the background color to the default color flightcontroller_value = ttk.Label(self.view_port, text=value_str) else: - flightcontroller_value = ttk.Label(self.view_port, text="N/A", background="orange") + flightcontroller_value = ttk.Label(self.view_port, text=_("N/A"), background="orange") if doc_tooltip: show_tooltip(flightcontroller_value, doc_tooltip) return flightcontroller_value @@ -255,7 +258,7 @@ def __update_combobox_style_on_selection(self, combobox_widget, param_default): has_default_value = param_default is not None and is_within_tolerance(current_value, param_default.value) combobox_widget.configure(style='default_v.TCombobox' if has_default_value else 'readonly.TCombobox') except ValueError: - logging_info(f'Could not solve the selected {combobox_widget} key to a float value.') + logging_info(_(f'Could not solve the selected {combobox_widget} key to a float value.')) @staticmethod def __update_new_value_entry_text(new_value_entry: ttk.Entry, value: float, param_default): @@ -311,7 +314,7 @@ def __create_new_value_entry(self, param_name, param, # pylint: disable=too-man try: old_value = self.local_filesystem.file_parameters[self.current_file][param_name].value except KeyError as e: - logging_critical("Parameter %s not found in the %s file: %s", param_name, self.current_file, e, exc_info=True) + logging_critical(_("Parameter %s not found in the %s file: %s"), param_name, self.current_file, e, exc_info=True) sys_exit(1) if present_as_forced: new_value_entry.config(state='disabled', background='light grey') @@ -355,7 +358,7 @@ def update_label(): # Temporarily unbind the FocusIn event to prevent triggering the window again event.widget.unbind("") window = tk.Toplevel(self.root) - window.title(f"Select {param_name} Bitmask Options") + window.title(_(f"Select {param_name} Bitmask Options")) checkbox_vars = {} main_frame = ttk.Frame(window) @@ -391,7 +394,7 @@ def update_label(): def __create_unit_label(self, param_metadata): unit_label = ttk.Label(self.view_port, text=param_metadata.get('unit') if param_metadata else "") unit_tooltip = param_metadata.get('unit_tooltip') if param_metadata else \ - "No documentation available in apm.pdef.xml for this parameter" + _("No documentation available in apm.pdef.xml for this parameter") if unit_tooltip: show_tooltip(unit_label, unit_tooltip) return unit_label @@ -400,7 +403,7 @@ def __create_upload_checkbutton(self, param_name, fc_connected): self.upload_checkbutton_var[param_name] = tk.BooleanVar(value=fc_connected) upload_checkbutton = ttk.Checkbutton(self.view_port, variable=self.upload_checkbutton_var[param_name]) upload_checkbutton.configure(state='normal' if fc_connected else 'disabled') - show_tooltip(upload_checkbutton, f'When selected upload {param_name} new value to the flight controller') + show_tooltip(upload_checkbutton, _(f'When selected upload {param_name} new value to the flight controller')) return upload_checkbutton def __create_change_reason_entry(self, param_name, param, new_value_entry): @@ -426,22 +429,22 @@ def __create_change_reason_entry(self, param_name, param, new_value_entry): else: change_reason_entry.bind("", lambda event, current_file=self.current_file, param_name=param_name: self.__on_parameter_change_reason_change(event, current_file, param_name)) - show_tooltip(change_reason_entry, f'Reason why {param_name} should change to {new_value_entry.get()}') + show_tooltip(change_reason_entry, _(f'Reason why {param_name} should change to {new_value_entry.get()}')) return change_reason_entry def __on_parameter_delete(self, param_name): - if messagebox.askyesno(f"{self.current_file}", f"Are you sure you want to delete the {param_name} parameter?"): + if messagebox.askyesno(f"{self.current_file}", _(f"Are you sure you want to delete the {param_name} parameter?")): del self.local_filesystem.file_parameters[self.current_file][param_name] self.at_least_one_param_edited = True self.parameter_editor.repopulate_parameter_table(self.current_file) def __on_parameter_add(self, fc_parameters): add_parameter_window = BaseWindow(self.root) - add_parameter_window.root.title("Add Parameter to " + self.current_file) + add_parameter_window.root.title(_("Add Parameter to ") + self.current_file) add_parameter_window.root.geometry("450x300") # Label for instruction - instruction_label = ttk.Label(add_parameter_window.main_frame, text="Enter the parameter name to add:") + instruction_label = ttk.Label(add_parameter_window.main_frame, text=_(_("Enter the parameter name to add:"))) instruction_label.pack(pady=5) if self.local_filesystem.doc_dict: @@ -450,8 +453,8 @@ def __on_parameter_add(self, fc_parameters): param_dict = fc_parameters if not param_dict: - messagebox.showerror("Operation not possible", - "No apm.pdef.xml file and no FC connected. Not possible autocomplete parameter names.") + messagebox.showerror(_("Operation not possible"), + _("No apm.pdef.xml file and no FC connected. Not possible autocomplete parameter names.")) return # Remove the parameters that are already displayed in this configuration step @@ -482,10 +485,10 @@ def custom_selection_handler(event): def __confirm_parameter_addition(self, param_name: str, fc_parameters: dict) -> bool: if not param_name: - messagebox.showerror("Invalid parameter name.", "Parameter name can not be empty.") + messagebox.showerror(_("Invalid parameter name."), _("Parameter name can not be empty.")) return False if param_name in self.local_filesystem.file_parameters[self.current_file]: - messagebox.showerror("Invalid parameter name.", "Parameter already exists, edit it instead") + messagebox.showerror(_("Invalid parameter name."), _("Parameter already exists, edit it instead")) return False if fc_parameters: if param_name in fc_parameters: @@ -493,7 +496,7 @@ def __confirm_parameter_addition(self, param_name: str, fc_parameters: dict) -> self.at_least_one_param_edited = True self.parameter_editor.repopulate_parameter_table(self.current_file) return True - messagebox.showerror("Invalid parameter name.", "Parameter name not found in the flight controller.") + messagebox.showerror(_("Invalid parameter name."), _("Parameter name not found in the flight controller.")) elif self.local_filesystem.doc_dict: if param_name in self.local_filesystem.doc_dict: self.local_filesystem.file_parameters[self.current_file][param_name] = Par( \ @@ -501,10 +504,10 @@ def __confirm_parameter_addition(self, param_name: str, fc_parameters: dict) -> self.at_least_one_param_edited = True self.parameter_editor.repopulate_parameter_table(self.current_file) return True - messagebox.showerror("Invalid parameter name.", f"'{param_name}' not found in the apm.pdef.xml file.", ) + messagebox.showerror(_("Invalid parameter name."), _(f"'{param_name}' not found in the apm.pdef.xml file."), ) else: - messagebox.showerror("Operation not possible", - "Can not add parameter when no FC is connected and no apm.pdef.xml file exists.") + messagebox.showerror(_("Operation not possible"), + _("Can not add parameter when no FC is connected and no apm.pdef.xml file exists.")) return False @@ -517,7 +520,7 @@ def __on_parameter_value_change(self, event, current_file, param_name): try: old_value = self.local_filesystem.file_parameters[current_file][param_name].value except KeyError as e: - logging_critical("Parameter %s not found in the %s file: %s", param_name, current_file, e, exc_info=True) + logging_critical(_("Parameter %s not found in the %s file: %s"), param_name, current_file, e, exc_info=True) sys_exit(1) valid = True # Check if the input is a valid float @@ -529,22 +532,22 @@ def __on_parameter_value_change(self, event, current_file, param_name): p_max = param_metadata.get('max', None) if param_metadata else None if changed: if p_min and p < p_min: - if not messagebox.askyesno("Out-of-bounds Value", - f"The value for {param_name} {p} should be greater than {p_min}\n" - "Use out-of-bounds value?", icon='warning'): + if not messagebox.askyesno(_("Out-of-bounds Value"), + _(f"The value for {param_name} {p} should be greater than {p_min}\n" + "Use out-of-bounds value?"), icon='warning'): valid = False if p_max and p > p_max: - if not messagebox.askyesno("Out-of-bounds Value", - f"The value for {param_name} {p} should be smaller than {p_max}\n" - "Use out-of-bounds value?", icon='warning'): + if not messagebox.askyesno(_("Out-of-bounds Value"), + _(f"The value for {param_name} {p} should be smaller than {p_max}\n" + "Use out-of-bounds value?"), icon='warning'): valid = False except ValueError: # Optionally, you can handle the invalid value here, for example, by showing an error message - messagebox.showerror("Invalid Value", f"The value for {param_name} must be a valid float.") + messagebox.showerror(_("Invalid Value"), _(f"The value for {param_name} must be a valid float.")) valid = False if valid: if changed and not self.at_least_one_param_edited: - logging_debug("Parameter %s changed, will later ask if change(s) should be saved to file.", param_name) + logging_debug(_("Parameter %s changed, will later ask if change(s) should be saved to file."), param_name) self.at_least_one_param_edited = changed or self.at_least_one_param_edited # Update the params dictionary with the new value self.local_filesystem.file_parameters[current_file][param_name].value = p @@ -560,12 +563,12 @@ def __on_parameter_change_reason_change(self, event, current_file, param_name): changed = new_value != self.local_filesystem.file_parameters[current_file][param_name].comment and \ not (new_value == "" and self.local_filesystem.file_parameters[current_file][param_name].comment is None) except KeyError as e: - logging_critical("Parameter %s not found in the %s file %s: %s", param_name, current_file, + logging_critical(_("Parameter %s not found in the %s file %s: %s"), param_name, current_file, new_value, e, exc_info=True) sys_exit(1) if changed and not self.at_least_one_param_edited: - logging_debug("Parameter %s change reason changed from %s to %s, will later ask if change(s) should be saved to " - "file.", + logging_debug(_("Parameter %s change reason changed from %s to %s, will later ask if change(s) should be saved to " + "file."), param_name, self.local_filesystem.file_parameters[current_file][param_name].comment, new_value) self.at_least_one_param_edited = changed or self.at_least_one_param_edited # Update the params dictionary with the new value diff --git a/MethodicConfigurator/frontend_tkinter_template_overview.py b/MethodicConfigurator/frontend_tkinter_template_overview.py index 97ee3b6d..4a2d9bd2 100644 --- a/MethodicConfigurator/frontend_tkinter_template_overview.py +++ b/MethodicConfigurator/frontend_tkinter_template_overview.py @@ -24,6 +24,8 @@ from MethodicConfigurator.frontend_tkinter_base import BaseWindow +from MethodicConfigurator.internationalization import _ + from MethodicConfigurator.version import VERSION class TemplateOverviewWindow(BaseWindow): @@ -44,10 +46,10 @@ class TemplateOverviewWindow(BaseWindow): """ def __init__(self, parent: tk.Tk): super().__init__(parent) - self.root.title(f"Amilcar Lucas's - ArduPilot methodic configurator {VERSION} - Template Overview and selection") + self.root.title(_(f"Amilcar Lucas's - ArduPilot methodic configurator {VERSION} - Template Overview and selection")) self.root.geometry("1200x300") - instruction_text = "Please double-click the template below that most resembles your own vehicle components" + instruction_text = _("Please double-click the template below that most resembles your own vehicle components") instruction_label = ttk.Label(self.main_frame, text=instruction_text, font=('Arial', 12)) instruction_label.pack(pady=(10, 20)) @@ -151,12 +153,12 @@ def argument_parser(): Returns: argparse.Namespace: An object containing the parsed arguments. """ - parser = argparse.ArgumentParser(description='ArduPilot methodic configurator is a GUI-based tool designed to simplify ' + parser = argparse.ArgumentParser(description=_('ArduPilot methodic configurator is a GUI-based tool designed to simplify ' 'the management and visualization of ArduPilot parameters. It enables users ' 'to browse through various vehicle templates, edit parameter files, and ' 'apply changes directly to the flight controller. The tool is built to ' 'semi-automate the configuration process of ArduPilot for drones by ' - 'providing a clear and intuitive interface for parameter management.') + 'providing a clear and intuitive interface for parameter management.')) return add_common_arguments_and_parse(parser) diff --git a/MethodicConfigurator/internationalization.py b/MethodicConfigurator/internationalization.py new file mode 100644 index 00000000..d87cd0eb --- /dev/null +++ b/MethodicConfigurator/internationalization.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +''' +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3.0-or-later +''' + +import gettext + + +# Setup language +locale_path = 'locale' # directory of locale file +language = 'zh_CN' # select language + +# create translation +translation = gettext.translation('messages', localedir=locale_path, languages=[language], fallback=True) + +# set translation object as _() +_ = translation.gettext