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/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 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
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),
}

View File

@ -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": ""}

View File

@ -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()

View File

@ -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

View File

@ -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."
)
)

View File

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

View File

@ -26,12 +26,12 @@
<script type="application/javascript">
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);
}
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);
}
</script>
@ -45,19 +45,19 @@
<div class="header">
{% if auth_active %}
{% 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 %}
<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">
<div>Log In to Use Web Interface</div>
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="{{ _("Password") }}">
<input type="submit" value="Login">
</form>
</span>
{% endif %}
{% 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 %}
<table width="100%">
<tbody>
@ -84,8 +84,8 @@
{% block content %}{% endblock content %}
</div>
<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>Pi environment: {{ running_env["env"] }}</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>
</div>
</div>
</body>

View File

@ -1,19 +1,19 @@
{% extends "base.html" %}
{% block content %}
<p><a href="/">Cancel</a></p>
<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>
<h2>Hard Drives</h2>
<p><a href="/">{{ _("Cancel") }}</a></p>
<h2>{{ _("Disclaimer") }}</h2>
<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>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for hd in hd_conf %}
<tr>
@ -22,7 +22,7 @@
<td style="text-align:left">{{ hd.description }}</td>
<td style="text-align:left">
{% if hd.url != "" %}
<a href="{{ hd.url }}">Link</a>
<a href="{{ hd.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
@ -36,9 +36,9 @@
<input type="hidden" name="block_size" value="{{ hd.block_size }}">
<input type="hidden" name="size" value="{{ hd.size }}">
<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="submit" value="Create" />
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
</tr>
@ -48,16 +48,16 @@
<hr/>
<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>
<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>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for cd in cd_conf %}
<tr>
@ -66,7 +66,7 @@
<td style="text-align:left">{{ cd.description }}</td>
<td style="text-align:left">
{% if cd.url != "" %}
<a href="{{ cd.url }}">Link</a>
<a href="{{ cd.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
@ -77,7 +77,7 @@
<input type="hidden" name="product" value="{{ cd.product }}">
<input type="hidden" name="revision" value="{{ cd.revision }}">
<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">
{% for f in files %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
@ -85,7 +85,7 @@
{% endif %}
{% endfor %}
</select>
<input type="submit" value="Create" />
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
</tr>
@ -95,15 +95,15 @@
<hr/>
<h2>Removable Drives</h2>
<h2>{{ _("Removable Drives") }}</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
<td><b>{{ _("Name") }}</b></td>
<td><b>{{ _("Size (MB)") }}</b></td>
<td><b>{{ _("Description") }}</b></td>
<td><b>{{ _("Ref.") }}</b></td>
<td><b>{{ _("Action") }}</b></td>
</tr>
{% for rm in rm_conf %}
<tr>
@ -112,7 +112,7 @@
<td style="text-align:left">{{ rm.description }}</td>
<td style="text-align:left">
{% if rm.url != "" %}
<a href="{{ rm.url }}">Link</a>
<a href="{{ rm.url }}">{{ _("Link") }}</a>
{% else %}
-
{% endif %}
@ -126,16 +126,16 @@
<input type="hidden" name="block_size" value="{{ rm.block_size }}">
<input type="hidden" name="size" value="{{ rm.size }}">
<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="submit" value="Create" />
<input type="submit" value="{{ _("Create") }}" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>Available disk space on the Pi: {{ free_disk }} MB</small></p>
<p><a href="/">Cancel</a></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>
{% endblock content %}

View File

@ -3,12 +3,12 @@
<details>
<summary class="heading">
Current RaSCSI Configuration
{{ _("Current RaSCSI Configuration") }}
</summary>
<ul>
<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>The <em>default</em> configuration will be loaded when the Web UI starts up, if available.</li>
<li>{{ _("Displays the currently attached devices for each available SCSI ID.") }}</li>
<li>{{ _("Save and load device configurations, stored as json files in <tt>%(config_dir)s</tt>", config_dir=CFG_DIR) }}</tt></li>
<li>{{ _("To have a particular device configuration load when RaSCSI starts, save it as <em>default</em>.") }}</li>
</ul>
</details>
@ -22,31 +22,31 @@
{% endfor %}
{% else %}
<option disabled>
No saved configs
{{ _("No saved configurations") }}
</option>
{% endif %}
</select>
<input name="load" type="submit" value="Load" onclick="return confirm('Detach all current device and Load config?')">
<input name="delete" type="submit" value="Delete" onclick="return confirm('Delete config file?')">
<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 configuration file?") }}')">
</form></p>
<p><form action="/config/save" method="post">
<input name="name" placeholder="default" size="20">
<input type="submit" value="Save">
<input type="submit" value="{{ _("Save") }}">
</form></p>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>ID</b></td>
<td><b>{{ _("ID") }}</b></td>
{% if units %}
<td><b>LUN</b></td>
<td><b>{{ _("LUN") }}</b></td>
{% endif %}
<td><b>Type</b></td>
<td><b>Status</b></td>
<td><b>File</b></td>
<td><b>Product</b></td>
<td><b>Actions</b></td>
<td><b>{{ _("Type") }}</b></td>
<td><b>{{ _("Status") }}</b></td>
<td><b>{{ _("File") }}</b></td>
<td><b>{{ _("Product") }}</b></td>
<td><b>{{ _("Actions") }}</b></td>
</tr>
{% for device in devices %}
<tr>
@ -81,7 +81,7 @@
{% endif %}
{% endfor %}
</select>
<input type="submit" value="Attach">
<input type="submit" value="{{ _("Attach") }}">
</form>
{% else %}
{{ device.file }}
@ -95,28 +95,28 @@
<td style="text-align:center">
{% if device.device_type != "-" %}
{% 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="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Eject">
<input type="submit" value="{{ _("Eject") }}">
</form>
{% 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="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Detach">
<input type="submit" value="{{ _("Detach") }}">
</form>
{% endif %}
<form action="/scsi/info" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.unit }}">
<input type="submit" value="Info">
<input type="submit" value="{{ _("Info") }}">
</form>
{% 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="memo" id="memo_{{ device.id }}" type="hidden" value="">
<input type="submit" value="Reserve">
<input type="submit" value="{{ _("Reserve") }}">
</form>
{% endif %}
</td>
@ -126,13 +126,13 @@
<td class="inactive"></td>
{% endif %}
<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"></td>
<td class="inactive">
<form action="/scsi/unreserve" method="post">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input type="submit" value="Unreserve">
<input type="submit" value="{{ _("Unreserve") }}">
</form>
</td>
{% endif %}
@ -141,31 +141,31 @@
</tbody>
</table>
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('Detach all SCSI Devices?')">
<input type="submit" value="Detach All Devices">
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('{{ _("Detach all SCSI Devices?") }}')">
<input type="submit" value="{{ _("Detach All Devices") }}">
</form></p>
<hr/>
<details>
<summary class="heading">
Image File Management
{{ _("Image File Management") }}
</summary>
<ul>
<li>Manage image files in the active RaSCSI image directory: <tt>{{ base_dir }}</tt> with a scan depth of {{ 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>{{ _("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=\"%(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>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>{{ _("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>
</ul>
</details>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>File</b></td>
<td><b>Size</b></td>
<td><b>Actions</b></td>
<td><b>{{ _("File") }}</b></td>
<td><b>{{ _("Size") }}</b></td>
<td><b>{{ _("Actions") }}</b></td>
</tr>
{% for file in files %}
<tr>
@ -181,7 +181,7 @@
{% endfor %}
<form action="/files/download" method="post">
<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>
</ul>
</details>
@ -201,7 +201,7 @@
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<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>
</summary>
<ul style="list-style: none;">
@ -215,7 +215,7 @@
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<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>
{% endif %}
</li>
@ -230,26 +230,26 @@
<td style="text-align:center">
<form action="/files/download" method="post">
<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>
</td>
<td>
{% if file["name"] in attached_images %}
<center>
Attached!
{{ _("Attached!") }}
</center>
{% else %}
{% if file["name"].lower().endswith(ARCHIVE_FILE_SUFFIX) %}
<form action="/files/unzip" method="post">
<input name="zip_file" type="hidden" value="{{ file['name'] }}">
<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>
{% else %}
<form action="/scsi/attach" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="file_size" type="hidden" value="{{ file['size'] }}">
<label for="id">ID</label>
<label for="id">{{ _("ID") }}</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>
@ -257,7 +257,7 @@
</option>
{% endfor %}
</select>
<label for="unit">LUN</label>
<label for="unit">{{ _("LUN") }}</label>
<input name="unit" type="number" size="2" value="0" min="0" max="31">
{% if file["detected_type"] != "UNDEFINED" %}
<input name="type" type="hidden" value="{{ file['detected_type'] }}">
@ -265,7 +265,7 @@
{% else %}
<select name="type">
<option selected value="">
Type
{{ _("Type") }}
</option>
{% for d in device_types %}
<option value="{{ d }}">
@ -274,17 +274,17 @@
{% endfor %}
{% endif %}
</select>
<input type="submit" value="Attach">
<input type="submit" value="{{ _("Attach") }}">
{% endif %}
</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="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value="">
<input type="submit" value="Rename">
<input type="submit" value="{{ _("Rename") }}">
</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 type="submit" value="Delete">
<input type="submit" value="{{ _("Delete") }}">
</form>
{% endif %}
</td>
@ -292,22 +292,22 @@
{% endfor %}
</tbody>
</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/>
<details>
<summary class="heading">
Attach Ethernet Adapter
{{ _("Attach Ethernet Adapter") }}
</summary>
<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>If you have a DHCP setup, choose only the interface, and 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>{{ _("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 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 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>
</ul>
</details>
@ -315,7 +315,7 @@
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/daynaport/attach" method="post">
<label for="if">Interface:</label>
<label for="if">{{ _("Interface:") }}</label>
<select name="if">
{% for if in netinfo["ifs"] %}
<option value="{{ if }}">
@ -323,10 +323,10 @@
</option>
{% endfor %}
</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="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">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
@ -334,26 +334,26 @@
</option>
{% endfor %}
</select>
<input type="submit" value="Attach">
<input type="submit" value="{{ _("Attach") }}">
</form>
</td>
</tr>
</table>
{% 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 %}
<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 %}
<hr/>
<details>
<summary class="heading">
Upload File
{{ _("Upload File") }}
</summary>
<ul>
<li>Uploads file to <tt>{{ base_dir }}</tt>. The largest file size accepted is {{ max_file_size }} MB.</li>
<li>For unrecognized file types, try renaming hard drive images to '.hds' and CD-ROM images to '.iso' before uploading.</li>
<li>Recognized file types: {{ valid_file_suffix }}</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', CD-ROM images to '.iso', and removable drive images to '.hdr' before uploading.") }}</li>
<li>{{ _("Recognized file types: %(valid_file_suffix)s", valid_file_suffix=valid_file_suffix) }}</li>
</ul>
</details>
@ -380,10 +380,10 @@
<details>
<summary class="heading">
Download File to Images
{{ _("Download File to Images") }}
</summary>
<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>
</details>
@ -391,9 +391,9 @@
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post">
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<input type="submit" value="Download" onclick="processNotify('Downloading File to Images...')">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to Images...") }}')">
</form>
</td>
</tr>
@ -403,13 +403,12 @@
<details>
<summary class="heading">
Download File to AppleShare
{{ _("Download File to AppleShare") }}
</summary>
<ul>
<li>Given a URL, download that file to the <tt>{{ AFP_DIR }}</tt> directory and share it over AFP.</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>
<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>{{ _("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>
</ul>
</details>
@ -418,43 +417,42 @@
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post">
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<input type="submit" value="Download" onclick="processNotify('Downloading File to AppleShare...')">
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File to AppleShare...") }}')">
</form>
</td>
</tr>
</table>
{% 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 %}
<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 %}
<p><small>{{ netatalk_configured - 1 }} active AFP connections</small></p>
<p><small>{{ _("%(value)d active AFP connections", value=(netatalk_configured - 1)) }}</small></p>
{% endif %}
{% 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 %}
<hr/>
<details>
<summary class="heading">
Download File and Create CD-ROM ISO image
{{ _("Download File and Create CD-ROM image") }}
</summary>
<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>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>
<li>If the target file is a zip archive, we will attempt to unzip it and store the resulting files only.</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>{{ _("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>{{ _("If the downloaded file is a zip archive, we will attempt to unzip it and store the resulting files.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<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">
<select name="scsi_id">
{% for id in scsi_ids %}
@ -463,9 +461,9 @@
</option>
{% endfor %}
</select>
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<label for="type">Type:</label>
<label for="url">{{ _("URL:") }}</label>
<input name="url" placeholder="{{ _("URL") }}" required="" type="url">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="-hfs">
HFS
@ -486,7 +484,7 @@
Rock Ridge
</option>
</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>
</td>
</tr>
@ -496,41 +494,41 @@
<details>
<summary class="heading">
Create Empty Disk Image File
{{ _("Create Empty Disk Image File") }}
</summary>
<ul>
<li>The Generic image type is recommended for most systems</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>SASI images should only be used on early X68000 or UNIX workstation systems that use this pre-SCSI standard.</li>
<li>{{ _("The Generic image type is recommended for most computer platforms.") }}</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 the original Sharp X68000, or other legacy systems that utilize this pre-SCSI standard.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/create" method="post">
<label for="file_name">File Name:</label>
<input name="file_name" placeholder="File name" required="" type="text">
<label for="type">Type:</label>
<label for="file_name">{{ _("File Name:") }}</label>
<input name="file_name" placeholder="{{ _("File Name") }}" required="" type="text">
<label for="type">{{ _("Type:") }}</label>
<select name="type">
<option value="hds">
SCSI Hard Disk image (Generic) [.hds]
{{ _("SCSI Hard Disk image (Generic) [.hds]") }}
</option>
<option value="hda">
SCSI Hard Disk image (APPLE GENUINE - use with Mac) [.hda]
{{ _("SCSI Hard Disk image (APPLE GENUINE) [.hda]") }}
</option>
<option value="hdn">
SCSI Hard Disk image (NEC GENUINE - use with PC98) [.hdn]
{{ _("SCSI Hard Disk image (NEC GENUINE) [.hdn]") }}
</option>
<option value="hdr">
SCSI Removable Media Disk image (Generic) [.hdr]
{{ _("SCSI Removable Media Disk image (Generic) [.hdr]") }}
</option>
<option value="hdf">
SASI Hard Disk image (use with X68000) [.hdf]
{{ _("SASI Hard Disk image (Legacy) [.hdf]") }}
</option>
</select>
<label for="size">Size:</label>
<input name="size" type="number" placeholder="MB" min="1" size="6" required>
<input type="submit" value="Create">
<label for="size">{{ _("Size:") }}</label>
<input name="size" type="number" placeholder="{{ _("MB") }}" min="1" size="6" required>
<input type="submit" value="{{ _("Create") }}">
</form>
</td>
</tr>
@ -540,32 +538,32 @@
<details>
<summary class="heading">
Create Named Drive
{{ _("Create Named Drive") }}
</summary>
<ul>
<li>Here you can 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>{{ _("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>
</ul>
</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/>
<details>
<summary class="heading">
Logging
{{ _("Logging") }}
</summary>
<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>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<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">
<label for="scope">Scope:</label>
<label for="scope">{{ _("Scope:") }}</label>
<select name="scope">
<option value="default">
default
@ -577,7 +575,7 @@
rascsi-web.service
</option>
</select>
<input type="submit" value="Show Logs">
<input type="submit" value="{{ _("Show Logs") }}">
</form>
</td>
</tr>
@ -587,18 +585,18 @@
<details>
<summary class="heading">
Server Log Level
{{ _("Server Log Level") }}
</summary>
<ul>
<li>Change the log level of the RaSCSI backend service.</li>
<li>The dropdown will indicate the current log level.</li>
<li>{{ _("Change the log level of the RaSCSI backend process.") }}</li>
<li>{{ _("The current dropdown selection indicates the active log level.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post">
<label for="level">Log Level:</label>
<label for="level">{{ _("Log Level:") }}</label>
<select name="level">
{% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
@ -606,7 +604,7 @@
</option>
{% endfor %}
</select>
<input type="submit" value="Set Log Level">
<input type="submit" value="{{ _("Set Log Level") }}">
</form>
</td>
</tr>
@ -616,23 +614,23 @@
<details>
<summary class="heading">
Raspberry Pi Operations
{{ _("Raspberry Pi Operations") }}
</summary>
<ul>
<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>{{ _("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 loss.") }}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<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();">
<input type="submit" value="Reboot Raspberry Pi">
<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") }}">
</form>
</td>
<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();">
<input type="submit" value="Shut Down Raspberry Pi">
<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") }}">
</form>
</td>
</tr>

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ from flask import (
session,
abort,
)
from flask_babel import Babel, _
from file_cmds import (
list_images,
@ -81,6 +82,11 @@ from settings import (
)
APP = Flask(__name__)
BABEL = Babel(APP)
@BABEL.localeselector
def get_locale():
return request.accept_languages.best_match(["en", "de", "sv"])
@APP.route("/")
def index():
@ -88,7 +94,7 @@ def index():
Sets up data structures for and renders the index page
"""
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()
disk = disk_space()
@ -182,7 +188,7 @@ def drive_list():
return redirect(url_for("index"))
conf = process["conf"]
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"))
hd_conf = []
@ -246,7 +252,7 @@ def login():
if authenticate(str(username), str(password)):
session["username"] = request.form["username"]
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"))
@ -294,11 +300,12 @@ def drive_create():
size = request.form.get("size")
file_type = request.form.get("file_type")
file_name = request.form.get("file_name")
full_file_name = file_name + "." + file_type
# Creating the image file
process = create_new_image(file_name, file_type, size)
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:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -392,6 +399,7 @@ def config_load():
flash(process['msg'], "error")
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")
return redirect(url_for("index"))
@ -422,8 +430,7 @@ def show_logs():
headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers
flash("Failed to get logs")
flash(process.stdout.decode("utf-8"), "stdout")
flash(_(u"An error occurred when fetching logs."))
flash(process.stderr.decode("utf-8"), "stderr")
return redirect(url_for("index"))
@ -438,10 +445,10 @@ def log_level():
process = set_log_level(level)
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"))
flash(f"Failed to set log level to {level}!", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -456,22 +463,26 @@ def daynaport_attach():
ip_addr = request.form.get("ip")
mask = request.form.get("mask")
error_msg = ("Please follow the instructions at "
"https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link")
error_url = "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 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"))
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"))
else:
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"))
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"))
kwargs = {"device_type": "SCDP"}
@ -483,7 +494,7 @@ def daynaport_attach():
process = attach_image(scsi_id, **kwargs)
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"))
flash(process["msg"], "error")
@ -531,14 +542,17 @@ def attach():
process = attach_image(scsi_id, **kwargs)
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):
flash(f"The image file size {file_size} bytes is not a multiple of "
f"{expected_block_size} and RaSCSI will ignore the trailing data. "
f"The image may be corrupted so proceed with caution.", "error")
flash(_(u"The image file size %(file_size)s bytes is not a multiple of "
u"%(block_size)s. RaSCSI will ignore the trailing data. "
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"))
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")
return redirect(url_for("index"))
@ -551,7 +565,7 @@ def detach_all_devices():
"""
process = detach_all()
if process["status"]:
flash("Detached all SCSI devices")
flash(_(u"Detached all SCSI devices"))
return redirect(url_for("index"))
flash(process["msg"], "error")
@ -568,10 +582,12 @@ def detach():
unit = request.form.get("unit")
process = detach_by_id(scsi_id, unit)
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"))
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")
return redirect(url_for("index"))
@ -587,10 +603,12 @@ def eject():
process = eject_by_id(scsi_id, unit)
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"))
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")
return redirect(url_for("index"))
@ -612,18 +630,19 @@ def device_info():
# the one and only device that should have been returned
device = devices["device_list"][0]
if str(device["id"]) == scsi_id:
flash("=== DEVICE INFO ===")
flash(f"SCSI ID: {device['id']}")
flash(f"LUN: {device['unit']}")
flash(f"Type: {device['device_type']}")
flash(f"Status: {device['status']}")
flash(f"File: {device['image']}")
flash(f"Parameters: {device['params']}")
flash(f"Vendor: {device['vendor']}")
flash(f"Product: {device['product']}")
flash(f"Revision: {device['revision']}")
flash(f"Block Size: {device['block_size']} bytes")
flash(f"Image Size: {device['size']} bytes")
flash(_(u"DEVICE INFO"))
flash("===========")
flash(_(u"SCSI ID: %(id_number)s", id_number=device["id"]))
flash(_(u"LUN: %(unit_number)s", unit_number=device["unit"]))
flash(_(u"Type: %(device_type)s", device_type=device["device_type"]))
flash(_(u"Status: %(device_status)s", device_status=device["status"]))
flash(_(u"File: %(image_file)s", image_file=device["image"]))
flash(_(u"Parameters: %(value)s", value=device["params"]))
flash(_(u"Vendor: %(value)s", value=device["vendor"]))
flash(_(u"Product: %(value)s", value=device["product"]))
flash(_(u"Revision: %(revision_number)s", revision_number=device["revision"]))
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"))
flash(devices["msg"], "error")
@ -642,9 +661,10 @@ def reserve_id():
process = reserve_scsi_ids(reserved_ids)
if process["status"]:
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"))
flash(_(u"Failed to reserve SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -660,9 +680,10 @@ def unreserve_id():
process = reserve_scsi_ids(reserved_ids)
if process["status"]:
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"))
flash(_(u"Failed to release the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -700,18 +721,19 @@ def download_to_iso():
process = download_file_to_iso(url, *iso_args)
if process["status"]:
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:
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")
return redirect(url_for("index"))
process_attach = attach_image(scsi_id, device_type="SCCD", image=process["file_name"])
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"))
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")
return redirect(url_for("index"))
@ -729,7 +751,7 @@ def download_img():
flash(process["msg"])
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")
return redirect(url_for("index"))
@ -746,7 +768,7 @@ def download_afp():
flash(process["msg"])
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")
return redirect(url_for("index"))
@ -766,26 +788,26 @@ def upload_file():
from os import path
log = logging.getLogger("pydrop")
file = request.files["file"]
filename = secure_filename(file.filename)
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
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'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
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:
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file.stream.read())
save.write(file_object.stream.read())
except OSError:
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"])
@ -795,14 +817,14 @@ def upload_file():
log.error("Finished transferring %s, "
"but it has a size mismatch with the original file."
"Got %s but we expected %s.",
file.filename, path.getsize(save_path), request.form['dztotalfilesize'])
return make_response(("Transferred file corrupted!", 500))
file_object.filename, path.getsize(save_path), request.form['dztotalfilesize'])
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.",
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"])
@ -814,10 +836,11 @@ def create_file():
file_name = request.form.get("file_name")
size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type")
full_file_name = file_name + "." + file_type
process = create_new_image(file_name, file_type, size)
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"))
flash(process["msg"], "error")
@ -844,7 +867,7 @@ def delete():
process = delete_image(file_name)
if process["status"]:
flash(f"Image file deleted: {file_name}")
flash(_(u"Image file deleted: %(file_name)s", file_name=file_name))
else:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -874,7 +897,7 @@ def rename():
process = rename_image(file_name, new_file_name)
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:
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -911,16 +934,16 @@ def unzip():
process = unzip_file(zip_file, zip_member, zip_members)
if process["status"]:
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"))
flash("Unzipped the following files:")
flash(_(u"Unzipped the following files:"))
for msg in process["msg"]:
flash(msg)
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"))
flash("Failed to unzip " + zip_file, "error")
flash(_(u"Failed to unzip %(zip_file)s", zip_file=zip_file), "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@ -928,8 +951,12 @@ def unzip():
@APP.before_first_request
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():
read_config(DEFAULT_CONFIG)
@ -939,7 +966,7 @@ if __name__ == "__main__":
APP.config["SESSION_TYPE"] = "filesystem"
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(
"--port",
type=int,