Skip to content

Commit 5975ee7

Browse files
committed
fix(translate): corrected string ordering
The project and category language views defaulted to wrong ordering and didn't show a component switch reliably. Fixes #19613
1 parent a84c28a commit 5975ee7

5 files changed

Lines changed: 138 additions & 15 deletions

File tree

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Weblate 2026.6
1313
.. rubric:: Bug fixes
1414

1515
* Searching for strings with content changes without a recorded author now supports ``changed_by:""``.
16+
* Project-language translation sessions now keep strings grouped by component priority and show component switch warnings reliably.
1617

1718
.. rubric:: Compatibility
1819

weblate/trans/models/unit.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@
7878
NEWLINES = re.compile(r"\r\n|\r|\n")
7979

8080

81+
def orders_units_by_component(obj: object) -> bool:
82+
"""Return whether the object scope spans component-ordered units."""
83+
if isinstance(obj, (Project, Category)):
84+
return True
85+
86+
from weblate.utils.stats import CategoryLanguage, ProjectLanguage # noqa: PLC0415
87+
88+
return isinstance(obj, (ProjectLanguage, CategoryLanguage))
89+
90+
8191
def fill_in_source_translation(units: Iterable[Unit]) -> None:
8292
"""
8393
Inject source translation into component from the source unit.
@@ -282,7 +292,7 @@ def order_by_request(self, form_data, obj) -> UnitQuerySet:
282292
if not sort_list:
283293
if hasattr(obj, "component") and obj.component.is_glossary:
284294
sort_list = ["source"]
285-
elif isinstance(obj, (Project, Category)):
295+
elif orders_units_by_component(obj):
286296
sort_list = [
287297
# Show locked components at the end
288298
"translation__component__locked",
@@ -292,6 +302,7 @@ def order_by_request(self, form_data, obj) -> UnitQuerySet:
292302
"translation__component__is_glossary",
293303
"translation__component__name",
294304
"-priority",
305+
"position",
295306
]
296307
else:
297308
sort_list = ["-priority", "position"]

weblate/trans/tests/test_edit.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,52 @@ def test_revert_history_after_component_move_uses_current_translate_url(
13541354
f'href="{translate_url}?checksum={unit.checksum}&revert={change.id}"',
13551355
)
13561356

1357+
def test_project_language_warns_when_switching_component(self) -> None:
1358+
Component.objects.filter(pk=self.component.pk).update(priority=120)
1359+
high_component = self.create_po(name="High", priority=80, project=self.project)
1360+
high_translation = high_component.translation_set.get(
1361+
language=self.translation.language
1362+
)
1363+
translate_url = ProjectLanguage(
1364+
self.project, self.translation.language
1365+
).get_translate_url()
1366+
high_component_offset = high_translation.unit_set.count()
1367+
1368+
response = self.client.get(translate_url, {"offset": high_component_offset})
1369+
self.assertNotContains(response, "You have shifted from")
1370+
1371+
response = self.client.get(translate_url, {"offset": high_component_offset + 1})
1372+
self.assertContains(response, "You have shifted from")
1373+
1374+
def test_project_language_ignores_stale_component_shift_unit(self) -> None:
1375+
Component.objects.filter(pk=self.component.pk).update(priority=120)
1376+
high_component = self.create_po(name="High", priority=80, project=self.project)
1377+
high_translation = high_component.translation_set.get(
1378+
language=self.translation.language
1379+
)
1380+
translate_url = ProjectLanguage(
1381+
self.project, self.translation.language
1382+
).get_translate_url()
1383+
high_component_offset = high_translation.unit_set.count()
1384+
1385+
response = self.client.get(translate_url, {"offset": high_component_offset})
1386+
self.assertEqual(response.status_code, 200)
1387+
1388+
stale_unit_id = Unit.objects.order_by("-pk").values_list("pk", flat=True)[0] + 1
1389+
session = self.client.session
1390+
search_key = next(key for key in session if key.startswith("search_"))
1391+
session_result = session[search_key]
1392+
session_result["last_viewed_unit_id"] = stale_unit_id
1393+
session[search_key] = session_result
1394+
session.save()
1395+
1396+
response = self.client.get(translate_url, {"offset": high_component_offset + 1})
1397+
self.assertEqual(response.status_code, 200)
1398+
self.assertNotContains(response, "You have shifted from")
1399+
self.assertNotEqual(
1400+
self.client.session[search_key]["last_viewed_unit_id"], stale_unit_id
1401+
)
1402+
13571403
def test_revert_plural(self) -> None:
13581404
source = "Orangutan has %d banana.\n"
13591405
target = [

weblate/trans/tests/test_models.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from weblate.trans.models import (
2828
Announcement,
2929
AutoComponentList,
30+
Category,
3031
Change,
3132
Comment,
3233
Component,
@@ -57,7 +58,7 @@
5758
STATE_READONLY,
5859
STATE_TRANSLATED,
5960
)
60-
from weblate.utils.stats import GlobalStats
61+
from weblate.utils.stats import CategoryLanguage, GlobalStats, ProjectLanguage
6162
from weblate.utils.version import GIT_VERSION
6263

6364

@@ -1065,6 +1066,51 @@ def test_order_by_request(self) -> None:
10651066
).order_by_request({"sort_by": "position,timestamp"}, None)
10661067
self.assertEqual(multiple_ordered_unit.count(), 4)
10671068

1069+
def test_order_by_request_language_scope_orders_by_component(self) -> None:
1070+
category = Category.objects.create(
1071+
name="Docs", slug="docs", project=self.component.project
1072+
)
1073+
Component.objects.filter(pk=self.component.pk).update(
1074+
category=category, priority=120
1075+
)
1076+
high_component = self.create_po(
1077+
category=category,
1078+
name="High",
1079+
priority=80,
1080+
project=self.component.project,
1081+
)
1082+
language = Language.objects.get(code="cs")
1083+
high_translation = high_component.translation_set.get(language=language)
1084+
high_units = list(high_translation.unit_set.order_by("position"))
1085+
low_units = list(
1086+
self.component.translation_set.get(language=language).unit_set.order_by(
1087+
"position"
1088+
)
1089+
)
1090+
high_priority_unit = high_units[-1]
1091+
Unit.objects.filter(pk=high_priority_unit.pk).update(priority=200)
1092+
1093+
expected_ids = [
1094+
high_priority_unit.pk,
1095+
*(unit.pk for unit in high_units if unit.pk != high_priority_unit.pk),
1096+
*(unit.pk for unit in low_units),
1097+
]
1098+
1099+
scopes = [
1100+
ProjectLanguage(self.component.project, language),
1101+
CategoryLanguage(category, language),
1102+
]
1103+
for scope in scopes:
1104+
ordered_ids = list(
1105+
Unit.objects.filter(
1106+
translation__component__in=(self.component, high_component),
1107+
translation__language=language,
1108+
)
1109+
.order_by_request({}, scope)
1110+
.values_list("pk", flat=True)
1111+
)
1112+
self.assertEqual(ordered_ids, expected_ids)
1113+
10681114
def test_get_max_length_no_pk(self) -> None:
10691115
unit = Unit.objects.filter(translation__language_code="cs")[0]
10701116
unit.pk = False

weblate/trans/views/edit.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,37 @@ def handle_suggestions(
621621
return HttpResponseRedirect(redirect_url)
622622

623623

624+
def handle_component_shift_notice(
625+
request: AuthenticatedHttpRequest, unit_set, search_result, unit
626+
) -> None:
627+
"""Show a warning when sequential navigation moves to another component."""
628+
last_viewed_unit_id = search_result.get("last_viewed_unit_id")
629+
if last_viewed_unit_id:
630+
try:
631+
previous_unit = unit_set.get(pk=last_viewed_unit_id)
632+
except Unit.DoesNotExist:
633+
previous_unit = None
634+
if (
635+
previous_unit is not None
636+
and unit.translation.component != previous_unit.translation.component
637+
):
638+
messages.warning(
639+
request,
640+
gettext("You have shifted from %(previous)s to %(current)s.")
641+
% {
642+
"previous": previous_unit.translation.full_slug,
643+
"current": unit.translation.full_slug,
644+
},
645+
)
646+
647+
search_result["last_viewed_unit_id"] = unit.id
648+
session_key = search_result["key"]
649+
if session_key in request.session:
650+
session_result = request.session[session_key]
651+
session_result["last_viewed_unit_id"] = unit.id
652+
request.session[session_key] = session_result
653+
654+
624655
@transaction.atomic
625656
def translate(request: AuthenticatedHttpRequest, path):
626657
"""Translate, suggest and search view."""
@@ -675,19 +706,7 @@ def translate(request: AuthenticatedHttpRequest, path):
675706
messages.error(request, gettext("Invalid search string!"))
676707
return redirect(obj)
677708

678-
last_viewed_unit_id = search_result.get("last_viewed_unit_id")
679-
if last_viewed_unit_id:
680-
previous_unit = unit_set.get(pk=last_viewed_unit_id)
681-
if unit.translation.component != previous_unit.translation.component:
682-
messages.warning(
683-
request,
684-
gettext("You have shifted from %(previous)s to %(current)s.")
685-
% {
686-
"previous": previous_unit.translation.full_slug,
687-
"current": unit.translation.full_slug,
688-
},
689-
)
690-
search_result["last_viewed_unit_id"] = unit.id
709+
handle_component_shift_notice(request, unit_set, search_result, unit)
691710

692711
# Some URLs we will most likely use
693712
base_unit_url = f"{obj.get_translate_url()}?{search_result['url']}&offset="

0 commit comments

Comments
 (0)