Skip to content

Commit 54b243a

Browse files
committed
feat(checks): add accelerator key consistency check
Add an opt-in quality check that flags strings where the source and translation do not contain a matching accelerator key. It checks both the "&" (Qt/Windows) and "_" (GTK) markers, reporting a problem when the counts differ or the translation contains more than one. The check is disabled by default and is turned on per string or component with the "accelerators" flag, or skipped with "ignore-accelerators". Closes #5962
1 parent 3afb206 commit 54b243a

7 files changed

Lines changed: 86 additions & 0 deletions

File tree

docs/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Weblate 2026.7
55

66
.. rubric:: New features
77

8+
* 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.
9+
810
.. rubric:: Improvements
911

1012
* Management interface access control is now more fine-grained with dedicated site-wide permissions.

docs/snippets/check-flags-autogenerated.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
.. AUTOGENERATED START: check-flags-enables
4848
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
4949
50+
``accelerators``
51+
Enables the :ref:`check-accelerators` quality check.
5052
``bbcode-text``
5153
Treat a text as an Bulletin Board Code (BBCode) document, affects :ref:`check-same`.
5254
Enables the :ref:`check-bbcode` quality check.
@@ -150,6 +152,8 @@
150152
.. AUTOGENERATED START: check-flags-ignores
151153
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
152154
155+
``ignore-accelerators``
156+
Skip the :ref:`check-accelerators` quality check.
153157
``ignore-bbcode``
154158
Skip the :ref:`check-bbcode` quality check.
155159
``ignore-xml-chars-around-tags``

docs/snippets/checks-autogenerated.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ Translation checks
44
Executed upon every translation change, helping translators maintain
55
good quality translations.
66

7+
.. AUTOGENERATED START: check-accelerators
8+
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
9+
10+
.. _check-accelerators:
11+
12+
Accelerator key
13+
~~~~~~~~~~~~~~~
14+
15+
.. versionadded:: 2026.7
16+
17+
:Summary: Source and translation do not both contain an accelerator key.
18+
:Scope: translated strings
19+
:Check class: ``weblate.checks.chars.AcceleratorKeyCheck``
20+
:Check identifier: ``accelerators``
21+
:Trigger: This check needs to be enabled using a flag.
22+
:Flag to enable: ``accelerators``
23+
:Flag to ignore: ``ignore-accelerators``
24+
25+
.. AUTOGENERATED END: check-accelerators
26+
27+
Accelerator keys (also known as mnemonics) let users trigger a UI action from
28+
the keyboard. They are marked in the string by a single character, typically
29+
``&`` (Qt, Windows) or ``_`` (GTK). This check verifies that the source and the
30+
translation contain the same number of accelerator keys, and that the
31+
translation does not contain more than one.
32+
33+
.. note::
34+
35+
A string such as ``Walter & Sons`` might be translated without the
36+
ampersand and trigger a false positive. Use the ``ignore-accelerators``
37+
flag to skip the check for such strings.
38+
739
.. AUTOGENERATED START: check-bbcode
840
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
941

weblate/checks/chars.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@
4141
INTERROBANGS = ("?!", "!?", "؟!", "!؟", "?!", "!?", "⁈", "⁉")
4242

4343

44+
class AcceleratorKeyCheck(TargetCheck):
45+
"""Check for inconsistent accelerator keys."""
46+
47+
check_id = "accelerators"
48+
name = gettext_lazy("Accelerator key")
49+
description = gettext_lazy(
50+
"Source and translation do not both contain an accelerator key."
51+
)
52+
default_disabled = True
53+
version_added = "2026.7"
54+
55+
def _check_accelerator(self, source: str, target: str, key: str) -> bool:
56+
return target.count(key) > 1 or source.count(key) != target.count(key)
57+
58+
def check_single(self, source: str, target: str, unit: Unit):
59+
return self._check_accelerator(source, target, "&") or self._check_accelerator(
60+
source, target, "_"
61+
)
62+
63+
4464
class BeginNewlineCheck(TargetCheck):
4565
"""Check for newlines at beginning."""
4666

weblate/checks/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
DEFAULT_CHECK_LIST: tuple[str, ...] = (
88
"weblate.checks.same.SameCheck",
9+
"weblate.checks.chars.AcceleratorKeyCheck",
910
"weblate.checks.chars.BeginNewlineCheck",
1011
"weblate.checks.chars.EndNewlineCheck",
1112
"weblate.checks.chars.BeginSpaceCheck",

weblate/checks/tests/test_chars_checks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.test import SimpleTestCase
88

99
from weblate.checks.chars import (
10+
AcceleratorKeyCheck,
1011
BeginNewlineCheck,
1112
BeginSpaceCheck,
1213
DoubleSpaceCheck,
@@ -32,6 +33,31 @@
3233
from weblate.trans.tests.factories import make_check, make_unit
3334

3435

36+
class AcceleratorKeyCheckTest(CheckTestCase):
37+
check = AcceleratorKeyCheck()
38+
39+
def setUp(self) -> None:
40+
super().setUp()
41+
self.test_good_matching = ("S&tring", "Str&ing", "accelerators")
42+
self.test_good_none = ("String", "String", "accelerators")
43+
self.test_good_flag = ("S&tring", "String", "")
44+
self.test_failure_1 = ("S&tring", "String", "accelerators")
45+
self.test_failure_2 = ("String", "Str&ing", "accelerators")
46+
self.test_failure_3 = ("S&tring", "S&tr&ing", "accelerators")
47+
48+
def test_underscore_accelerator(self) -> None:
49+
self.do_test(False, ("S_tring", "Str_ing", "accelerators"))
50+
self.do_test(True, ("S_tring", "String", "accelerators"))
51+
self.do_test(True, ("String", "Str_ing", "accelerators"))
52+
53+
def test_literal_ampersand(self) -> None:
54+
self.do_test(False, ("Walter & Sons", "Walter & Sons", "accelerators"))
55+
56+
def test_multiple_accelerators(self) -> None:
57+
self.do_test(True, ("S&tr&ing", "S&tr&ing", "accelerators"))
58+
self.do_test(True, ("S&tring", "S_tring", "accelerators"))
59+
60+
3561
class BeginNewlineCheckTest(CheckTestCase):
3662
check = BeginNewlineCheck()
3763

weblate/settings_example.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@
728728
# List of quality checks
729729
# CHECK_LIST = (
730730
# "weblate.checks.same.SameCheck",
731+
# "weblate.checks.chars.AcceleratorKeyCheck",
731732
# "weblate.checks.chars.BeginNewlineCheck",
732733
# "weblate.checks.chars.EndNewlineCheck",
733734
# "weblate.checks.chars.BeginSpaceCheck",

0 commit comments

Comments
 (0)