mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
fix(locale): the VM page's and Settings page's messages show translated
The VM page's and the Settings page's views marked their messages with
gettext_noop, so every message they gave (a refused resize, a saved
setting, a QoS change, a busy VM) showed in English whatever the
language, though the catalogs translate them. They translate in the
request's language now; their module-level texts lazily. The Settings
page's group titles stay English in the view, as the page links them
by their slugs and translates them itself.
Two values went into translated messages as English codes: a job's
state ("... is running on this resource") and a QoS direction ("Remove
the inbound limit ..."). They are named as the pages name them.
This commit is contained in:
parent
763f1525ca
commit
9d57adc097
10 changed files with 412 additions and 380 deletions
|
|
@ -30,6 +30,12 @@ class AppSettingsTestCase(TestCase):
|
|||
choice = choice[:choice.index("</select>")]
|
||||
self.assertEqual(re.findall(r'<option value="([^"]+)"', choice), ["en", "tr", "zh-hans"])
|
||||
|
||||
def test_the_page_answers_in_the_users_language(self):
|
||||
response = self.client.post(reverse("appsettings"), {}, HTTP_ACCEPT_LANGUAGE="tr", follow=True)
|
||||
self.assertEqual([str(m) for m in get_messages(response.wsgi_request)], ["Hiçbir şey değişmedi"])
|
||||
# the groups' links stay the English slugs: the page translates only their titles
|
||||
self.assertContains(response, 'href="#new-vm-defaults"')
|
||||
|
||||
def test_appsettings_update_setting(self):
|
||||
setting = AppSettings.objects.get(key="SHOW_ACCESS_ROOT_PASSWORD")
|
||||
original_value = setting.value
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from django.contrib import messages
|
|||
from django.db import DatabaseError, transaction
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_noop as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from logs.audit import audit
|
||||
from operations.utils import BOUNDS
|
||||
|
||||
|
|
@ -30,23 +31,24 @@ NUMBERS = {
|
|||
}
|
||||
|
||||
|
||||
# the Settings page's groups, in order; a setting in none shows under Other
|
||||
# the Settings page's groups, in order; a setting in none shows under Other. The
|
||||
# titles stay English here: the page links them by slug and translates them
|
||||
GROUPS = [
|
||||
(_("New VM defaults"), ["INSTANCE_ARCH_DEFAULT_TYPE", "INSTANCE_MACHINE_DEFAULT_TYPE",
|
||||
(gettext_noop("New VM defaults"), ["INSTANCE_ARCH_DEFAULT_TYPE", "INSTANCE_MACHINE_DEFAULT_TYPE",
|
||||
"INSTANCE_FIRMWARE_DEFAULT_TYPE", "INSTANCE_CPU_DEFAULT_MODE", "INSTANCE_NIC_DEFAULT_TYPE",
|
||||
"INSTANCE_VIDEO_DEFAULT_TYPE", "INSTANCE_INPUT_DEFAULT_DEVICE", "INSTANCE_CDROM_ADD"]),
|
||||
(_("Disks"), ["INSTANCE_VOLUME_DEFAULT_BUS", "INSTANCE_VOLUME_DEFAULT_FORMAT", "INSTANCE_VOLUME_DEFAULT_CACHE",
|
||||
(gettext_noop("Disks"), ["INSTANCE_VOLUME_DEFAULT_BUS", "INSTANCE_VOLUME_DEFAULT_FORMAT", "INSTANCE_VOLUME_DEFAULT_CACHE",
|
||||
"INSTANCE_VOLUME_DEFAULT_IO", "INSTANCE_VOLUME_DEFAULT_DISCARD",
|
||||
"INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES", "INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER",
|
||||
"INSTANCE_VOLUME_DEFAULT_OWNER_UID", "INSTANCE_VOLUME_DEFAULT_OWNER_GID"]),
|
||||
(_("Clones"), ["CLONE_INSTANCE_AUTO_NAME", "CLONE_INSTANCE_DEFAULT_PREFIX"]),
|
||||
(_("Console"), ["CONSOLE_SCALE", "CONSOLE_VIEW_ONLY", "CONSOLE_RESIZE_SESSION", "CONSOLE_CLIP_VIEWPORT"]),
|
||||
(_("VM access and owners"), ["SHOW_ACCESS_ROOT_PASSWORD", "SHOW_ACCESS_SSH_KEYS",
|
||||
(gettext_noop("Clones"), ["CLONE_INSTANCE_AUTO_NAME", "CLONE_INSTANCE_DEFAULT_PREFIX"]),
|
||||
(gettext_noop("Console"), ["CONSOLE_SCALE", "CONSOLE_VIEW_ONLY", "CONSOLE_RESIZE_SESSION", "CONSOLE_CLIP_VIEWPORT"]),
|
||||
(gettext_noop("VM access and owners"), ["SHOW_ACCESS_ROOT_PASSWORD", "SHOW_ACCESS_SSH_KEYS",
|
||||
"ALLOW_INSTANCE_MULTIPLE_OWNER", "QUOTA_DEBUG"]),
|
||||
(_("Snapshots and jobs"), ["SNAPSHOT_FREE_SPACE_PERCENT", "SNAPSHOT_FREE_SPACE_MIN_GIB",
|
||||
(gettext_noop("Snapshots and jobs"), ["SNAPSHOT_FREE_SPACE_PERCENT", "SNAPSHOT_FREE_SPACE_MIN_GIB",
|
||||
"WORKER_CONCURRENCY", "JOB_RETENTION_DAYS"]),
|
||||
(_("Login"), ["LOGIN_FAILURE_LIMIT", "LOGIN_LOCKOUT_MINUTES"]),
|
||||
(_("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VM_DRBD_STATUS", "LOGS_PER_PAGE"]),
|
||||
(gettext_noop("Login"), ["LOGIN_FAILURE_LIMIT", "LOGIN_LOCKOUT_MINUTES"]),
|
||||
(gettext_noop("Lists and pages"), ["VIEW_INSTANCES_LIST_STYLE", "VM_DRBD_STATUS", "LOGS_PER_PAGE"]),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -117,7 +119,7 @@ def appsettings(request):
|
|||
for title, keys in GROUPS:
|
||||
groups.append((title, [settings_by_key.pop(key) for key in keys if key in settings_by_key]))
|
||||
if settings_by_key:
|
||||
groups.append((_("Other"), sorted(settings_by_key.values(), key=lambda s: s.name)))
|
||||
groups.append((gettext_noop("Other"), sorted(settings_by_key.values(), key=lambda s: s.name)))
|
||||
for _title, settings_list in groups:
|
||||
for setting in settings_list:
|
||||
setting.bounds = NUMBERS.get(setting.key)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% load i18n %}
|
||||
{# one QoS limit of a NIC: its values to change, or to delete #}
|
||||
<form class="mt-2" style="max-width: 32rem" action="{% url 'instances:set_qos' instance.id %}" method="post" aria-label="{% blocktrans %}{{ direction }} QoS form{% endblocktrans %}">{% csrf_token %}
|
||||
{% if direction == "inbound" %}{% trans "inbound" as direction_name %}{% else %}{% trans "outbound" as direction_name %}{% endif %}
|
||||
<form class="mt-2" style="max-width: 32rem" action="{% url 'instances:set_qos' instance.id %}" method="post" aria-label="{% blocktrans with direction=direction_name %}{{ direction }} QoS form{% endblocktrans %}">{% csrf_token %}
|
||||
<input type="hidden" name="net-mac" value="{{ network.mac }}">
|
||||
<input type="hidden" name="qos_direction" value="{{ direction }}">
|
||||
<div class="fw-semibold mb-1">{% if direction == "inbound" %}{% trans "Inbound" %}{% else %}{% trans "Outbound" %}{% endif %}</div>
|
||||
|
|
@ -21,6 +22,6 @@
|
|||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary" name="set_qos">{% trans "Save" %}</button>
|
||||
<button type="submit" class="btn btn-sm btn-danger" name="unset_qos" formaction="{% url 'instances:unset_qos' instance.id %}" formnovalidate
|
||||
data-confirm="{% blocktrans with mac=network.mac %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}">{% trans "Delete limit" %}</button>
|
||||
data-confirm="{% blocktrans with direction=direction_name mac=network.mac %}Remove the {{ direction }} limit of {{ mac }}? A running VM keeps it until it is powered off and on.{% endblocktrans %}">{% trans "Delete limit" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.urls import reverse
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_noop as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django.views.decorators.http import require_POST
|
||||
from libvirt import (VIR_DOMAIN_PAUSED,
|
||||
VIR_DOMAIN_RUNNING,
|
||||
|
|
@ -741,11 +742,11 @@ def invalid_disk_options(
|
|||
# libvirt's words when a running q35 VM has no free root port for a PCIe device
|
||||
PCI_SLOTS_FULL = "No more available PCI slots"
|
||||
# a disk on the scsi bus needs no slot of its own when the VM already has a SCSI controller
|
||||
DISK_NO_PCI_SLOT = _("The running VM has no free PCI slot for another disk on this bus: choose the scsi bus "
|
||||
DISK_NO_PCI_SLOT = gettext_lazy("The running VM has no free PCI slot for another disk on this bus: choose the scsi bus "
|
||||
"if the VM has a SCSI controller, or shut the VM down and add the disk then; Add PCIe slots "
|
||||
"on the Disks tab gives it free ones from its next start. A new volume stays in its pool, "
|
||||
"to attach as an existing volume.")
|
||||
SCSI_NO_PCI_SLOT = _("The running VM has no free PCI slot for the SCSI controller the disk needs: shut the VM "
|
||||
SCSI_NO_PCI_SLOT = gettext_lazy("The running VM has no free PCI slot for the SCSI controller the disk needs: shut the VM "
|
||||
"down and add the disk then; Add PCIe slots on the Disks tab gives it free ones from its next "
|
||||
"start. A new volume stays in its pool, to attach as an existing volume.")
|
||||
|
||||
|
|
@ -1059,7 +1060,7 @@ def gstfsd_request(compute, data):
|
|||
|
||||
# root password and SSH key: gstfsd changes a shut-off VM's disk, the QEMU
|
||||
# guest agent a running VM
|
||||
AGENT_OR_SHUT_OFF = _("Shut the VM down, or start its QEMU guest agent, and try again")
|
||||
AGENT_OR_SHUT_OFF = gettext_lazy("Shut the VM down, or start its QEMU guest agent, and try again")
|
||||
|
||||
|
||||
# libvirt's value; bindings built against older libvirt (Ubuntu 24.04's) lack
|
||||
|
|
@ -2207,13 +2208,13 @@ def set_qos(request, pk):
|
|||
instance.proxy.set_qos(mac, qos_dir, average, peak, burst)
|
||||
if instance.proxy.get_status() == 5:
|
||||
messages.success(
|
||||
request, _("%(qos_dir)s QoS is set") % {"qos_dir": qos_dir.capitalize()}
|
||||
request, _("%(qos_dir)s QoS is set") % {"qos_dir": _(qos_dir.capitalize())}
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on.")
|
||||
% {"qos_dir": qos_dir.capitalize()},
|
||||
% {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
|
||||
return _back(request, pk, "network")
|
||||
|
|
@ -2234,13 +2235,13 @@ def unset_qos(request, pk):
|
|||
|
||||
if instance.proxy.get_status() == 5:
|
||||
messages.success(
|
||||
request, _("%(qos_dir)s QoS is deleted") % {"qos_dir": qos_dir.capitalize()}
|
||||
request, _("%(qos_dir)s QoS is deleted") % {"qos_dir": _(qos_dir.capitalize())}
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is powered off and on.")
|
||||
% {"qos_dir": qos_dir.capitalize()},
|
||||
% {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
return _back(request, pk, "network")
|
||||
|
||||
|
|
@ -2492,9 +2493,9 @@ def create_instance_select_type(request, compute_id):
|
|||
|
||||
# the create form's disk sources: (value, title, help)
|
||||
DISK_SOURCES = [
|
||||
("new", _("New disk"), _("An empty disk of the size you set")),
|
||||
("existing", _("Existing volumes"), _("Volumes already in a pool")),
|
||||
("template", _("Clone from template"), _("A copy of a template volume")),
|
||||
("new", gettext_lazy("New disk"), gettext_lazy("An empty disk of the size you set")),
|
||||
("existing", gettext_lazy("Existing volumes"), gettext_lazy("Volumes already in a pool")),
|
||||
("template", gettext_lazy("Clone from template"), gettext_lazy("A copy of a template volume")),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -288,6 +288,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for direction, limit in qos.items %}
|
||||
{% if direction == "inbound" %}{% trans "inbound" as direction_name %}{% else %}{% trans "outbound" as direction_name %}{% endif %}
|
||||
<tr>
|
||||
<td>{% if direction == "inbound" %}{% trans "Inbound" %}{% else %}{% trans "Outbound" %}{% endif %}</td>
|
||||
<td>{% if limit.average %}{{ limit.average }} KiB/s{% else %}–{% endif %}</td>
|
||||
|
|
@ -301,7 +302,7 @@
|
|||
<form method="post" class="btn-group">{% csrf_token %}
|
||||
<input type="hidden" name="qos_direction" value="{{ direction }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary" name="unset_qos" aria-label="{% trans "Remove" %}"
|
||||
data-confirm="{% blocktrans with name=pool %}Remove the {{ direction }} limit of network {{ name }}? It stays in force until the network is stopped and started.{% endblocktrans %}">{% bs_icon 'trash' %}</button>
|
||||
data-confirm="{% blocktrans with direction=direction_name name=pool %}Remove the {{ direction }} limit of network {{ name }}? It stays in force until the network is stopped and started.{% endblocktrans %}">{% bs_icon 'trash' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -221,12 +221,12 @@ def network(request, compute_id, pool):
|
|||
_(
|
||||
"%(qos_dir)s QoS is updated. Network XML is changed. Stop and start network to activate new config"
|
||||
)
|
||||
% {"qos_dir": qos_dir.capitalize()},
|
||||
% {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("%(qos_dir)s QoS is set") % {"qos_dir": qos_dir.capitalize()},
|
||||
_("%(qos_dir)s QoS is set") % {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
except (libvirtError, ValueError) as err:
|
||||
messages.error(request, err)
|
||||
|
|
@ -247,12 +247,12 @@ def network(request, compute_id, pool):
|
|||
"%(qos_dir)s QoS is deleted. Network XML is changed. "
|
||||
"Stop and start network to activate new config"
|
||||
)
|
||||
% {"qos_dir": qos_dir.capitalize()},
|
||||
% {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("%(qos_dir)s QoS is deleted") % {"qos_dir": qos_dir.capitalize()},
|
||||
_("%(qos_dir)s QoS is deleted") % {"qos_dir": _(qos_dir.capitalize())},
|
||||
)
|
||||
return HttpResponseRedirect(here)
|
||||
# read only for the page: every action above redirects
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ from operations.templatetags.operations import STATE_COLORS, STATE_LABELS, state
|
|||
from operations.tests import fakes
|
||||
|
||||
|
||||
class BusyMessageTestCase(SimpleTestCase):
|
||||
def test_the_state_of_the_job_in_the_way_is_named_in_the_users_language(self):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from operations.utils import busy_message
|
||||
op = Operation(kind="snapshot.create", state=Operation.RUNNING, created_at=django_timezone.now())
|
||||
with translation.override("tr"):
|
||||
text = busy_message(SimpleNamespace(blocker=op), SimpleNamespace(is_superuser=False, pk=1))
|
||||
self.assertIn(" çalışıyor ", text)
|
||||
self.assertNotIn("running", text)
|
||||
|
||||
|
||||
class StateColorsTestCase(SimpleTestCase):
|
||||
def test_every_state_has_its_colour(self):
|
||||
# job_panel.test.js checks the same map in job_panel.js
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import libvirt
|
|||
from django.utils import timezone
|
||||
from django.utils.timesince import timesince
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import pgettext
|
||||
from django.utils.translation import gettext_lazy
|
||||
from libvirt import libvirtError
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ def busy_message(busy, user):
|
|||
op = busy.blocker
|
||||
if op is None:
|
||||
return _("A short operation is in progress on this resource; try again in a moment.")
|
||||
values = {"label": _(jobs.label_of(op.kind)), "state": op.state, "since": timesince(op.created_at, timezone.now())}
|
||||
values = {"label": _(jobs.label_of(op.kind)), "state": pgettext("job state", op.state), "since": timesince(op.created_at, timezone.now())}
|
||||
if may_see(op, user):
|
||||
return _("%(label)s is %(state)s on this resource (started by %(user)s, %(since)s ago); "
|
||||
"see the Jobs page.") % {**values, "user": op.username}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue