Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Weblate 2026.7

.. rubric:: New features

* Added the :ref:`check-accelerators` quality check, which verifies that accelerator keys are used consistently between the source and the translation. Enable it with the ``accelerators`` flag.

.. rubric:: Improvements

* Management interface access control is now more fine-grained with dedicated site-wide permissions.
Expand Down
4 changes: 4 additions & 0 deletions docs/snippets/check-flags-autogenerated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
.. AUTOGENERATED START: check-flags-enables
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.

``accelerators``
Enables the :ref:`check-accelerators` quality check.
``bbcode-text``
Treat a text as an Bulletin Board Code (BBCode) document, affects :ref:`check-same`.
Enables the :ref:`check-bbcode` quality check.
Expand Down Expand Up @@ -150,6 +152,8 @@
.. AUTOGENERATED START: check-flags-ignores
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.

``ignore-accelerators``
Skip the :ref:`check-accelerators` quality check.
``ignore-bbcode``
Skip the :ref:`check-bbcode` quality check.
``ignore-xml-chars-around-tags``
Expand Down
32 changes: 32 additions & 0 deletions docs/snippets/checks-autogenerated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@ Translation checks
Executed upon every translation change, helping translators maintain
good quality translations.

.. AUTOGENERATED START: check-accelerators
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.

.. _check-accelerators:

Accelerator key
~~~~~~~~~~~~~~~

.. versionadded:: 2026.7

:Summary: Source and translation do not both contain an accelerator key.
:Scope: translated strings
:Check class: ``weblate.checks.chars.AcceleratorKeyCheck``
:Check identifier: ``accelerators``
:Trigger: This check needs to be enabled using a flag.
:Flag to enable: ``accelerators``
:Flag to ignore: ``ignore-accelerators``

.. AUTOGENERATED END: check-accelerators

Accelerator keys (also known as mnemonics) let users trigger a UI action from
the keyboard. They are marked in the string by a single character, typically
``&`` (Qt, Windows) or ``_`` (GTK). This check verifies that the source and the
translation contain the same number of accelerator keys, and that the
translation does not contain more than one.

.. note::

A string such as ``Walter & Sons`` might be translated without the
ampersand and trigger a false positive. Use the ``ignore-accelerators``
flag to skip the check for such strings.

.. AUTOGENERATED START: check-bbcode
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.

Expand Down
20 changes: 20 additions & 0 deletions weblate/checks/chars.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@
INTERROBANGS = ("?!", "!?", "؟!", "!؟", "?!", "!?", "⁈", "⁉")


class AcceleratorKeyCheck(TargetCheck):
"""Check for inconsistent accelerator keys."""

check_id = "accelerators"
name = gettext_lazy("Accelerator key")
description = gettext_lazy(
"Source and translation do not both contain an accelerator key."
)
default_disabled = True
version_added = "2026.7"

def _check_accelerator(self, source: str, target: str, key: str) -> bool:
return target.count(key) > 1 or source.count(key) != target.count(key)

def check_single(self, source: str, target: str, unit: Unit):
return self._check_accelerator(source, target, "&") or self._check_accelerator(
source, target, "_"
)


class BeginNewlineCheck(TargetCheck):
"""Check for newlines at beginning."""

Expand Down Expand Up @@ -553,7 +573,7 @@
)
for i, char in enumerate(target):
# Advance to the next highlighted range if we've passed the current one.
while current_start is not None and i >= current_end:

Check failure on line 576 in weblate/checks/chars.py

View workflow job for this annotation

GitHub Actions / mypy

Unsupported operand types for >= ("int" and "None")
range_index += 1
if range_index < len(highlighted_ranges):
current_start, current_end = highlighted_ranges[range_index]
Expand All @@ -561,7 +581,7 @@
current_start = current_end = None
break
# Skip characters that fall inside a highlighted range.
if current_start is not None and current_start <= i < current_end:

Check failure on line 584 in weblate/checks/chars.py

View workflow job for this annotation

GitHub Actions / mypy

Unsupported operand types for < ("int" and "None")
continue
if char in FRENCH_PUNCTUATION:
if i == 0:
Expand Down
1 change: 1 addition & 0 deletions weblate/checks/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

DEFAULT_CHECK_LIST: tuple[str, ...] = (
"weblate.checks.same.SameCheck",
"weblate.checks.chars.AcceleratorKeyCheck",
"weblate.checks.chars.BeginNewlineCheck",
"weblate.checks.chars.EndNewlineCheck",
"weblate.checks.chars.BeginSpaceCheck",
Expand Down
29 changes: 29 additions & 0 deletions weblate/checks/tests/test_chars_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.test import SimpleTestCase

from weblate.checks.chars import (
AcceleratorKeyCheck,
BeginNewlineCheck,
BeginSpaceCheck,
DoubleSpaceCheck,
Expand All @@ -32,6 +33,34 @@
from weblate.trans.tests.factories import make_check, make_unit


class AcceleratorKeyCheckTest(CheckTestCase):
check = AcceleratorKeyCheck()

def setUp(self) -> None:
super().setUp()
self.test_good_matching = ("&File", "&File", "accelerators")
self.test_good_none = ("File", "File", "accelerators")
self.test_good_flag = ("&File", "File", "")
self.test_failure_1 = ("&File", "File", "accelerators")
self.test_failure_2 = ("File", "&File", "accelerators")
self.test_failure_3 = ("&File", "&File &Edit", "accelerators")

def test_underscore_accelerator(self) -> None:
self.do_test(False, ("_File", "_File", "accelerators"))
self.do_test(True, ("_File", "File", "accelerators"))
self.do_test(True, ("File", "_File", "accelerators"))

def test_literal_ampersand(self) -> None:
# A balanced, literal ampersand is not treated as an accelerator key.
self.do_test(False, ("Walter & Sons", "Walter & Sons", "accelerators"))

def test_multiple_accelerators(self) -> None:
# More than one accelerator in the target is reported even if counts match.
self.do_test(True, ("&File &Edit", "&File &Edit", "accelerators"))
# A mismatched marker type (& vs _) is reported.
self.do_test(True, ("&File", "_File", "accelerators"))


class BeginNewlineCheckTest(CheckTestCase):
check = BeginNewlineCheck()

Expand Down
1 change: 1 addition & 0 deletions weblate/settings_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@
# List of quality checks
# CHECK_LIST = (
# "weblate.checks.same.SameCheck",
# "weblate.checks.chars.AcceleratorKeyCheck",
# "weblate.checks.chars.BeginNewlineCheck",
# "weblate.checks.chars.EndNewlineCheck",
# "weblate.checks.chars.BeginSpaceCheck",
Expand Down
Loading