Skip to content

Commit 83922b6

Browse files
authored
Merge branch 'main' into 15949_user_profile_API
2 parents e92eebd + 5aa7d7e commit 83922b6

11 files changed

Lines changed: 527 additions & 51 deletions

File tree

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Weblate 2026.8
1010
* :doc:`Translation reports </devel/reporting>` are now generated in the background, stored for later download, available at workspace scope, and include translator work analysis.
1111
* Added :guilabel:`Use keywords exclusively` option to :ref:`addon-weblate.gettext.xgettext`, allowing projects to disable xgettext default keywords and rely only on a custom keyword.
1212
* Added API support for reading and updating :ref:`user-profile` preferences. See :ref:`api-users` endpoint.
13+
* Added :ref:`check-asciidoc-markup` quality check for AsciiDoc strings.
1314
* Added support for legacy Qt Linguist TS version 1 files. See :ref:`qtling`.
1415

1516
.. rubric:: Improvements

docs/snippets/check-flags-autogenerated.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
``accelerator``
5151
Specify the single punctuation accelerator marker, for example ``accelerator:&``, ``accelerator:_``, or ``accelerator:~``.
5252
Enables the :ref:`check-accelerator` quality check.
53+
``asciidoc-text``
54+
Treat a text as an AsciiDoc document, affects :ref:`check-same`.
55+
Enables the :ref:`check-asciidoc-markup` quality check.
5356
``bbcode-text``
5457
Treat a text as an Bulletin Board Code (BBCode) document, affects :ref:`check-same`.
5558
Enables the :ref:`check-bbcode` quality check.
@@ -157,6 +160,8 @@
157160
158161
``ignore-accelerator``
159162
Skip the :ref:`check-accelerator` quality check.
163+
``ignore-asciidoc-markup``
164+
Skip the :ref:`check-asciidoc-markup` quality check.
160165
``ignore-bbcode``
161166
Skip the :ref:`check-bbcode` quality check.
162167
``ignore-xml-chars-around-tags``

docs/snippets/checks-autogenerated.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@ of accelerator keys, and that the translation does not contain more than one.
4040
ampersand and trigger a false positive. Use the ``ignore-accelerator``
4141
flag to skip the check for such strings.
4242

43+
.. AUTOGENERATED START: check-asciidoc-markup
44+
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
45+
46+
.. _check-asciidoc-markup:
47+
48+
AsciiDoc markup
49+
~~~~~~~~~~~~~~~
50+
51+
.. versionadded:: 2026.8
52+
53+
:Summary: AsciiDoc markup does not match source.
54+
:Scope: translated strings
55+
:Check class: ``weblate.checks.markup.AsciiDocMarkupCheck``
56+
:Check identifier: ``asciidoc-markup``
57+
:Trigger: This check needs to be enabled using a flag.
58+
:File formats automatically enabling this check: :ref:`asciidoc`
59+
:Flag to enable: ``asciidoc-text``
60+
:Flag to ignore: ``ignore-asciidoc-markup``
61+
62+
.. AUTOGENERATED END: check-asciidoc-markup
63+
64+
This check compares the markups found in the source with the markups found in the translation.
65+
It includes:
66+
67+
- inline and block macros (e.g. ``link:`` and ``image::``)
68+
- cross-references (``<<id>>``)
69+
- passthroughs (e.g. ``+++...+++``, ``$$...$$``)
70+
71+
Macro attribute text and cross-reference labels may be translated; the
72+
macro name, target, and reference identifier must stay the same in both the source and the translation.
73+
4374
.. AUTOGENERATED START: check-bbcode
4475
.. This section is automatically generated by `./manage.py list_checks`. Do not edit manually.
4576

docs/snippets/format-features/asciidoc-features.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
* - Supports removing obsolete strings :ref:`<obsolete-strings>`
2828
- ``No``
2929
* - Check flags added by this format :ref:`<custom-checks>`
30-
- :ref:`check-safe-html`, ``strict-same``
30+
- :ref:`check-safe-html`, ``strict-same``, ``asciidoc-text``
3131

3232
.. AUTOGENERATED END: format-features asciidoc

weblate/checks/base.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,44 @@ def format_result(self, result: MissingExtraDict) -> Iterable[StrOrPromise]:
446446
yield self.get_errors_text(set(errors))
447447

448448

449+
class PluralResultDescriptionMixin(TargetCheck):
450+
"""
451+
Build check descriptions by merging MissingExtraDict results across plurals.
452+
453+
For checks whose ``check_single`` returns a ``MissingExtraDict`` (missing,
454+
extra, and/or errors), this merges those results from every plural form and
455+
formats them into the check description.
456+
"""
457+
458+
def get_description(self, check_obj: Check) -> StrOrPromise:
459+
unit = check_obj.unit
460+
461+
errors: list[StrOrPromise] = []
462+
463+
# Merge plurals
464+
results: MissingExtraDict = {}
465+
for result in self.check_target_generator(
466+
unit.get_source_plurals(), unit.get_target_plurals(), unit
467+
):
468+
if not isinstance(result, dict):
469+
continue
470+
if missing := result.get("missing"):
471+
results.setdefault("missing", []).extend(missing)
472+
if extra := result.get("extra"):
473+
results.setdefault("extra", []).extend(extra)
474+
if result_errors := result.get("errors"):
475+
results.setdefault("errors", []).extend(result_errors)
476+
if any(results.values()):
477+
errors.extend(self.format_result(results))
478+
if errors:
479+
return format_html_join(
480+
mark_safe("<br />"),
481+
"{}",
482+
((error,) for error in errors),
483+
)
484+
return super().get_description(check_obj)
485+
486+
449487
class SourceCheck(BaseCheck):
450488
"""Basic class for source checks."""
451489

weblate/checks/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"weblate.checks.markup.SafeHTMLCheck",
7171
"weblate.checks.markup.RSTReferencesCheck",
7272
"weblate.checks.markup.RSTSyntaxCheck",
73+
"weblate.checks.markup.AsciiDocMarkupCheck",
7374
"weblate.checks.mdx.SafeMDXCheck",
7475
"weblate.checks.placeholders.PlaceholderCheck",
7576
"weblate.checks.placeholders.RegexCheck",

weblate/checks/management/commands/list_checks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def get_scope(check: BaseCheck) -> str | None:
5858
),
5959
"rst-text": "Treat a text as an reStructuredText document, affects :ref:`check-same`.",
6060
"bbcode-text": "Treat a text as an Bulletin Board Code (BBCode) document, affects :ref:`check-same`.",
61+
"asciidoc-text": "Treat a text as an AsciiDoc document, affects :ref:`check-same`.",
6162
"md-text": "Treat a text as a Markdown document, and provide Markdown syntax highlighting on the translation text area.",
6263
"auto-java-messageformat": "Treat a text as conditional Java MessageFormat, enabling :ref:`check-java-format` only when the source contains Java MessageFormat placeholders.",
6364
"auto-safe-html": "Treat a text as conditional HTML, enabling :ref:`check-safe-html` only for plain text or source strings that contain standard HTML markup or valid custom elements. This is useful for extended Markdown variants such as MDX, where angle-bracket syntax may not be HTML.",

weblate/checks/markup.py

Lines changed: 120 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import re
88
import threading
9-
from collections import Counter, defaultdict
9+
from collections import Counter
1010
from functools import cache, lru_cache
1111
from itertools import chain
1212
from types import SimpleNamespace
@@ -16,8 +16,6 @@
1616
from django.core.exceptions import ValidationError
1717
from django.core.validators import URLValidator
1818
from django.utils.functional import cached_property
19-
from django.utils.html import format_html_join
20-
from django.utils.safestring import mark_safe
2119
from django.utils.translation import gettext, gettext_lazy
2220
from docutils import utils
2321
from docutils.core import Publisher
@@ -39,7 +37,7 @@
3937
from docutils.readers.standalone import Reader
4038
from docutils.writers.null import Writer
4139

42-
from weblate.checks.base import Highlight, TargetCheck
40+
from weblate.checks.base import Highlight, PluralResultDescriptionMixin, TargetCheck
4341
from weblate.checks.format import (
4442
ES_TEMPLATE_MATCH,
4543
FLAG_RULES,
@@ -73,7 +71,6 @@
7371
from weblate.trans.models import Unit
7472

7573
from .base import FixupType
76-
from .models import Check
7774

7875
DOCUTILS_PARSER_LOCK = threading.Lock()
7976
BBCODE_MATCH = re.compile(
@@ -902,7 +899,7 @@ def extract_rst_references(
902899
return dict(result), Counter(item[0] for item in result), tuple(highlights)
903900

904901

905-
class RSTReferencesCheck(RSTBaseCheck):
902+
class RSTReferencesCheck(PluralResultDescriptionMixin, RSTBaseCheck):
906903
check_id = "rst-references"
907904
name = gettext_lazy("Inconsistent reStructuredText")
908905
description = gettext_lazy(
@@ -947,29 +944,6 @@ def check_single(
947944
}
948945
return False
949946

950-
def get_description(self, check_obj: Check) -> StrOrPromise:
951-
unit = check_obj.unit
952-
953-
errors: list[StrOrPromise] = []
954-
results: MissingExtraDict = cast("MissingExtraDict", defaultdict(list))
955-
956-
# Merge plurals
957-
for result in self.check_target_generator(
958-
unit.get_source_plurals(), unit.get_target_plurals(), unit
959-
):
960-
if isinstance(result, dict):
961-
for key, value in result.items():
962-
results[key].extend(value)
963-
if results:
964-
errors.extend(self.format_result(results))
965-
if errors:
966-
return format_html_join(
967-
mark_safe("<br />"),
968-
"{}",
969-
((error,) for error in errors),
970-
)
971-
return super().get_description(check_obj)
972-
973947
def check_highlight(self, source: str, unit: Unit):
974948
if self.should_skip(unit):
975949
return
@@ -1060,7 +1034,7 @@ def error_collector(data: system_message) -> None:
10601034
return tuple(errors), tuple(roles)
10611035

10621036

1063-
class RSTSyntaxCheck(RSTBaseCheck):
1037+
class RSTSyntaxCheck(PluralResultDescriptionMixin, RSTBaseCheck):
10641038
check_id = "rst-syntax"
10651039
name = gettext_lazy("reStructuredText syntax error")
10661040
description = gettext_lazy("reStructuredText syntax error in the translation.")
@@ -1092,25 +1066,122 @@ def check_single(
10921066
return {"errors": errors}
10931067
return False
10941068

1095-
def get_description(self, check_obj: Check) -> StrOrPromise:
1096-
unit = check_obj.unit
10971069

1098-
errors: list[StrOrPromise] = []
1099-
results: MissingExtraDict = cast("MissingExtraDict", defaultdict(list))
1070+
# inline (`name:target[attrs]`) and block (`name::target[attrs]`) macros.
1071+
ASCIIDOC_MACRO = re.compile(
1072+
r"(?P<name>[a-z][a-z0-9]*)(?P<sep>::?)(?P<target>[^\s\[]*)\[(?P<attrs>(?:\\.|[^\]])*)\]"
1073+
)
1074+
# cross references: <<id>> or <<id,text>>.
1075+
ASCIIDOC_XREF = re.compile(r"<<(?P<id>[^,>]+)(?:,(?P<text>[^>]*))?>>")
1076+
# inline anchors: [[id]] or [[id,label]].
1077+
ASCIIDOC_INLINE_ANCHOR = re.compile(r"\[\[(?P<id>[^,\]]+)(?:,(?P<label>[^\]]*))?\]\]")
1078+
# passthroughs. Bare single-plus `+...+` is excluded to avoid false positives.
1079+
ASCIIDOC_PASSTHROUGH = re.compile(
1080+
r"\+\+\+.+?\+\+\+|\+\+.+?\+\+|`\+.+?\+`|\$\$.+?\$\$",
1081+
re.DOTALL,
1082+
)
1083+
11001084

1101-
# Merge plurals
1102-
for result in self.check_target_generator(
1103-
unit.get_source_plurals(), unit.get_target_plurals(), unit
1104-
):
1105-
if isinstance(result, dict):
1106-
for key, value in result.items():
1107-
results[key].extend(value)
1108-
if results:
1109-
errors.extend(self.format_result(results))
1110-
if errors:
1111-
return format_html_join(
1112-
mark_safe("<br />"),
1113-
"{}",
1114-
((error,) for error in errors),
1085+
def is_preceded_by_asciidoc_escape(text: str, start: int) -> bool:
1086+
"""Return whether markup at ``start`` is escaped and renders as literal text."""
1087+
backslashes = 0
1088+
pos = start - 1
1089+
while pos >= 0 and text[pos] == "\\":
1090+
backslashes += 1
1091+
pos -= 1
1092+
return backslashes % 2 == 1
1093+
1094+
1095+
class AsciiDocMarkupCheck(PluralResultDescriptionMixin, TargetCheck):
1096+
"""Check that AsciiDoc macros, xrefs, and passthroughs match the source."""
1097+
1098+
check_id = "asciidoc-markup"
1099+
name = gettext_lazy("AsciiDoc markup")
1100+
description = gettext_lazy("AsciiDoc markup does not match source.")
1101+
version_added = "2026.8"
1102+
default_disabled = True
1103+
1104+
def __init__(self) -> None:
1105+
super().__init__()
1106+
self.enable_string = "asciidoc-text"
1107+
1108+
def get_missing_text(self, values: Iterable[str]) -> StrOrPromise:
1109+
return self.get_values_text(
1110+
gettext("The following AsciiDoc markup is missing: {}"), values
1111+
)
1112+
1113+
def get_extra_text(self, values: Iterable[str]) -> StrOrPromise:
1114+
return self.get_values_text(
1115+
gettext("The following AsciiDoc markup is extra: {}"), values
1116+
)
1117+
1118+
def check_single(
1119+
self, source: str, target: str, unit: Unit
1120+
) -> bool | MissingExtraDict:
1121+
src_set = extract_asciidoc_markup(source)
1122+
tgt_set = extract_asciidoc_markup(target)
1123+
1124+
missing = src_set - tgt_set
1125+
extra = tgt_set - src_set
1126+
1127+
if missing or extra:
1128+
return {
1129+
"missing": list(missing.elements()),
1130+
"extra": list(extra.elements()),
1131+
"errors": [],
1132+
}
1133+
return False
1134+
1135+
def check_highlight(self, source: str, unit: Unit):
1136+
if self.should_skip(unit):
1137+
return
1138+
yield from iter_asciidoc_highlights(source)
1139+
1140+
1141+
def extract_asciidoc_markup(text: str) -> Counter[str]:
1142+
tokens: list[str] = []
1143+
for match in ASCIIDOC_MACRO.finditer(text):
1144+
if is_preceded_by_asciidoc_escape(text, match.start()):
1145+
continue
1146+
if match.group("name") == "pass":
1147+
# special case for passthrough macros, the content is not translatable and must be preserved
1148+
tokens.append(match.group())
1149+
else:
1150+
tokens.append(
1151+
f"{match.group('name')}{match.group('sep')}{match.group('target')}[]"
11151152
)
1116-
return super().get_description(check_obj)
1153+
tokens.extend(
1154+
f"<<{match.group('id')}>>"
1155+
for match in ASCIIDOC_XREF.finditer(text)
1156+
if not is_preceded_by_asciidoc_escape(text, match.start())
1157+
)
1158+
tokens.extend(
1159+
f"[[{match.group('id')}]]"
1160+
for match in ASCIIDOC_INLINE_ANCHOR.finditer(text)
1161+
if not is_preceded_by_asciidoc_escape(text, match.start())
1162+
)
1163+
tokens.extend(
1164+
match.group()
1165+
for match in ASCIIDOC_PASSTHROUGH.finditer(text)
1166+
if not is_preceded_by_asciidoc_escape(text, match.start())
1167+
)
1168+
return Counter(tokens)
1169+
1170+
1171+
def iter_asciidoc_highlights(text: str) -> Iterable[Highlight]:
1172+
for match in ASCIIDOC_MACRO.finditer(text):
1173+
if is_preceded_by_asciidoc_escape(text, match.start()):
1174+
continue
1175+
yield Highlight(match.start(), match.end(), match.group(), kind="syntax")
1176+
for match in ASCIIDOC_XREF.finditer(text):
1177+
if is_preceded_by_asciidoc_escape(text, match.start()):
1178+
continue
1179+
yield Highlight(match.start(), match.end(), match.group(), kind="syntax")
1180+
for match in ASCIIDOC_INLINE_ANCHOR.finditer(text):
1181+
if is_preceded_by_asciidoc_escape(text, match.start()):
1182+
continue
1183+
yield Highlight(match.start(), match.end(), match.group(), kind="syntax")
1184+
for match in ASCIIDOC_PASSTHROUGH.finditer(text):
1185+
if is_preceded_by_asciidoc_escape(text, match.start()):
1186+
continue
1187+
yield Highlight(match.start(), match.end(), match.group(), kind="syntax")

0 commit comments

Comments
 (0)