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>