From 7d86160ade49da9d9f9dc9b31313f673d7c1f3d1 Mon Sep 17 00:00:00 2001 From: fahadhewad Date: Thu, 4 Jun 2026 16:37:43 +0100 Subject: [PATCH 1/5] 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 --- docs/changes.rst | 2 ++ docs/snippets/check-flags-autogenerated.rst | 4 +++ docs/snippets/checks-autogenerated.rst | 32 +++++++++++++++++++++ weblate/checks/chars.py | 20 +++++++++++++ weblate/checks/defaults.py | 1 + weblate/checks/tests/test_chars_checks.py | 29 +++++++++++++++++++ weblate/settings_example.py | 1 + 7 files changed, 89 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index ee082ab73f85..ae17725db8b5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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. diff --git a/docs/snippets/check-flags-autogenerated.rst b/docs/snippets/check-flags-autogenerated.rst index 4d60bdb64599..d18c2e38ba4c 100644 --- a/docs/snippets/check-flags-autogenerated.rst +++ b/docs/snippets/check-flags-autogenerated.rst @@ -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. @@ -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`` diff --git a/docs/snippets/checks-autogenerated.rst b/docs/snippets/checks-autogenerated.rst index 051650d62041..9a5aea26b850 100644 --- a/docs/snippets/checks-autogenerated.rst +++ b/docs/snippets/checks-autogenerated.rst @@ -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. diff --git a/weblate/checks/chars.py b/weblate/checks/chars.py index 0629c34ffafd..7fe70b12446f 100644 --- a/weblate/checks/chars.py +++ b/weblate/checks/chars.py @@ -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.""" diff --git a/weblate/checks/defaults.py b/weblate/checks/defaults.py index ab5352c5ae4f..ca7c946c205c 100644 --- a/weblate/checks/defaults.py +++ b/weblate/checks/defaults.py @@ -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", diff --git a/weblate/checks/tests/test_chars_checks.py b/weblate/checks/tests/test_chars_checks.py index 6508ca6f392a..dbcfe52abeca 100644 --- a/weblate/checks/tests/test_chars_checks.py +++ b/weblate/checks/tests/test_chars_checks.py @@ -7,6 +7,7 @@ from django.test import SimpleTestCase from weblate.checks.chars import ( + AcceleratorKeyCheck, BeginNewlineCheck, BeginSpaceCheck, DoubleSpaceCheck, @@ -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() diff --git a/weblate/settings_example.py b/weblate/settings_example.py index 2f77f296f77f..805c49d630e0 100644 --- a/weblate/settings_example.py +++ b/weblate/settings_example.py @@ -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", From 3e44c9fd52f51b1cbfed78c8ebf6a364831b3b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 12 Jun 2026 08:50:12 +0200 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/snippets/checks-autogenerated.rst | 8 +++++--- weblate/checks/chars.py | 17 +++++++++++++++-- weblate/checks/tests/test_chars_checks.py | 18 +++++++++++------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/docs/snippets/checks-autogenerated.rst b/docs/snippets/checks-autogenerated.rst index 9a5aea26b850..5d4ce9d1e51b 100644 --- a/docs/snippets/checks-autogenerated.rst +++ b/docs/snippets/checks-autogenerated.rst @@ -26,9 +26,11 @@ Accelerator key 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. +``&`` (Qt, Windows) or ``_`` (GTK). Literal marker characters can usually be +escaped (for example ``&&`` in Qt/Windows and ``__`` in GTK) and are ignored by +this check, as are HTML entities such as ``&``. 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:: diff --git a/weblate/checks/chars.py b/weblate/checks/chars.py index 7fe70b12446f..bc202899833c 100644 --- a/weblate/checks/chars.py +++ b/weblate/checks/chars.py @@ -52,10 +52,23 @@ class AcceleratorKeyCheck(TargetCheck): default_disabled = True version_added = "2026.7" + def _count_accelerators(self, text: str, key: str) -> int: + # HTML/XML entities start with '&' but are not accelerator markers. + if key == "&": + text = strip_entities(text) + # Qt/Windows escape a literal ampersand as "&&". + text = text.replace("&&", "") + elif key == "_": + # GTK escapes a literal underscore as "__". + text = text.replace("__", "") + return text.count(key) + def _check_accelerator(self, source: str, target: str, key: str) -> bool: - return target.count(key) > 1 or source.count(key) != target.count(key) + src_count = self._count_accelerators(source, key) + tgt_count = self._count_accelerators(target, key) + return tgt_count > 1 or src_count != tgt_count - def check_single(self, source: str, target: str, unit: Unit): + def check_single(self, source: str, target: str, unit: Unit) -> bool: return self._check_accelerator(source, target, "&") or self._check_accelerator( source, target, "_" ) diff --git a/weblate/checks/tests/test_chars_checks.py b/weblate/checks/tests/test_chars_checks.py index dbcfe52abeca..d7976839a191 100644 --- a/weblate/checks/tests/test_chars_checks.py +++ b/weblate/checks/tests/test_chars_checks.py @@ -53,13 +53,17 @@ def test_underscore_accelerator(self) -> None: 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")) - + # Escaped/literal ampersands (Qt/Windows "&&") should not count as accelerators. + self.do_test(False, ("Save && Exit", "Save && Exit", "accelerators")) + # HTML entities should not be treated as accelerators. + self.do_test(False, ("Fish & Chips", "Fish & Chips", "accelerators")) + self.do_test(False, ("A & B & C", "A & B & C", "accelerators")) + + def test_escaped_underscore(self) -> None: + # Escaped/literal underscores (GTK "__") should not count as accelerators. + self.do_test(False, ("__File", "__File", "accelerators")) + # "___" is commonly used for a literal underscore plus an accelerator marker. + self.do_test(False, ("___File", "___File", "accelerators")) class BeginNewlineCheckTest(CheckTestCase): check = BeginNewlineCheck() From 2c88e1fb8ff06413ccf5dd00db756271a0d019b9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 06:51:18 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- weblate/checks/tests/test_chars_checks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/weblate/checks/tests/test_chars_checks.py b/weblate/checks/tests/test_chars_checks.py index d7976839a191..16967203e3e6 100644 --- a/weblate/checks/tests/test_chars_checks.py +++ b/weblate/checks/tests/test_chars_checks.py @@ -65,6 +65,7 @@ def test_escaped_underscore(self) -> None: # "___" is commonly used for a literal underscore plus an accelerator marker. self.do_test(False, ("___File", "___File", "accelerators")) + class BeginNewlineCheckTest(CheckTestCase): check = BeginNewlineCheck() From afec5c6957e999eaa2248a735b09b671d7e64f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 12 Jun 2026 09:04:32 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- weblate/checks/chars.py | 2 +- weblate/checks/tests/test_chars_checks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/weblate/checks/chars.py b/weblate/checks/chars.py index bc202899833c..421cdc9f0c8e 100644 --- a/weblate/checks/chars.py +++ b/weblate/checks/chars.py @@ -47,7 +47,7 @@ class AcceleratorKeyCheck(TargetCheck): check_id = "accelerators" name = gettext_lazy("Accelerator key") description = gettext_lazy( - "Source and translation do not both contain an accelerator key." + "Source and translation contain inconsistent accelerator keys." ) default_disabled = True version_added = "2026.7" diff --git a/weblate/checks/tests/test_chars_checks.py b/weblate/checks/tests/test_chars_checks.py index 16967203e3e6..8c87d25edd79 100644 --- a/weblate/checks/tests/test_chars_checks.py +++ b/weblate/checks/tests/test_chars_checks.py @@ -51,7 +51,7 @@ def test_underscore_accelerator(self) -> None: self.do_test(True, ("File", "_File", "accelerators")) def test_literal_ampersand(self) -> None: - # A balanced, literal ampersand is not treated as an accelerator key. + # A literal ampersand present in both source and translation should not trigger this check. self.do_test(False, ("Walter & Sons", "Walter & Sons", "accelerators")) # Escaped/literal ampersands (Qt/Windows "&&") should not count as accelerators. self.do_test(False, ("Save && Exit", "Save && Exit", "accelerators")) From 27074da91b42975c5cdb5a7297c6da4137c23e22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 07:13:48 +0000 Subject: [PATCH 5/5] docs: Documentation snippets update --- docs/snippets/checks-autogenerated.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/snippets/checks-autogenerated.rst b/docs/snippets/checks-autogenerated.rst index 5d4ce9d1e51b..5fac9c7b8ed5 100644 --- a/docs/snippets/checks-autogenerated.rst +++ b/docs/snippets/checks-autogenerated.rst @@ -14,7 +14,7 @@ Accelerator key .. versionadded:: 2026.7 -:Summary: Source and translation do not both contain an accelerator key. +:Summary: Source and translation contain inconsistent accelerator keys. :Scope: translated strings :Check class: ``weblate.checks.chars.AcceleratorKeyCheck`` :Check identifier: ``accelerators``