diff --git a/.gitignore b/.gitignore index a9d4c876..6226875f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ src/oled_monitor/current src/oled_monitor/rascsi_interface_pb2.py src/raspberrypi/hfdisk/ *~ +messages.pot +messages.mo diff --git a/src/web/README.md b/src/web/README.md index 8a7a79f3..b5c61325 100644 --- a/src/web/README.md +++ b/src/web/README.md @@ -46,3 +46,15 @@ $ cd ~/source/RASCSI $ git remote add pi ssh://pi@rascsi/home/pi/dev.git $ git push pi master ``` + +## Localizing the Web Interface + +We use the Flask-Babel library and Flask/Jinja2 extension for i18n. + +To create a new localization, it needs to be added to accept_languages in +the get_locale() method, and also to localizer.cpp in the RaSCSI C++ code. + +Once this is done, follow the steps in the [Flask-Babel documentation](https://flask-babel.tkte.ch/#translating-applications) +to generate the messages.po for the new language. + +Updating an existing messages.po is also covered above. diff --git a/src/web/babel.cfg b/src/web/babel.cfg new file mode 100644 index 00000000..f0234b32 --- /dev/null +++ b/src/web/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/src/web/file_cmds.py b/src/web/file_cmds.py index fb1469a0..84791959 100644 --- a/src/web/file_cmds.py +++ b/src/web/file_cmds.py @@ -6,6 +6,7 @@ import os import logging from pathlib import PurePath from flask import current_app +from flask_babel import _ from ractl_cmds import ( get_server_info, @@ -65,6 +66,7 @@ def list_images(): command = proto.PbCommand() command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -121,6 +123,7 @@ def create_new_image(file_name, file_type, size): command = proto.PbCommand() command.operation = proto.PbOperation.CREATE_IMAGE command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] command.params["file"] = file_name + "." + file_type command.params["size"] = str(size) @@ -141,6 +144,7 @@ def delete_image(file_name): command = proto.PbCommand() command.operation = proto.PbOperation.DELETE_IMAGE command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] command.params["file"] = file_name @@ -159,6 +163,7 @@ def rename_image(file_name, new_file_name): command = proto.PbCommand() command.operation = proto.PbOperation.RENAME_IMAGE command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] command.params["from"] = file_name command.params["to"] = new_file_name @@ -176,8 +181,14 @@ def delete_file(file_path): """ if os.path.exists(file_path): os.remove(file_path) - return {"status": True, "msg": f"File deleted: {file_path}"} - return {"status": False, "msg": f"File to delete not found: {file_path}"} + return { + "status": True, + "msg": _(u"File deleted: %(file_path)s", file_path=file_path), + } + return { + "status": False, + "msg": _(u"File to delete not found: %(file_path)s", file_path=file_path), + } def rename_file(file_path, target_path): @@ -187,8 +198,14 @@ def rename_file(file_path, target_path): """ if os.path.exists(PurePath(target_path).parent): os.rename(file_path, target_path) - return {"status": True, "msg": f"File moved to: {target_path}"} - return {"status": False, "msg": f"Unable to move to: {target_path}"} + return { + "status": True, + "msg": _(u"File moved to: %(target_path)s", target_path=target_path), + } + return { + "status": False, + "msg": _(u"Unable to move file to: %(target_path)s", target_path=target_path), + } def unzip_file(file_name, member=False, members=False): @@ -303,7 +320,7 @@ def download_file_to_iso(url, *iso_args): return { "status": True, - "msg": f"Created CD-ROM ISO image with arguments \"" + " ".join(iso_args) + "\"", + "msg": _(u"Created CD-ROM ISO image with arguments \"%(value)s\"", value=" ".join(iso_args)), "file_name": iso_filename, } @@ -331,7 +348,14 @@ def download_to_dir(url, save_dir): logging.info("Response content-type: %s", req.headers["content-type"]) logging.info("Response status code: %s", req.status_code) - return {"status": True, "msg": f"{file_name} downloaded to {save_dir}"} + return { + "status": True, + "msg": _( + u"%(file_name)s downloaded to %(save_dir)s", + file_name=file_name, + save_dir=save_dir, + ), + } def write_config(file_name): @@ -369,7 +393,7 @@ def write_config(file_name): json_file, indent=4 ) - return {"status": True, "msg": f"Saved config to {file_name}"} + return {"status": True, "msg": _(u"Saved configuration file to %(file_name)s", file_name=file_name)} except (IOError, ValueError, EOFError, TypeError) as error: logging.error(str(error)) delete_file(file_name) @@ -377,7 +401,10 @@ def write_config(file_name): except: logging.error("Could not write to file: %s", file_name) delete_file(file_name) - return {"status": False, "msg": f"Could not write to file: {file_name}"} + return { + "status": False, + "msg": _(u"Could not write to file: %(file_name)s", file_name=file_name), + } def read_config(file_name): @@ -434,14 +461,20 @@ def read_config(file_name): kwargs[param] = params[param] attach_image(row["id"], **kwargs) else: - return {"status": False, "msg": "Invalid config file format."} - return {"status": True, "msg": f"Loaded config from: {file_name}"} + return {"status": False, "msg": _(u"Invalid configuration file format")} + return { + "status": True, + "msg": _(u"Loaded configurations from: %(file_name)s", file_name=file_name), + } except (IOError, ValueError, EOFError, TypeError) as error: logging.error(str(error)) return {"status": False, "msg": str(error)} except: logging.error("Could not read file: %s", file_name) - return {"status": False, "msg": f"Could not read file: {file_name}"} + return { + "status": False, + "msg": _(u"Could not read configuration file: %(file_name)s", file_name=file_name), + } def write_drive_properties(file_name, conf): @@ -455,7 +488,10 @@ def write_drive_properties(file_name, conf): try: with open(file_path, "w") as json_file: dump(conf, json_file, indent=4) - return {"status": True, "msg": f"Created file: {file_path}"} + return { + "status": True, + "msg": _(u"Created properties file: %(file_path)s", file_path=file_path), + } except (IOError, ValueError, EOFError, TypeError) as error: logging.error(str(error)) delete_file(file_path) @@ -463,23 +499,33 @@ def write_drive_properties(file_name, conf): except: logging.error("Could not write to file: %s", file_path) delete_file(file_path) - return {"status": False, "msg": f"Could not write to file: {file_path}"} + return { + "status": False, + "msg": _(u"Could not write to properties file: %(file_path)s", file_path=file_path), + } -def read_drive_properties(path_name): +def read_drive_properties(file_path): """ Reads drive properties from json formatted file. - Takes (str) path_name as argument. + Takes (str) file_path as argument. Returns (dict) with (bool) status, (str) msg, (dict) conf """ from json import load try: - with open(path_name) as json_file: + with open(file_path) as json_file: conf = load(json_file) - return {"status": True, "msg": f"Read from file: {path_name}", "conf": conf} + return { + "status": True, + "msg": _(u"Read properties from file: %(file_path)s", file_path=file_path), + "conf": conf, + } except (IOError, ValueError, EOFError, TypeError) as error: logging.error(str(error)) return {"status": False, "msg": str(error)} except: - logging.error("Could not read file: %s", path_name) - return {"status": False, "msg": f"Could not read file: {path_name}"} + logging.error("Could not read file: %s", file_path) + return { + "status": False, + "msg": _(u"Could not read properties from file: %(file_path)s", file_path=file_path), + } diff --git a/src/web/pi_cmds.py b/src/web/pi_cmds.py index 5e44e8a5..da9718e2 100644 --- a/src/web/pi_cmds.py +++ b/src/web/pi_cmds.py @@ -5,6 +5,7 @@ Module for methods controlling and getting information about the Pi's Linux syst import subprocess import asyncio import logging +from flask_babel import _ from settings import AUTH_GROUP @@ -175,6 +176,6 @@ def auth_active(): if AUTH_GROUP in groups: return { "status": True, - "msg": "You must log in to use this function!", + "msg": _(u"You must log in to use this function"), } return {"status": False, "msg": ""} diff --git a/src/web/ractl_cmds.py b/src/web/ractl_cmds.py index 0379d9a7..6ed3b060 100644 --- a/src/web/ractl_cmds.py +++ b/src/web/ractl_cmds.py @@ -5,6 +5,7 @@ Module for commands sent to the RaSCSI backend service. from settings import REMOVABLE_DEVICE_TYPES from socket_cmds import send_pb_command from flask import current_app +from flask_babel import _ import rascsi_interface_pb2 as proto @@ -24,6 +25,7 @@ def get_server_info(): command = proto.PbCommand() command.operation = proto.PbOperation.SERVER_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -82,6 +84,7 @@ def get_reserved_ids(): command = proto.PbCommand() command.operation = proto.PbOperation.RESERVED_IDS_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -103,6 +106,7 @@ def get_network_info(): command = proto.PbCommand() command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -121,6 +125,7 @@ def get_device_types(): command = proto.PbCommand() command.operation = proto.PbOperation.DEVICE_TYPES_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -142,6 +147,8 @@ def get_image_files_info(): """ command = proto.PbCommand() command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO + command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -170,6 +177,7 @@ def attach_image(scsi_id, **kwargs): """ command = proto.PbCommand() command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] devices = proto.PbDeviceDefinition() devices.id = int(scsi_id) @@ -195,8 +203,12 @@ def attach_image(scsi_id, **kwargs): if current_type != device_type: return { "status": False, - "msg": "Cannot insert an image for " + device_type + \ - " into a " + current_type + " device." + "msg": _( + u"Cannot insert an image for %(device_type)s into a " + u"%(current_device_type)s device", + device_type=device_type, + current_device_type=current_type + ), } command.operation = proto.PbOperation.INSERT # Handling attaching a new device @@ -241,6 +253,7 @@ def detach_by_id(scsi_id, unit=None): command.operation = proto.PbOperation.DETACH command.devices.append(devices) command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -256,6 +269,7 @@ def detach_all(): command = proto.PbCommand() command.operation = proto.PbOperation.DETACH_ALL command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -278,6 +292,7 @@ def eject_by_id(scsi_id, unit=None): command.operation = proto.PbOperation.EJECT command.devices.append(devices) command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -297,6 +312,7 @@ def list_devices(scsi_id=None, unit=None): command = proto.PbCommand() command.operation = proto.PbOperation.DEVICES_INFO command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] # If method is called with scsi_id parameter, return the info on those devices # Otherwise, return the info on all attached devices @@ -374,6 +390,7 @@ def reserve_scsi_ids(reserved_scsi_ids): command.operation = proto.PbOperation.RESERVE_IDS command.params["ids"] = ",".join(reserved_scsi_ids) command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -391,6 +408,7 @@ def set_log_level(log_level): command.operation = proto.PbOperation.LOG_LEVEL command.params["level"] = str(log_level) command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -408,6 +426,7 @@ def shutdown_pi(mode): command.operation = proto.PbOperation.SHUT_DOWN command.params["mode"] = str(mode) command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() @@ -424,6 +443,7 @@ def is_token_auth(): command = proto.PbCommand() command.operation = proto.PbOperation.CHECK_AUTHENTICATION command.params["token"] = current_app.config["TOKEN"] + command.params["locale"] = current_app.config["LOCALE"] data = send_pb_command(command.SerializeToString()) result = proto.PbResult() diff --git a/src/web/requirements.txt b/src/web/requirements.txt index 9ee82b36..c9ced2c3 100644 --- a/src/web/requirements.txt +++ b/src/web/requirements.txt @@ -7,3 +7,4 @@ MarkupSafe==2.0.1 protobuf==3.17.3 requests==2.26.0 simplepam==0.1.5 +flask_babel==2.0.0 diff --git a/src/web/socket_cmds.py b/src/web/socket_cmds.py index ba540e76..710f1394 100644 --- a/src/web/socket_cmds.py +++ b/src/web/socket_cmds.py @@ -4,6 +4,7 @@ Module for sending and receiving data over a socket connection with the RaSCSI b import logging from flask import abort +from flask_babel import _ from time import sleep def send_pb_command(payload): @@ -35,9 +36,12 @@ def send_pb_command(payload): logging.error(error_msg) # After failing all attempts, throw a 404 error - abort(404, "The RaSCSI Web Interface failed to connect to RaSCSI at " + str(host) + \ - ":" + str(port) + " with error: " + error_msg + \ - ". The RaSCSI service is not running or may have crashed.") + abort(404, _( + u"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s " + u"with error: %(error_msg)s. The RaSCSI process is not running or may have crashed.", + host=host, port=port, error_msg=error_msg, + ) + ) def send_over_socket(sock, payload): @@ -72,9 +76,11 @@ def send_over_socket(sock, payload): "RaSCSI may have crashed." ) abort( - 503, "The RaSCSI Web Interface lost connection to RaSCSI. " - "Please go back and try again. " - "If the issue persists, please report a bug." + 503, _( + u"The RaSCSI Web Interface lost connection to RaSCSI. " + u"Please go back and try again. " + u"If the issue persists, please report a bug." + ) ) chunks.append(chunk) bytes_recvd = bytes_recvd + len(chunk) @@ -86,8 +92,9 @@ def send_over_socket(sock, payload): "RaSCSI may have crashed." ) abort( - 500, - "The RaSCSI Web Interface did not get a valid response from RaSCSI. " - "Please go back and try again. " - "If the issue persists, please report a bug." + 500, _( + u"The RaSCSI Web Interface did not get a valid response from RaSCSI. " + u"Please go back and try again. " + u"If the issue persists, please report a bug." + ) ) diff --git a/src/web/start.sh b/src/web/start.sh index 3cba5077..4a6e3d36 100755 --- a/src/web/start.sh +++ b/src/web/start.sh @@ -74,6 +74,8 @@ else fi set -e +pybabel compile -d translations + # parse arguments while [ "$1" != "" ]; do PARAM=$(echo "$1" | awk -F= '{print $1}') diff --git a/src/web/templates/base.html b/src/web/templates/base.html index 0a6c2765..5465227a 100644 --- a/src/web/templates/base.html +++ b/src/web/templates/base.html @@ -26,12 +26,12 @@ @@ -45,19 +45,19 @@