Web Interface i18n (#564)

* Add flask_babel 2.0.0 to requirements

* Partial i18n

* Use current locale for protobuf requests

* Don't store generated messages.pot in git

* Internationalize all python code

* Formatting fixes

* Partial internationalization of html

* Iterate on html i18n

* Completed i18n of code

* Improve i18n of strings

* Blurb about i18n in the readme

* Improve i18n strings

* Add the compiled messages.mo files to .gitignore

* Add complete Swedish localization

* Generate localizations in start.sh

* Only compile messages.mo in start.sh; better sequence

* Fix bug
This commit is contained in:
Daniel Markstedt 2021-12-26 13:36:12 -08:00 committed by GitHub
parent c19c814863
commit ab82d6e4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1514 additions and 268 deletions

2
.gitignore vendored
View File

@ -11,3 +11,5 @@ src/oled_monitor/current
src/oled_monitor/rascsi_interface_pb2.py src/oled_monitor/rascsi_interface_pb2.py
src/raspberrypi/hfdisk/ src/raspberrypi/hfdisk/
*~ *~
messages.pot
messages.mo

View File

@ -46,3 +46,15 @@ $ cd ~/source/RASCSI
$ git remote add pi ssh://pi@rascsi/home/pi/dev.git $ git remote add pi ssh://pi@rascsi/home/pi/dev.git
$ git push pi master $ 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.

3
src/web/babel.cfg Normal file
View File

@ -0,0 +1,3 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@ -6,6 +6,7 @@ import os
import logging import logging
from pathlib import PurePath from pathlib import PurePath
from flask import current_app from flask import current_app
from flask_babel import _
from ractl_cmds import ( from ractl_cmds import (
get_server_info, get_server_info,
@ -65,6 +66,7 @@ def list_images():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -121,6 +123,7 @@ def create_new_image(file_name, file_type, size):
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE command.operation = proto.PbOperation.CREATE_IMAGE
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
command.params["file"] = file_name + "." + file_type command.params["file"] = file_name + "." + file_type
command.params["size"] = str(size) command.params["size"] = str(size)
@ -141,6 +144,7 @@ def delete_image(file_name):
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE command.operation = proto.PbOperation.DELETE_IMAGE
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
command.params["file"] = file_name command.params["file"] = file_name
@ -159,6 +163,7 @@ def rename_image(file_name, new_file_name):
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.RENAME_IMAGE command.operation = proto.PbOperation.RENAME_IMAGE
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
command.params["from"] = file_name command.params["from"] = file_name
command.params["to"] = new_file_name command.params["to"] = new_file_name
@ -176,8 +181,14 @@ def delete_file(file_path):
""" """
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) os.remove(file_path)
return {"status": True, "msg": f"File deleted: {file_path}"} return {
return {"status": False, "msg": f"File to delete not found: {file_path}"} "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): 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): if os.path.exists(PurePath(target_path).parent):
os.rename(file_path, target_path) os.rename(file_path, target_path)
return {"status": True, "msg": f"File moved to: {target_path}"} return {
return {"status": False, "msg": f"Unable to move to: {target_path}"} "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): def unzip_file(file_name, member=False, members=False):
@ -303,7 +320,7 @@ def download_file_to_iso(url, *iso_args):
return { return {
"status": True, "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, "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 content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code) 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): def write_config(file_name):
@ -369,7 +393,7 @@ def write_config(file_name):
json_file, json_file,
indent=4 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: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
delete_file(file_name) delete_file(file_name)
@ -377,7 +401,10 @@ def write_config(file_name):
except: except:
logging.error("Could not write to file: %s", file_name) logging.error("Could not write to file: %s", file_name)
delete_file(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): def read_config(file_name):
@ -434,14 +461,20 @@ def read_config(file_name):
kwargs[param] = params[param] kwargs[param] = params[param]
attach_image(row["id"], **kwargs) attach_image(row["id"], **kwargs)
else: else:
return {"status": False, "msg": "Invalid config file format."} return {"status": False, "msg": _(u"Invalid configuration file format")}
return {"status": True, "msg": f"Loaded config from: {file_name}"} return {
"status": True,
"msg": _(u"Loaded configurations from: %(file_name)s", file_name=file_name),
}
except (IOError, ValueError, EOFError, TypeError) as error: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except:
logging.error("Could not read file: %s", file_name) 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): def write_drive_properties(file_name, conf):
@ -455,7 +488,10 @@ def write_drive_properties(file_name, conf):
try: try:
with open(file_path, "w") as json_file: with open(file_path, "w") as json_file:
dump(conf, json_file, indent=4) 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: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
delete_file(file_path) delete_file(file_path)
@ -463,23 +499,33 @@ def write_drive_properties(file_name, conf):
except: except:
logging.error("Could not write to file: %s", file_path) logging.error("Could not write to file: %s", file_path)
delete_file(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. 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 Returns (dict) with (bool) status, (str) msg, (dict) conf
""" """
from json import load from json import load
try: try:
with open(path_name) as json_file: with open(file_path) as json_file:
conf = load(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: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except:
logging.error("Could not read file: %s", path_name) logging.error("Could not read file: %s", file_path)
return {"status": False, "msg": f"Could not read file: {path_name}"} return {
"status": False,
"msg": _(u"Could not read properties from file: %(file_path)s", file_path=file_path),
}

View File

@ -5,6 +5,7 @@ Module for methods controlling and getting information about the Pi's Linux syst
import subprocess import subprocess
import asyncio import asyncio
import logging import logging
from flask_babel import _
from settings import AUTH_GROUP from settings import AUTH_GROUP
@ -175,6 +176,6 @@ def auth_active():
if AUTH_GROUP in groups: if AUTH_GROUP in groups:
return { return {
"status": True, "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": ""} return {"status": False, "msg": ""}

View File

@ -5,6 +5,7 @@ Module for commands sent to the RaSCSI backend service.
from settings import REMOVABLE_DEVICE_TYPES from settings import REMOVABLE_DEVICE_TYPES
from socket_cmds import send_pb_command from socket_cmds import send_pb_command
from flask import current_app from flask import current_app
from flask_babel import _
import rascsi_interface_pb2 as proto import rascsi_interface_pb2 as proto
@ -24,6 +25,7 @@ def get_server_info():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO command.operation = proto.PbOperation.SERVER_INFO
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -82,6 +84,7 @@ def get_reserved_ids():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.RESERVED_IDS_INFO command.operation = proto.PbOperation.RESERVED_IDS_INFO
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -103,6 +106,7 @@ def get_network_info():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -121,6 +125,7 @@ def get_device_types():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO command.operation = proto.PbOperation.DEVICE_TYPES_INFO
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -142,6 +147,8 @@ def get_image_files_info():
""" """
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO 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()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -170,6 +177,7 @@ def attach_image(scsi_id, **kwargs):
""" """
command = proto.PbCommand() command = proto.PbCommand()
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
devices = proto.PbDeviceDefinition() devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id) devices.id = int(scsi_id)
@ -195,8 +203,12 @@ def attach_image(scsi_id, **kwargs):
if current_type != device_type: if current_type != device_type:
return { return {
"status": False, "status": False,
"msg": "Cannot insert an image for " + device_type + \ "msg": _(
" into a " + current_type + " device." 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 command.operation = proto.PbOperation.INSERT
# Handling attaching a new device # Handling attaching a new device
@ -241,6 +253,7 @@ def detach_by_id(scsi_id, unit=None):
command.operation = proto.PbOperation.DETACH command.operation = proto.PbOperation.DETACH
command.devices.append(devices) command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -256,6 +269,7 @@ def detach_all():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL command.operation = proto.PbOperation.DETACH_ALL
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -278,6 +292,7 @@ def eject_by_id(scsi_id, unit=None):
command.operation = proto.PbOperation.EJECT command.operation = proto.PbOperation.EJECT
command.devices.append(devices) command.devices.append(devices)
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -297,6 +312,7 @@ def list_devices(scsi_id=None, unit=None):
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICES_INFO command.operation = proto.PbOperation.DEVICES_INFO
command.params["token"] = current_app.config["TOKEN"] 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 # If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached 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.operation = proto.PbOperation.RESERVE_IDS
command.params["ids"] = ",".join(reserved_scsi_ids) command.params["ids"] = ",".join(reserved_scsi_ids)
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -391,6 +408,7 @@ def set_log_level(log_level):
command.operation = proto.PbOperation.LOG_LEVEL command.operation = proto.PbOperation.LOG_LEVEL
command.params["level"] = str(log_level) command.params["level"] = str(log_level)
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -408,6 +426,7 @@ def shutdown_pi(mode):
command.operation = proto.PbOperation.SHUT_DOWN command.operation = proto.PbOperation.SHUT_DOWN
command.params["mode"] = str(mode) command.params["mode"] = str(mode)
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
@ -424,6 +443,7 @@ def is_token_auth():
command = proto.PbCommand() command = proto.PbCommand()
command.operation = proto.PbOperation.CHECK_AUTHENTICATION command.operation = proto.PbOperation.CHECK_AUTHENTICATION
command.params["token"] = current_app.config["TOKEN"] command.params["token"] = current_app.config["TOKEN"]
command.params["locale"] = current_app.config["LOCALE"]
data = send_pb_command(command.SerializeToString()) data = send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()

View File

@ -7,3 +7,4 @@ MarkupSafe==2.0.1
protobuf==3.17.3 protobuf==3.17.3
requests==2.26.0 requests==2.26.0
simplepam==0.1.5 simplepam==0.1.5
flask_babel==2.0.0

View File

@ -4,6 +4,7 @@ Module for sending and receiving data over a socket connection with the RaSCSI b
import logging import logging
from flask import abort from flask import abort
from flask_babel import _
from time import sleep from time import sleep
def send_pb_command(payload): def send_pb_command(payload):
@ -35,9 +36,12 @@ def send_pb_command(payload):
logging.error(error_msg) logging.error(error_msg)
# After failing all attempts, throw a 404 error # After failing all attempts, throw a 404 error
abort(404, "The RaSCSI Web Interface failed to connect to RaSCSI at " + str(host) + \ abort(404, _(
":" + str(port) + " with error: " + error_msg + \ u"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s "
". The RaSCSI service is not running or may have crashed.") 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): def send_over_socket(sock, payload):
@ -72,9 +76,11 @@ def send_over_socket(sock, payload):
"RaSCSI may have crashed." "RaSCSI may have crashed."
) )
abort( abort(
503, "The RaSCSI Web Interface lost connection to RaSCSI. " 503, _(
"Please go back and try again. " u"The RaSCSI Web Interface lost connection to RaSCSI. "
"If the issue persists, please report a bug." u"Please go back and try again. "
u"If the issue persists, please report a bug."
)
) )
chunks.append(chunk) chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk) bytes_recvd = bytes_recvd + len(chunk)
@ -86,8 +92,9 @@ def send_over_socket(sock, payload):
"RaSCSI may have crashed." "RaSCSI may have crashed."
) )
abort( abort(
500, 500, _(
"The RaSCSI Web Interface did not get a valid response from RaSCSI. " u"The RaSCSI Web Interface did not get a valid response from RaSCSI. "
"Please go back and try again. " u"Please go back and try again. "
"If the issue persists, please report a bug." u"If the issue persists, please report a bug."
)
) )

View File

@ -74,6 +74,8 @@ else
fi fi
set -e set -e
pybabel compile -d translations
# parse arguments # parse arguments
while [ "$1" != "" ]; do while [ "$1" != "" ]; do
PARAM=$(echo "$1" | awk -F= '{print $1}') PARAM=$(echo "$1" | awk -F= '{print $1}')

View File

@ -26,12 +26,12 @@
<script type="application/javascript"> <script type="application/javascript">
var processNotify = function(Notification) { var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + " This process may take a while, and will continue in the background if you navigate away from this page.</div>"; document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div>";
window.scrollTo(0,0); window.scrollTo(0,0);
} }
var shutdownNotify = function(Notification) { var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + " The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.</div>"; document.getElementById("flash").innerHTML = "<div class='message'>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div>";
window.scrollTo(0,0); window.scrollTo(0,0);
} }
</script> </script>
@ -45,19 +45,19 @@
<div class="header"> <div class="header">
{% if auth_active %} {% if auth_active %}
{% if username %} {% if username %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Logged in as <em>{{ username }}</em> &#8211; <a href="/logout">Log Out</a></span> <span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Logged in as <em>%(username)s</em>", username=username) }} &#8211; <a href="/logout">{{ _("Log Out") }}</a></span>
{% else %} {% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: red; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;"> <span style="display: inline-block; width: 100%; color: white; background-color: red; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">
<form method="POST" action="/login"> <form method="POST" action="/login">
<div>Log In to Use Web Interface</div> <div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="Username"> <input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="Password"> <input type="password" name="password" placeholder="{{ _("Password") }}">
<input type="submit" value="Login"> <input type="submit" value="Login">
</form> </form>
</span> </span>
{% endif %} {% endif %}
{% else %} {% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Web Interface Authentication Disabled &#8211; See <a href="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication" target="_blank">Wiki</a> for more information</span> <span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} &#8211; {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
{% endif %} {% endif %}
<table width="100%"> <table width="100%">
<tbody> <tbody>
@ -84,8 +84,8 @@
{% block content %}{% endblock content %} {% block content %}{% endblock content %}
</div> </div>
<div class="footer"> <div class="footer">
<center><tt>RaSCSI version: <strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}" target="_blank">{{ running_env["git"][:7] }}</a></strong></tt></center> <center><tt>{{ _("RaSCSI version: ") }}<strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}" target="_blank">{{ running_env["git"][:7] }}</a></strong></tt></center>
<center><tt>Pi environment: {{ running_env["env"] }}</tt></center> <center><tt>{{ _("Pi environment: ") }}{{ running_env["env"] }}</tt></center>
</div> </div>
</div> </div>
</body> </body>

View File

@ -1,19 +1,19 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<p><a href="/">Cancel</a></p> <p><a href="/">{{ _("Cancel") }}</a></p>
<h2>Disclaimer</h2> <h2>{{ _("Disclaimer") }}</h2>
<p>These device profiles are provided as-is with no guarantee to work on the systems mentioned. You may need appropirate device drivers and/or configuration parameters. If you have improvement suggestions or success stories to share we would love to hear from you, so please connect with us at <a href="https://github.com/akuker/RASCSI">GitHub</a> or <a href="https://discord.gg/PyS58u6">Discord</a>!</p> <p>{{ _("These device profiles are provided as-is with no guarantee to work equally to the actual physical device they are named after. You may need to provide appropirate device drivers and/or configuration parameters for them to function properly. If you would like to see data modified, or have additional devices to add to the list, please raise an issue ticket at <a href=\"%(url)s\">GitHub</a>.", url="https://github.com/akuker/RASCSI/issues") }}</p>
<h2>Hard Drives</h2> <h2>{{ _("Hard Drives") }}</h2>
<table cellpadding="3" border="black"> <table cellpadding="3" border="black">
<tbody> <tbody>
<tr> <tr>
<td><b>Name</b></td> <td><b>{{ _("Name") }}</b></td>
<td><b>Size (MB)</b></td> <td><b>{{ _("Size (MB)") }}</b></td>
<td><b>Description</b></td> <td><b>{{ _("Description") }}</b></td>
<td><b>Ref.</b></td> <td><b>{{ _("Ref.") }}</b></td>
<td><b>Action</b></td> <td><b>{{ _("Action") }}</b></td>
</tr> </tr>
{% for hd in hd_conf %} {% for hd in hd_conf %}
<tr> <tr>
@ -22,7 +22,7 @@
<td style="text-align:left">{{ hd.description }}</td> <td style="text-align:left">{{ hd.description }}</td>
<td style="text-align:left"> <td style="text-align:left">
{% if hd.url != "" %} {% if hd.url != "" %}
<a href="{{ hd.url }}">Link</a> <a href="{{ hd.url }}">{{ _("Link") }}</a>
{% else %} {% else %}
- -
{% endif %} {% endif %}
@ -36,9 +36,9 @@
<input type="hidden" name="block_size" value="{{ hd.block_size }}"> <input type="hidden" name="block_size" value="{{ hd.block_size }}">
<input type="hidden" name="size" value="{{ hd.size }}"> <input type="hidden" name="size" value="{{ hd.size }}">
<input type="hidden" name="file_type" value="{{ hd.file_type }}"> <input type="hidden" name="file_type" value="{{ hd.file_type }}">
<label for="file_name">Save as:</label> <label for="file_name">{{ _("Save as:") }}</label>
<input type="text" name="file_name" value="{{ hd.secure_name }}" required />.{{ hd.file_type }} <input type="text" name="file_name" value="{{ hd.secure_name }}" required />.{{ hd.file_type }}
<input type="submit" value="Create" /> <input type="submit" value="{{ _("Create") }}" />
</form> </form>
</td> </td>
</tr> </tr>
@ -48,16 +48,16 @@
<hr/> <hr/>
<h2>CD-ROM Drives</h2> <h2>{{ _("CD-ROM Drives") }}</h2>
<p><em>This will create a properties file for the given CD-ROM image. No new image file will be created.</em></p> <p><em>{{ _("This will create a properties file for the given CD-ROM image. No new image file will be created.") }}</em></p>
<table cellpadding="3" border="black"> <table cellpadding="3" border="black">
<tbody> <tbody>
<tr> <tr>
<td><b>Name</b></td> <td><b>{{ _("Name") }}</b></td>
<td><b>Size (MB)</b></td> <td><b>{{ _("Size (MB)") }}</b></td>
<td><b>Description</b></td> <td><b>{{ _("Description") }}</b></td>
<td><b>Ref.</b></td> <td><b>{{ _("Ref.") }}</b></td>
<td><b>Action</b></td> <td><b>{{ _("Action") }}</b></td>
</tr> </tr>
{% for cd in cd_conf %} {% for cd in cd_conf %}
<tr> <tr>
@ -66,7 +66,7 @@
<td style="text-align:left">{{ cd.description }}</td> <td style="text-align:left">{{ cd.description }}</td>
<td style="text-align:left"> <td style="text-align:left">
{% if cd.url != "" %} {% if cd.url != "" %}
<a href="{{ cd.url }}">Link</a> <a href="{{ cd.url }}">{{ _("Link") }}</a>
{% else %} {% else %}
- -
{% endif %} {% endif %}
@ -77,7 +77,7 @@
<input type="hidden" name="product" value="{{ cd.product }}"> <input type="hidden" name="product" value="{{ cd.product }}">
<input type="hidden" name="revision" value="{{ cd.revision }}"> <input type="hidden" name="revision" value="{{ cd.revision }}">
<input type="hidden" name="block_size" value="{{ cd.block_size }}"> <input type="hidden" name="block_size" value="{{ cd.block_size }}">
<label for="file_name">Create for:</label> <label for="file_name">{{ _("Create for:") }}</label>
<select type="select" name="file_name"> <select type="select" name="file_name">
{% for f in files %} {% for f in files %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %} {% if f["name"].lower().endswith(cdrom_file_suffix) %}
@ -85,7 +85,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Create" /> <input type="submit" value="{{ _("Create") }}" />
</form> </form>
</td> </td>
</tr> </tr>
@ -95,15 +95,15 @@
<hr/> <hr/>
<h2>Removable Drives</h2> <h2>{{ _("Removable Drives") }}</h2>
<table cellpadding="3" border="black"> <table cellpadding="3" border="black">
<tbody> <tbody>
<tr> <tr>
<td><b>Name</b></td> <td><b>{{ _("Name") }}</b></td>
<td><b>Size (MB)</b></td> <td><b>{{ _("Size (MB)") }}</b></td>
<td><b>Description</b></td> <td><b>{{ _("Description") }}</b></td>
<td><b>Ref.</b></td> <td><b>{{ _("Ref.") }}</b></td>
<td><b>Action</b></td> <td><b>{{ _("Action") }}</b></td>
</tr> </tr>
{% for rm in rm_conf %} {% for rm in rm_conf %}
<tr> <tr>
@ -112,7 +112,7 @@
<td style="text-align:left">{{ rm.description }}</td> <td style="text-align:left">{{ rm.description }}</td>
<td style="text-align:left"> <td style="text-align:left">
{% if rm.url != "" %} {% if rm.url != "" %}
<a href="{{ rm.url }}">Link</a> <a href="{{ rm.url }}">{{ _("Link") }}</a>
{% else %} {% else %}
- -
{% endif %} {% endif %}
@ -126,16 +126,16 @@
<input type="hidden" name="block_size" value="{{ rm.block_size }}"> <input type="hidden" name="block_size" value="{{ rm.block_size }}">
<input type="hidden" name="size" value="{{ rm.size }}"> <input type="hidden" name="size" value="{{ rm.size }}">
<input type="hidden" name="file_type" value="{{ rm.file_type }}"> <input type="hidden" name="file_type" value="{{ rm.file_type }}">
<label for="file_name">Save as:</label> <label for="file_name">{{ _("Save as:") }}</label>
<input type="text" name="file_name" value="{{ rm.secure_name }}" required />.{{ rm.file_type }} <input type="text" name="file_name" value="{{ rm.secure_name }}" required />.{{ rm.file_type }}
<input type="submit" value="Create" /> <input type="submit" value="{{ _("Create") }}" />
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p><small>Available disk space on the Pi: {{ free_disk }} MB</small></p> <p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<p><a href="/">Cancel</a></p> <p><a href="/">{{ _("Cancel") }}</a></p>
{% endblock content %} {% endblock content %}

View File

@ -3,12 +3,12 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Current RaSCSI Configuration {{ _("Current RaSCSI Configuration") }}
</summary> </summary>
<ul> <ul>
<li>Displays the currently attached devices for each available SCSI ID.</li> <li>{{ _("Displays the currently attached devices for each available SCSI ID.") }}</li>
<li>Save and load device configurations into <tt>{{ CFG_DIR }}</tt></li> <li>{{ _("Save and load device configurations, stored as json files in <tt>%(config_dir)s</tt>", config_dir=CFG_DIR) }}</tt></li>
<li>The <em>default</em> configuration will be loaded when the Web UI starts up, if available.</li> <li>{{ _("To have a particular device configuration load when RaSCSI starts, save it as <em>default</em>.") }}</li>
</ul> </ul>
</details> </details>
@ -22,31 +22,31 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<option disabled> <option disabled>
No saved configs {{ _("No saved configurations") }}
</option> </option>
{% endif %} {% endif %}
</select> </select>
<input name="load" type="submit" value="Load" onclick="return confirm('Detach all current device and Load config?')"> <input name="load" type="submit" value="{{ _("Load") }}" onclick="return confirm('{{ _("Detach all current device and Load configuration?") }}')">
<input name="delete" type="submit" value="Delete" onclick="return confirm('Delete config file?')"> <input name="delete" type="submit" value="{{ _("Delete") }}" onclick="return confirm('{{ _("Delete configuration file?") }}')">
</form></p> </form></p>
<p><form action="/config/save" method="post"> <p><form action="/config/save" method="post">
<input name="name" placeholder="default" size="20"> <input name="name" placeholder="default" size="20">
<input type="submit" value="Save"> <input type="submit" value="{{ _("Save") }}">
</form></p> </form></p>
<table border="black" cellpadding="3"> <table border="black" cellpadding="3">
<tbody> <tbody>
<tr> <tr>
<td><b>ID</b></td> <td><b>{{ _("ID") }}</b></td>
{% if units %} {% if units %}
<td><b>LUN</b></td> <td><b>{{ _("LUN") }}</b></td>
{% endif %} {% endif %}
<td><b>Type</b></td> <td><b>{{ _("Type") }}</b></td>
<td><b>Status</b></td> <td><b>{{ _("Status") }}</b></td>
<td><b>File</b></td> <td><b>{{ _("File") }}</b></td>
<td><b>Product</b></td> <td><b>{{ _("Product") }}</b></td>
<td><b>Actions</b></td> <td><b>{{ _("Actions") }}</b></td>
</tr> </tr>
{% for device in devices %} {% for device in devices %}
<tr> <tr>
@ -81,7 +81,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Attach"> <input type="submit" value="{{ _("Attach") }}">
</form> </form>
{% else %} {% else %}
{{ device.file }} {{ device.file }}
@ -95,28 +95,28 @@
<td style="text-align:center"> <td style="text-align:center">
{% if device.device_type != "-" %} {% if device.device_type != "-" %}
{% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %} {% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %}
<form action="/scsi/eject" method="post" onsubmit="return confirm('Eject Disk? WARNING: On Mac OS, eject the Disk in Finder instead!')"> <form action="/scsi/eject" method="post" onsubmit="return confirm('{{ _("Eject Disk? WARNING: On Mac OS, eject the Disk in the Finder instead!") }}')">
<input name="scsi_id" type="hidden" value="{{ device.id }}"> <input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}"> <input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Eject"> <input type="submit" value="{{ _("Eject") }}">
</form> </form>
{% else %} {% else %}
<form action="/scsi/detach" method="post" onsubmit="return confirm('Detach Device?')"> <form action="/scsi/detach" method="post" onsubmit="return confirm('{{ _("Detach Device?") }}')">
<input name="scsi_id" type="hidden" value="{{ device.id }}"> <input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}"> <input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Detach"> <input type="submit" value="{{ _("Detach") }}">
</form> </form>
{% endif %} {% endif %}
<form action="/scsi/info" method="post"> <form action="/scsi/info" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}"> <input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}"> <input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Info"> <input type="submit" value="{{ _("Info") }}">
</form> </form>
{% else %} {% else %}
<form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('Enter a memo for this reservation'); if (memo === null) event.preventDefault(); document.getElementById('memo_{{ device.id }}').value = memo;"> <form action="/scsi/reserve" method="post" onsubmit="var memo = prompt('{{ _("Enter a memo for this reservation") }}'); if (memo === null) event.preventDefault(); document.getElementById('memo_{{ device.id }}').value = memo;">
<input name="scsi_id" type="hidden" value="{{ device.id }}"> <input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="memo" id="memo_{{ device.id }}" type="hidden" value=""> <input name="memo" id="memo_{{ device.id }}" type="hidden" value="">
<input type="submit" value="Reserve"> <input type="submit" value="{{ _("Reserve") }}">
</form> </form>
{% endif %} {% endif %}
</td> </td>
@ -126,13 +126,13 @@
<td class="inactive"></td> <td class="inactive"></td>
{% endif %} {% endif %}
<td class="inactive"></td> <td class="inactive"></td>
<td class="inactive">Reserved ID</td> <td class="inactive">{{ _("Reserved ID") }}</td>
<td class="inactive">{{ RESERVATIONS[device.id] }}</td> <td class="inactive">{{ RESERVATIONS[device.id] }}</td>
<td class="inactive"></td> <td class="inactive"></td>
<td class="inactive"> <td class="inactive">
<form action="/scsi/unreserve" method="post"> <form action="/scsi/unreserve" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}"> <input name="scsi_id" type="hidden" value="{{ device.id }}">
<input type="submit" value="Unreserve"> <input type="submit" value="{{ _("Unreserve") }}">
</form> </form>
</td> </td>
{% endif %} {% endif %}
@ -141,31 +141,31 @@
</tbody> </tbody>
</table> </table>
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('Detach all SCSI Devices?')"> <p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="Detach All Devices"> <input type="submit" value="{{ _("Detach All Devices") }}">
</form></p> </form></p>
<hr/> <hr/>
<details> <details>
<summary class="heading"> <summary class="heading">
Image File Management {{ _("Image File Management") }}
</summary> </summary>
<ul> <ul>
<li>Manage image files in the active RaSCSI image directory: <tt>{{ base_dir }}</tt> with a scan depth of {{ scan_depth }}.</li> <li>{{ _("Manage image files in the active RaSCSI image directory: <tt>%(directory)s</tt> with a scan depth of %(scan_depth)s.", directory=base_dir, scan_depth=scan_depth) }}</li>
<li>Select a valid SCSI ID and <a href="https://en.wikipedia.org/wiki/Logical_unit_number">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0. <li>{{ _("Select a valid SCSI ID and <a href=\"%(url)s\">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.", url="https://en.wikipedia.org/wiki/Logical_unit_number") }}
</li> </li>
<li>If RaSCSI was unable to detect the device type associated with the image, you can choose the type from the dropdown.</li> <li>{{ _("If RaSCSI was unable to detect the device type associated with the image, you can choose the type from the dropdown.") }}</li>
<li>Types: SAHD = SASI HDD | SCHD = SCSI HDD | SCRM = Removable | SCMO = Magneto-Optical | SCCD = CD-ROM | SCBR = Host Bridge | SCDP = DaynaPORT</li> <li>{{ _("Types: SAHD = SASI HDD | SCHD = SCSI HDD | SCRM = Removable | SCMO = Magneto-Optical | SCCD = CD-ROM | SCBR = Host Bridge | SCDP = DaynaPORT") }}</li>
</ul> </ul>
</details> </details>
<table border="black" cellpadding="3"> <table border="black" cellpadding="3">
<tbody> <tbody>
<tr> <tr>
<td><b>File</b></td> <td><b>{{ _("File") }}</b></td>
<td><b>Size</b></td> <td><b>{{ _("Size") }}</b></td>
<td><b>Actions</b></td> <td><b>{{ _("Actions") }}</b></td>
</tr> </tr>
{% for file in files %} {% for file in files %}
<tr> <tr>
@ -181,7 +181,7 @@
{% endfor %} {% endfor %}
<form action="/files/download" method="post"> <form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ CFG_DIR }}/{{ file['name'].replace(base_dir, '') }}.{{ PROPERTIES_SUFFIX }}"> <input name="file" type="hidden" value="{{ CFG_DIR }}/{{ file['name'].replace(base_dir, '') }}.{{ PROPERTIES_SUFFIX }}">
<input type="submit" value="Properties File &#8595;"> <input type="submit" value="{{ _("Properties File") }} &#8595;">
</form> </form>
</ul> </ul>
</details> </details>
@ -201,7 +201,7 @@
<form action="/files/unzip" method="post"> <form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}"> <input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_member" type="hidden" value="{{ member }}"> <input name="zip_member" type="hidden" value="{{ member }}">
<input type="submit" value="Unzip" onclick="processNotify('Unzipping a single file...')"> <input type="submit" value="{{ _("Unzip") }}" onclick="processNotify('{{ _("Unzipping a single file...") }}')">
</form> </form>
</summary> </summary>
<ul style="list-style: none;"> <ul style="list-style: none;">
@ -215,7 +215,7 @@
<form action="/files/unzip" method="post"> <form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}"> <input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_member" type="hidden" value="{{ member }}"> <input name="zip_member" type="hidden" value="{{ member }}">
<input type="submit" value="Unzip" onclick="processNotify('Unzipping a single file...')"> <input type="submit" value="{{ _("Unzip") }}" onclick="processNotify('{{ _("Unzipping a single file...") }}')">
</form> </form>
{% endif %} {% endif %}
</li> </li>
@ -230,26 +230,26 @@
<td style="text-align:center"> <td style="text-align:center">
<form action="/files/download" method="post"> <form action="/files/download" method="post">
<input name="file" type="hidden" value="{{ base_dir }}/{{ file['name'] }}"> <input name="file" type="hidden" value="{{ base_dir }}/{{ file['name'] }}">
<input type="submit" value="{{ file['size_mb'] }} MB &#8595;"> <input type="submit" value="{{ file['size_mb'] }} {{ _("MB") }} &#8595;">
</form> </form>
</td> </td>
<td> <td>
{% if file["name"] in attached_images %} {% if file["name"] in attached_images %}
<center> <center>
Attached! {{ _("Attached!") }}
</center> </center>
{% else %} {% else %}
{% if file["name"].lower().endswith(ARCHIVE_FILE_SUFFIX) %} {% if file["name"].lower().endswith(ARCHIVE_FILE_SUFFIX) %}
<form action="/files/unzip" method="post"> <form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}"> <input name="zip_file" type="hidden" value="{{ file['name'] }}">
<input name="zip_members" type="hidden" value="{{ file['zip_members'] }}"> <input name="zip_members" type="hidden" value="{{ file['zip_members'] }}">
<input type="submit" value="Unzip All" onclick="processNotify('Unzipping all files...')"> <input type="submit" value="{{ _("Unzip All") }}" onclick="processNotify('{{ _("Unzipping all files...") }}')">
</form> </form>
{% else %} {% else %}
<form action="/scsi/attach" method="post"> <form action="/scsi/attach" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}"> <input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="file_size" type="hidden" value="{{ file['size'] }}"> <input name="file_size" type="hidden" value="{{ file['size'] }}">
<label for="id">ID</label> <label for="id">{{ _("ID") }}</label>
<select name="scsi_id"> <select name="scsi_id">
{% for id in scsi_ids %} {% for id in scsi_ids %}
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}> <option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>
@ -257,7 +257,7 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<label for="unit">LUN</label> <label for="unit">{{ _("LUN") }}</label>
<input name="unit" type="number" size="2" value="0" min="0" max="31"> <input name="unit" type="number" size="2" value="0" min="0" max="31">
{% if file["detected_type"] != "UNDEFINED" %} {% if file["detected_type"] != "UNDEFINED" %}
<input name="type" type="hidden" value="{{ file['detected_type'] }}"> <input name="type" type="hidden" value="{{ file['detected_type'] }}">
@ -265,7 +265,7 @@
{% else %} {% else %}
<select name="type"> <select name="type">
<option selected value=""> <option selected value="">
Type {{ _("Type") }}
</option> </option>
{% for d in device_types %} {% for d in device_types %}
<option value="{{ d }}"> <option value="{{ d }}">
@ -274,17 +274,17 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</select> </select>
<input type="submit" value="Attach"> <input type="submit" value="{{ _("Attach") }}">
{% endif %} {% endif %}
</form> </form>
<form action="/files/rename" method="post" onsubmit="var new_file_name = prompt('Enter new file name for \'{{ file["name"] }}\'', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;"> <form action="/files/rename" method="post" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;">
<input name="file_name" type="hidden" value="{{ file['name'] }}"> <input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value=""> <input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value="">
<input type="submit" value="Rename"> <input type="submit" value="{{ _("Rename") }}">
</form> </form>
<form action="/files/delete" method="post" onsubmit="return confirm('Delete file \'{{ file["name"] }}\'?')"> <form action="/files/delete" method="post" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
<input name="file_name" type="hidden" value="{{ file['name'] }}"> <input name="file_name" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="Delete"> <input type="submit" value="{{ _("Delete") }}">
</form> </form>
{% endif %} {% endif %}
</td> </td>
@ -292,22 +292,22 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p><small>Available disk space on the Pi: {{ free_disk }} MB</small></p> <p><small>{{ _("%(disk_space)s MB disk space remaining on the Pi", disk_space=free_disk) }}</small></p>
<hr/> <hr/>
<details> <details>
<summary class="heading"> <summary class="heading">
Attach Ethernet Adapter {{ _("Attach Ethernet Adapter") }}
</summary> </summary>
<ul> <ul>
<li>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers and configuration required</a>. <li>{{ _("Emulates a SCSI DaynaPORT Ethernet Adapter. <a href=\"%(url)s\">Host drivers and configuration required</a>.", url="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link") }}
</li> </li>
<li>If you have a DHCP setup, choose only the interface, and ignore the Static IP fields when attaching.</li> <li>{{ _("If you have a DHCP setup, choose only the interface you have configured the bridge with. You can ignore the Static IP fields when attaching.") }}</li>
<li>Configure network forwarding by running easyinstall.sh, or follow the <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup">manual steps in the wiki</a>. <li>{{ _("Configure the network bridge by running easyinstall.sh, or follow the <a href=\"%(url)s\">manual steps in the wiki</a>.", url="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup") }}
</li> </li>
<li style="list-style: none">{% if bridge_configured %}</li> <li style="list-style: none">{% if bridge_configured %}</li>
<li>The <tt>rascsi_bridge</tt> interface is active and ready to be used by DaynaPORT!</li> <li>{{ _("The <tt>rascsi_bridge</tt> interface is active and ready to be used by DaynaPORT!") }}</li>
<li style="list-style: none">{% endif %}</li> <li style="list-style: none">{% endif %}</li>
</ul> </ul>
</details> </details>
@ -315,7 +315,7 @@
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/daynaport/attach" method="post"> <form action="/daynaport/attach" method="post">
<label for="if">Interface:</label> <label for="if">{{ _("Interface:") }}</label>
<select name="if"> <select name="if">
{% for if in netinfo["ifs"] %} {% for if in netinfo["ifs"] %}
<option value="{{ if }}"> <option value="{{ if }}">
@ -323,10 +323,10 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<label for="ip">Static IP (optional):</label> <label for="ip">{{ _("Static IP (optional):") }}</label>
<input name="ip" type="text" size="15" placeholder="10.10.20.1" minlength="7" maxlength="15" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"> <input name="ip" type="text" size="15" placeholder="10.10.20.1" minlength="7" maxlength="15" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
<input name="mask" type="number" size="2" placeholder="24" min="16" max="30"> <input name="mask" type="number" size="2" placeholder="24" min="16" max="30">
<label for="scsi_id">SCSI ID:</label> <label for="scsi_id">{{ _("SCSI ID:") }}</label>
<select name="scsi_id"> <select name="scsi_id">
{% for id in scsi_ids %} {% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}> <option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
@ -334,26 +334,26 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Attach"> <input type="submit" value="{{ _("Attach") }}">
</form> </form>
</td> </td>
</tr> </tr>
</table> </table>
{% if macproxy_configured %} {% if macproxy_configured %}
<p><small>Macproxy is running at {{ ip_addr }} port 5000</small></p> <p><small>{{ _("Macproxy is running at %(ip_addr)s (default port 5000)", ip_addr=ip_addr) }}</small></p>
{% else %} {% else %}
<p><small>Install <a href="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy">Macproxy</a> to browse the Web with any vintage browser. It's not just for Macs!</small></p> <p><small>{{ _("Install <a href=\"%(url)s\">Macproxy</a> to browse the Web with any vintage browser. It's not just for Macs!", url="https://github.com/akuker/RASCSI/wiki/Vintage-Web-Proxy#macproxy") }}</small></p>
{% endif %} {% endif %}
<hr/> <hr/>
<details> <details>
<summary class="heading"> <summary class="heading">
Upload File {{ _("Upload File") }}
</summary> </summary>
<ul> <ul>
<li>Uploads file to <tt>{{ base_dir }}</tt>. The largest file size accepted is {{ max_file_size }} MB.</li> <li>{{ _("Uploads file to <tt>%(directory)s</tt>. The largest file size accepted is %(max_file_size)s MB.", directory=base_dir, max_file_size=max_file_size) }}</li>
<li>For unrecognized file types, try renaming hard drive images to '.hds' and CD-ROM images to '.iso' before uploading.</li> <li>{{ _("For unrecognized file types, try renaming hard drive images to '.hds', CD-ROM images to '.iso', and removable drive images to '.hdr' before uploading.") }}</li>
<li>Recognized file types: {{ valid_file_suffix }}</li> <li>{{ _("Recognized file types: %(valid_file_suffix)s", valid_file_suffix=valid_file_suffix) }}</li>
</ul> </ul>
</details> </details>
@ -380,10 +380,10 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Download File to Images {{ _("Download File to Images") }}
</summary> </summary>
<ul> <ul>
<li>Given a URL, download that file to the <tt>{{ base_dir }}</tt> directory.</li> <li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory.", directory=base_dir) }}</li>
</ul> </ul>
</details> </details>
@ -391,9 +391,9 @@
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post"> <form action="/files/download_to_images" method="post">
<label for="url">URL:</label> <label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="URL" required="" type="url"> <input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="Download" onclick="processNotify('Downloading File to Images...')"> <input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to Images...") }}')">
</form> </form>
</td> </td>
</tr> </tr>
@ -403,13 +403,12 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Download File to AppleShare {{ _("Download File to AppleShare") }}
</summary> </summary>
<ul> <ul>
<li>Given a URL, download that file to the <tt>{{ AFP_DIR }}</tt> directory and share it over AFP.</li> <li>{{ _("Given a URL, download that file to the <tt>%(directory)s</tt> directory and share it over AFP.", directory=AFP_DIR) }}</li>
<li>Manage the files you download here through AppleShare on your vintage Mac.</li> <li>{{ _("Manage the files you download here through AppleShare on your vintage Mac.") }}</li>
<li>Requires <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to be installed and configured correctly for your network. <li>{{ _("Requires <a href=\"%(url)s\">Netatalk</a> to be installed and configured correctly for your network.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
</li>
</ul> </ul>
</details> </details>
@ -418,43 +417,42 @@
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post"> <form action="/files/download_to_afp" method="post">
<label for="url">URL:</label> <label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="URL" required="" type="url"> <input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="Download" onclick="processNotify('Downloading File to AppleShare...')"> <input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to AppleShare...") }}')">
</form> </form>
</td> </td>
</tr> </tr>
</table> </table>
{% if netatalk_configured == 1 %} {% if netatalk_configured == 1 %}
<p><small>The AppleShare server is running. No active connections</small></p> <p><small>{{ _("The AppleShare server is running. No active connections.") }}</small></p>
{% elif netatalk_configured == 2 %} {% elif netatalk_configured == 2 %}
<p><small>{{ netatalk_configured - 1 }} active AFP connection</small></p> <p><small>{{ _("%(value)d active AFP connection", value=(netatalk_configured - 1)) }}</small></p>
{% elif netatalk_configured > 2 %} {% elif netatalk_configured > 2 %}
<p><small>{{ netatalk_configured - 1 }} active AFP connections</small></p> <p><small>{{ _("%(value)d active AFP connections", value=(netatalk_configured - 1)) }}</small></p>
{% endif %} {% endif %}
{% else %} {% else %}
<p>Install <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to use the AppleShare File Server.</p> <p>{{ _("Install <a href=\"%(url)s\">Netatalk</a> to use the AppleShare File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</p>
{% endif %} {% endif %}
<hr/> <hr/>
<details> <details>
<summary class="heading"> <summary class="heading">
Download File and Create CD-ROM ISO image {{ _("Download File and Create CD-ROM image") }}
</summary> </summary>
<ul> <ul>
<li>Given a URL this will download a file, create a CD-ROM image with the selected file system, and mount it on the SCSI ID given.</li> <li>{{ _("Create an ISO file system CD-ROM image with the downloaded file, and mount it on the given SCSI ID.") }}</li>
<li>HFS is for Mac OS, Joliet for Windows, and Rock Ridge for POSIX.</li> <li>{{ _("HFS is for Mac OS, Joliet for Windows, and Rock Ridge for POSIX.") }}</li>
<li>On Mac OS, requires a <a href="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images">compatible CD-ROM driver</a> installed on the target system. <li>{{ _("On Mac OS, a <a href=\"%(url)s\">compatible CD-ROM driver</a> is required.", url="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images") }}</li>
</li> <li>{{ _("If the downloaded file is a zip archive, we will attempt to unzip it and store the resulting files.") }}</li>
<li>If the target file is a zip archive, we will attempt to unzip it and store the resulting files only.</li>
</ul> </ul>
</details> </details>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<label for="scsi_id">SCSI ID:</label> <label for="scsi_id">{{ _("SCSI ID:") }}</label>
<form action="/files/download_to_iso" method="post"> <form action="/files/download_to_iso" method="post">
<select name="scsi_id"> <select name="scsi_id">
{% for id in scsi_ids %} {% for id in scsi_ids %}
@ -463,9 +461,9 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<label for="url">URL:</label> <label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="URL" required="" type="url"> <input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<label for="type">Type:</label> <label for="type">{{ _("Type:") }}</label>
<select name="type"> <select name="type">
<option value="-hfs"> <option value="-hfs">
HFS HFS
@ -486,7 +484,7 @@
Rock Ridge Rock Ridge
</option> </option>
</select> </select>
<input type="submit" value="Download and Mount ISO" onclick="processNotify('Downloading File as ISO...')"> <input type="submit" value="{{ _("Download and Mount CD-ROM image") }}" onclick="processNotify('{{ _("Downloading File and generating CD-ROM image...") }}')">
</form> </form>
</td> </td>
</tr> </tr>
@ -496,41 +494,41 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Create Empty Disk Image File {{ _("Create Empty Disk Image File") }}
</summary> </summary>
<ul> <ul>
<li>The Generic image type is recommended for most systems</li> <li>{{ _("The Generic image type is recommended for most computer platforms.") }}</li>
<li>APPLE GENUINE and NEC GENUINE image types will make RaSCSI masquerade as a particular drive type that are recognized by Mac and PC98 systems, respectively.</li> <li>{{ _("APPLE GENUINE (.hda) and NEC GENUINE (.hdn) image types will make RaSCSI behave as a particular drive type that are recognized by Mac and PC98 systems, respectively.") }}</li>
<li>SASI images should only be used on early X68000 or UNIX workstation systems that use this pre-SCSI standard.</li> <li>{{ _("SASI images should only be used on the original Sharp X68000, or other legacy systems that utilize this pre-SCSI standard.") }}</li>
</ul> </ul>
</details> </details>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/files/create" method="post"> <form action="/files/create" method="post">
<label for="file_name">File Name:</label> <label for="file_name">{{ _("File Name:") }}</label>
<input name="file_name" placeholder="File name" required="" type="text"> <input name="file_name" placeholder="{{ _("File Name") }}" required="" type="text">
<label for="type">Type:</label> <label for="type">{{ _("Type:") }}</label>
<select name="type"> <select name="type">
<option value="hds"> <option value="hds">
SCSI Hard Disk image (Generic) [.hds] {{ _("SCSI Hard Disk image (Generic) [.hds]") }}
</option> </option>
<option value="hda"> <option value="hda">
SCSI Hard Disk image (APPLE GENUINE - use with Mac) [.hda] {{ _("SCSI Hard Disk image (APPLE GENUINE) [.hda]") }}
</option> </option>
<option value="hdn"> <option value="hdn">
SCSI Hard Disk image (NEC GENUINE - use with PC98) [.hdn] {{ _("SCSI Hard Disk image (NEC GENUINE) [.hdn]") }}
</option> </option>
<option value="hdr"> <option value="hdr">
SCSI Removable Media Disk image (Generic) [.hdr] {{ _("SCSI Removable Media Disk image (Generic) [.hdr]") }}
</option> </option>
<option value="hdf"> <option value="hdf">
SASI Hard Disk image (use with X68000) [.hdf] {{ _("SASI Hard Disk image (Legacy) [.hdf]") }}
</option> </option>
</select> </select>
<label for="size">Size:</label> <label for="size">{{ _("Size:") }}</label>
<input name="size" type="number" placeholder="MB" min="1" size="6" required> <input name="size" type="number" placeholder="{{ _("MB") }}" min="1" size="6" required>
<input type="submit" value="Create"> <input type="submit" value="{{ _("Create") }}">
</form> </form>
</td> </td>
</tr> </tr>
@ -540,32 +538,32 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Create Named Drive {{ _("Create Named Drive") }}
</summary> </summary>
<ul> <ul>
<li>Here you can create pairs of images and properties files from a list of real-life drives.</li> <li>{{ _("Create pairs of images and properties files from a list of real-life drives.") }}</li>
<li>This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems</li> <li>{{ _("This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems.") }}</li>
</ul> </ul>
</details> </details>
<p><a href="/drive/list">Create a named disk image that mimics real-life drives</a></p> <p><a href="/drive/list">{{ _("Create a named disk image that mimics real-life drives") }}</a></p>
<hr/> <hr/>
<details> <details>
<summary class="heading"> <summary class="heading">
Logging {{ _("Logging") }}
</summary> </summary>
<ul> <ul>
<li>Get a certain number of lines of service logs with the given scope.</li> <li>{{ _("Fetch a certain number of lines of system logs with the given scope.") }}</li>
</ul> </ul>
</details> </details>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post"> <form action="/logs/show" method="post">
<label for="lines">Log Lines:</label> <label for="lines">{{ _("Log Lines:") }}</label>
<input name="lines" type="number" placeholder="200" min="1" size="4"> <input name="lines" type="number" placeholder="200" min="1" size="4">
<label for="scope">Scope:</label> <label for="scope">{{ _("Scope:") }}</label>
<select name="scope"> <select name="scope">
<option value="default"> <option value="default">
default default
@ -577,7 +575,7 @@
rascsi-web.service rascsi-web.service
</option> </option>
</select> </select>
<input type="submit" value="Show Logs"> <input type="submit" value="{{ _("Show Logs") }}">
</form> </form>
</td> </td>
</tr> </tr>
@ -587,18 +585,18 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Server Log Level {{ _("Server Log Level") }}
</summary> </summary>
<ul> <ul>
<li>Change the log level of the RaSCSI backend service.</li> <li>{{ _("Change the log level of the RaSCSI backend process.") }}</li>
<li>The dropdown will indicate the current log level.</li> <li>{{ _("The current dropdown selection indicates the active log level.") }}</li>
</ul> </ul>
</details> </details>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post"> <form action="/logs/level" method="post">
<label for="level">Log Level:</label> <label for="level">{{ _("Log Level:") }}</label>
<select name="level"> <select name="level">
{% for level in log_levels %} {% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}> <option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
@ -606,7 +604,7 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Set Log Level"> <input type="submit" value="{{ _("Set Log Level") }}">
</form> </form>
</td> </td>
</tr> </tr>
@ -616,23 +614,23 @@
<details> <details>
<summary class="heading"> <summary class="heading">
Raspberry Pi Operations {{ _("Raspberry Pi Operations") }}
</summary> </summary>
<ul> <ul>
<li>Reboot or shut down the Raspberry Pi that RaSCSI is running on.</li> <li>{{ _("Reboot or shut down the Raspberry Pi that RaSCSI is running on.") }}</li>
<li>IMPORTANT: Always shut down the Pi before turning off the power. Failing to do so may lead to data corruption.</li> <li>{{ _("IMPORTANT: Always shut down the Pi before turning off the power. Failing to do so may lead to data loss.") }}</li>
</ul> </ul>
</details> </details>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/pi/reboot" method="post" onclick="if (confirm('Reboot the Raspberry Pi?')) shutdownNotify('Rebooting the Raspberry Pi...'); else event.preventDefault();"> <form action="/pi/reboot" method="post" onclick="if (confirm('{{ _("Reboot the Raspberry Pi?") }}')) shutdownNotify('{{ _("Rebooting the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="Reboot Raspberry Pi"> <input type="submit" value="{{ _("Reboot Raspberry Pi") }}">
</form> </form>
</td> </td>
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/pi/shutdown" method="post" onclick="if (confirm('Shut down the Raspberry Pi?')) shutdownNotify('Shutting down the Raspberry Pi...'); else event.preventDefault();"> <form action="/pi/shutdown" method="post" onclick="if (confirm('{{ _("Shut down the Raspberry Pi?") }}')) shutdownNotify('{{ _("Shutting down the Raspberry Pi...") }}'); else event.preventDefault();">
<input type="submit" value="Shut Down Raspberry Pi"> <input type="submit" value="{{ _("Shut Down Raspberry Pi") }}">
</form> </form>
</td> </td>
</tr> </tr>

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ from flask import (
session, session,
abort, abort,
) )
from flask_babel import Babel, _
from file_cmds import ( from file_cmds import (
list_images, list_images,
@ -81,6 +82,11 @@ from settings import (
) )
APP = Flask(__name__) APP = Flask(__name__)
BABEL = Babel(APP)
@BABEL.localeselector
def get_locale():
return request.accept_languages.best_match(["en", "de", "sv"])
@APP.route("/") @APP.route("/")
def index(): def index():
@ -88,7 +94,7 @@ def index():
Sets up data structures for and renders the index page Sets up data structures for and renders the index page
""" """
if not is_token_auth()["status"] and not APP.config["TOKEN"]: if not is_token_auth()["status"] and not APP.config["TOKEN"]:
abort(403, "RaSCSI is password protected. Start the Web Interface with the --password parameter.") abort(403, _(u"RaSCSI is password protected. Start the Web Interface with the --password parameter."))
server_info = get_server_info() server_info = get_server_info()
disk = disk_space() disk = disk_space()
@ -182,7 +188,7 @@ def drive_list():
return redirect(url_for("index")) return redirect(url_for("index"))
conf = process["conf"] conf = process["conf"]
else: else:
flash("Could not read drive properties from " + str(drive_properties), "error") flash(_("Could not read drive properties from %(properties_file)s", properties_file=drive_properties), "error")
return redirect(url_for("index")) return redirect(url_for("index"))
hd_conf = [] hd_conf = []
@ -246,7 +252,7 @@ def login():
if authenticate(str(username), str(password)): if authenticate(str(username), str(password)):
session["username"] = request.form["username"] session["username"] = request.form["username"]
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"You must log in with credentials for a user in the '{AUTH_GROUP}' group!", "error") flash(_(u"You must log in with credentials for a user in the '%(group)s' group", group=AUTH_GROUP), "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -294,11 +300,12 @@ def drive_create():
size = request.form.get("size") size = request.form.get("size")
file_type = request.form.get("file_type") file_type = request.form.get("file_type")
file_name = request.form.get("file_name") file_name = request.form.get("file_name")
full_file_name = file_name + "." + file_type
# Creating the image file # Creating the image file
process = create_new_image(file_name, file_type, size) process = create_new_image(file_name, file_type, size)
if process["status"]: if process["status"]:
flash(f"Created drive image file: {file_name}.{file_type}") flash(_(u"Image file created: %(file_name)s", file_name=full_file_name))
else: else:
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -392,6 +399,7 @@ def config_load():
flash(process['msg'], "error") flash(process['msg'], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
# The only reason we would reach here would be a Web UI bug. Will not localize.
flash("Got an unhandled request (needs to be either load or delete)", "error") flash("Got an unhandled request (needs to be either load or delete)", "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -422,8 +430,7 @@ def show_logs():
headers = {"content-type": "text/plain"} headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers return process.stdout.decode("utf-8"), int(lines), headers
flash("Failed to get logs") flash(_(u"An error occurred when fetching logs."))
flash(process.stdout.decode("utf-8"), "stdout")
flash(process.stderr.decode("utf-8"), "stderr") flash(process.stderr.decode("utf-8"), "stderr")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -438,10 +445,10 @@ def log_level():
process = set_log_level(level) process = set_log_level(level)
if process["status"]: if process["status"]:
flash(f"Log level set to {level}") flash(_(u"Log level set to %(value)s", value=level))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to set log level to {level}!", "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -456,22 +463,26 @@ def daynaport_attach():
ip_addr = request.form.get("ip") ip_addr = request.form.get("ip")
mask = request.form.get("mask") mask = request.form.get("mask")
error_msg = ("Please follow the instructions at " error_url = "https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link"
"https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link") error_msg = _(u"Please follow the instructions at %(url)s", url=error_url)
if interface.startswith("wlan"): if interface.startswith("wlan"):
if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"): if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
flash("IPv4 forwarding is not enabled. " + error_msg, "error") flash(_(u"Configure IPv4 forwarding before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index")) return redirect(url_for("index"))
if not Path("/etc/iptables/rules.v4").is_file(): if not Path("/etc/iptables/rules.v4").is_file():
flash("NAT has not been configured. " + error_msg, "error") flash(_(u"Configure NAT before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index")) return redirect(url_for("index"))
else: else:
if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + interface + r"$"): if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + interface + r"$"):
flash("The network bridge hasn't been configured. " + error_msg, "error") flash(_(u"Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index")) return redirect(url_for("index"))
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file(): if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
flash("The network bridge hasn't been configured. " + error_msg, "error") flash(_(u"Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index")) return redirect(url_for("index"))
kwargs = {"device_type": "SCDP"} kwargs = {"device_type": "SCDP"}
@ -483,7 +494,7 @@ def daynaport_attach():
process = attach_image(scsi_id, **kwargs) process = attach_image(scsi_id, **kwargs)
if process["status"]: if process["status"]:
flash(f"Attached DaynaPORT to SCSI ID {scsi_id}!") flash(_(u"Attached DaynaPORT to SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(process["msg"], "error") flash(process["msg"], "error")
@ -531,14 +542,17 @@ def attach():
process = attach_image(scsi_id, **kwargs) process = attach_image(scsi_id, **kwargs)
if process["status"]: if process["status"]:
flash(f"Attached {file_name} to SCSI ID {scsi_id} LUN {unit}") flash(_(u"Attached %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit))
if int(file_size) % int(expected_block_size): if int(file_size) % int(expected_block_size):
flash(f"The image file size {file_size} bytes is not a multiple of " flash(_(u"The image file size %(file_size)s bytes is not a multiple of "
f"{expected_block_size} and RaSCSI will ignore the trailing data. " u"%(block_size)s. RaSCSI will ignore the trailing data. "
f"The image may be corrupted so proceed with caution.", "error") u"The image may be corrupted, so proceed with caution.",
file_size=file_size, block_size=expected_block_size), "error")
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to attach {file_name} to SCSI ID {scsi_id} LUN {unit}", "error") flash(_(u"Failed to attach %(file_name)s to SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -551,7 +565,7 @@ def detach_all_devices():
""" """
process = detach_all() process = detach_all()
if process["status"]: if process["status"]:
flash("Detached all SCSI devices") flash(_(u"Detached all SCSI devices"))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(process["msg"], "error") flash(process["msg"], "error")
@ -568,10 +582,12 @@ def detach():
unit = request.form.get("unit") unit = request.form.get("unit")
process = detach_by_id(scsi_id, unit) process = detach_by_id(scsi_id, unit)
if process["status"]: if process["status"]:
flash(f"Detached SCSI ID {scsi_id} LUN {unit}") flash(_(u"Detached SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to detach SCSI ID {scsi_id} LUN {unit}", "error") flash(_(u"Failed to detach %(file_name)s from SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -587,10 +603,12 @@ def eject():
process = eject_by_id(scsi_id, unit) process = eject_by_id(scsi_id, unit)
if process["status"]: if process["status"]:
flash(f"Ejected SCSI ID {scsi_id} LUN {unit}") flash(_(u"Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to eject SCSI ID {scsi_id} LUN {unit}", "error") flash(_(u"Failed to eject %(file_name)s from SCSI ID %(id_number)s LUN %(unit_number)s",
file_name=file_name, id_number=scsi_id, unit_number=unit), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -612,18 +630,19 @@ def device_info():
# the one and only device that should have been returned # the one and only device that should have been returned
device = devices["device_list"][0] device = devices["device_list"][0]
if str(device["id"]) == scsi_id: if str(device["id"]) == scsi_id:
flash("=== DEVICE INFO ===") flash(_(u"DEVICE INFO"))
flash(f"SCSI ID: {device['id']}") flash("===========")
flash(f"LUN: {device['unit']}") flash(_(u"SCSI ID: %(id_number)s", id_number=device["id"]))
flash(f"Type: {device['device_type']}") flash(_(u"LUN: %(unit_number)s", unit_number=device["unit"]))
flash(f"Status: {device['status']}") flash(_(u"Type: %(device_type)s", device_type=device["device_type"]))
flash(f"File: {device['image']}") flash(_(u"Status: %(device_status)s", device_status=device["status"]))
flash(f"Parameters: {device['params']}") flash(_(u"File: %(image_file)s", image_file=device["image"]))
flash(f"Vendor: {device['vendor']}") flash(_(u"Parameters: %(value)s", value=device["params"]))
flash(f"Product: {device['product']}") flash(_(u"Vendor: %(value)s", value=device["vendor"]))
flash(f"Revision: {device['revision']}") flash(_(u"Product: %(value)s", value=device["product"]))
flash(f"Block Size: {device['block_size']} bytes") flash(_(u"Revision: %(revision_number)s", revision_number=device["revision"]))
flash(f"Image Size: {device['size']} bytes") flash(_(u"Block Size: %(value)s bytes", value=device["block_size"]))
flash(_(u"Image Size: %(value)s bytes", value=device["size"]))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(devices["msg"], "error") flash(devices["msg"], "error")
@ -642,9 +661,10 @@ def reserve_id():
process = reserve_scsi_ids(reserved_ids) process = reserve_scsi_ids(reserved_ids)
if process["status"]: if process["status"]:
RESERVATIONS[int(scsi_id)] = memo RESERVATIONS[int(scsi_id)] = memo
flash(f"Reserved SCSI ID {scsi_id}") flash(_(u"Reserved SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(_(u"Failed to reserve SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -660,9 +680,10 @@ def unreserve_id():
process = reserve_scsi_ids(reserved_ids) process = reserve_scsi_ids(reserved_ids)
if process["status"]: if process["status"]:
RESERVATIONS[int(scsi_id)] = "" RESERVATIONS[int(scsi_id)] = ""
flash(f"Released the reservation for SCSI ID {scsi_id}") flash(_(u"Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(_(u"Failed to release the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -700,18 +721,19 @@ def download_to_iso():
process = download_file_to_iso(url, *iso_args) process = download_file_to_iso(url, *iso_args)
if process["status"]: if process["status"]:
flash(process["msg"]) flash(process["msg"])
flash(f"Saved image as: {process['file_name']}") flash(_(u"Saved image as: %(file_name)s", file_name=process['file_name']))
else: else:
flash(f"Failed to create CD-ROM image from {url}", "error") flash(_(u"Failed to create CD-ROM image from %(url)s", url), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
process_attach = attach_image(scsi_id, device_type="SCCD", image=process["file_name"]) process_attach = attach_image(scsi_id, device_type="SCCD", image=process["file_name"])
if process_attach["status"]: if process_attach["status"]:
flash(f"Attached to SCSI ID {scsi_id}") flash(_(u"Attached to SCSI ID %(id_number)s", id_number=scsi_id))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to attach image to SCSI ID {scsi_id}. Try attaching it manually.", "error") flash(_(u"Failed to attach image to SCSI ID %(id_number)s. Try attaching it manually.",
id_number=scsi_id), "error")
flash(process_attach["msg"], "error") flash(process_attach["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -729,7 +751,7 @@ def download_img():
flash(process["msg"]) flash(process["msg"])
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to download file {url}", "error") flash(_(u"Failed to download file from %(url)s", url), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -746,7 +768,7 @@ def download_afp():
flash(process["msg"]) flash(process["msg"])
return redirect(url_for("index")) return redirect(url_for("index"))
flash(f"Failed to download file {url}", "error") flash(_(u"Failed to download file from %(url)s", url), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -766,26 +788,26 @@ def upload_file():
from os import path from os import path
log = logging.getLogger("pydrop") log = logging.getLogger("pydrop")
file = request.files["file"] file_object = request.files["file"]
filename = secure_filename(file.filename) file_name = secure_filename(file_object.filename)
server_info = get_server_info() server_info = get_server_info()
save_path = path.join(server_info["image_dir"], filename) save_path = path.join(server_info["image_dir"], file_name)
current_chunk = int(request.form['dzchunkindex']) current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file, # Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress # but continues writing to a file transfer in progress
if path.exists(save_path) and current_chunk == 0: if path.exists(save_path) and current_chunk == 0:
return make_response((f"The file {file.filename} already exists!", 400)) return make_response(_(u"The file already exists!"), 400)
try: try:
with open(save_path, "ab") as save: with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"])) save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file.stream.read()) save.write(file_object.stream.read())
except OSError: except OSError:
log.exception("Could not write to file") log.exception("Could not write to file")
return make_response(("Unable to write the file to disk!", 500)) return make_response(_(u"Unable to write the file to disk!"), 500)
total_chunks = int(request.form["dztotalchunkcount"]) total_chunks = int(request.form["dztotalchunkcount"])
@ -795,14 +817,14 @@ def upload_file():
log.error("Finished transferring %s, " log.error("Finished transferring %s, "
"but it has a size mismatch with the original file." "but it has a size mismatch with the original file."
"Got %s but we expected %s.", "Got %s but we expected %s.",
file.filename, path.getsize(save_path), request.form['dztotalfilesize']) file_object.filename, path.getsize(save_path), request.form['dztotalfilesize'])
return make_response(("Transferred file corrupted!", 500)) return make_response(_(u"Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file.filename) log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.", log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file.filename) current_chunk + 1, total_chunks, file_object.filename)
return make_response(("File upload successful!", 200)) return make_response(_(u"File upload successful!"), 200)
@APP.route("/files/create", methods=["POST"]) @APP.route("/files/create", methods=["POST"])
@ -814,10 +836,11 @@ def create_file():
file_name = request.form.get("file_name") file_name = request.form.get("file_name")
size = (int(request.form.get("size")) * 1024 * 1024) size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type") file_type = request.form.get("type")
full_file_name = file_name + "." + file_type
process = create_new_image(file_name, file_type, size) process = create_new_image(file_name, file_type, size)
if process["status"]: if process["status"]:
flash(f"Drive image created: {file_name}.{file_type}") flash(_(u"Image file created: %(file_name)s", file_name=full_file_name))
return redirect(url_for("index")) return redirect(url_for("index"))
flash(process["msg"], "error") flash(process["msg"], "error")
@ -844,7 +867,7 @@ def delete():
process = delete_image(file_name) process = delete_image(file_name)
if process["status"]: if process["status"]:
flash(f"Image file deleted: {file_name}") flash(_(u"Image file deleted: %(file_name)s", file_name=file_name))
else: else:
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -874,7 +897,7 @@ def rename():
process = rename_image(file_name, new_file_name) process = rename_image(file_name, new_file_name)
if process["status"]: if process["status"]:
flash(f"Image file renamed to: {new_file_name}") flash(_(u"Image file renamed to: %(file_name)s", file_name=new_file_name))
else: else:
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -911,16 +934,16 @@ def unzip():
process = unzip_file(zip_file, zip_member, zip_members) process = unzip_file(zip_file, zip_member, zip_members)
if process["status"]: if process["status"]:
if not process["msg"]: if not process["msg"]:
flash("Aborted unzip: File(s) with the same name already exists.", "error") flash(_(u"Aborted unzip: File(s) with the same name already exists."), "error")
return redirect(url_for("index")) return redirect(url_for("index"))
flash("Unzipped the following files:") flash(_(u"Unzipped the following files:"))
for msg in process["msg"]: for msg in process["msg"]:
flash(msg) flash(msg)
if process["prop_flag"]: if process["prop_flag"]:
flash(f"Properties file(s) have been moved to {CFG_DIR}") flash(_(u"Properties file(s) have been moved to %(directory)s", directory=CFG_DIR))
return redirect(url_for("index")) return redirect(url_for("index"))
flash("Failed to unzip " + zip_file, "error") flash(_(u"Failed to unzip %(zip_file)s", zip_file=zip_file), "error")
flash(process["msg"], "error") flash(process["msg"], "error")
return redirect(url_for("index")) return redirect(url_for("index"))
@ -928,8 +951,12 @@ def unzip():
@APP.before_first_request @APP.before_first_request
def load_default_config(): def load_default_config():
""" """
Load the default configuration file, if found Webapp initialization steps that require the Flask app to have started:
- Get the detected locale to use for localizations
- Load the default configuration file, if found
""" """
APP.config["LOCALE"] = get_locale()
logging.info("Detected locale: " + APP.config["LOCALE"])
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file(): if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
read_config(DEFAULT_CONFIG) read_config(DEFAULT_CONFIG)
@ -939,7 +966,7 @@ if __name__ == "__main__":
APP.config["SESSION_TYPE"] = "filesystem" APP.config["SESSION_TYPE"] = "filesystem"
APP.config["MAX_CONTENT_LENGTH"] = int(MAX_FILE_SIZE) APP.config["MAX_CONTENT_LENGTH"] = int(MAX_FILE_SIZE)
parser = argparse.ArgumentParser(description="RaSCSI Web Interface arguments") parser = argparse.ArgumentParser(description="RaSCSI Web Interface command line arguments")
parser.add_argument( parser.add_argument(
"--port", "--port",
type=int, type=int,