From b92788a6680b8e2f8ced375464c8e297e5e856e3 Mon Sep 17 00:00:00 2001 From: Brendan Date: Wed, 22 Apr 2026 19:07:46 -0400 Subject: [PATCH 1/6] Add separate repository browser URL for translation files --- weblate/screenshots/views.py | 2 +- weblate/trans/forms.py | 3 ++ .../0069_component_repoweb_translations.py | 19 +++++++++++++ weblate/trans/models/component.py | 17 ++++++++++- weblate/trans/tests/test_component.py | 28 +++++++++++++++++++ weblate/trans/views/edit.py | 7 ++--- 6 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 weblate/trans/migrations/0069_component_repoweb_translations.py diff --git a/weblate/screenshots/views.py b/weblate/screenshots/views.py index 4dd0eaf68826..9874b81fe008 100644 --- a/weblate/screenshots/views.py +++ b/weblate/screenshots/views.py @@ -18,7 +18,7 @@ from django.views.decorators.http import require_POST from django.views.generic import DetailView, ListView from PIL import Image -from tesserocr import OEM, PSM, RIL, PyTessBaseAPI, iterate_level +# from tesserocr import OEM, PSM, RIL, PyTessBaseAPI, iterate_level from weblate.logger import LOGGER from weblate.screenshots.forms import ScreenshotEditForm, ScreenshotForm, SearchForm diff --git a/weblate/trans/forms.py b/weblate/trans/forms.py index b9245e38f59d..d06dfcc5a57b 100644 --- a/weblate/trans/forms.py +++ b/weblate/trans/forms.py @@ -1683,6 +1683,7 @@ class Meta: "push", "push_branch", "repoweb", + "repoweb_translations", "push_on_commit", "commit_pending_age", "merge_style", @@ -1783,6 +1784,7 @@ def __init__(self, request: AuthenticatedHttpRequest, *args, **kwargs) -> None: "push", "push_branch", "repoweb", + "repoweb_translations", ), Fieldset( gettext("Version control settings"), @@ -1891,6 +1893,7 @@ class Meta: "push", "push_branch", "repoweb", + "repoweb_translations", "file_format", "file_format_params", "filemask", diff --git a/weblate/trans/migrations/0069_component_repoweb_translations.py b/weblate/trans/migrations/0069_component_repoweb_translations.py new file mode 100644 index 000000000000..6b8315a087d7 --- /dev/null +++ b/weblate/trans/migrations/0069_component_repoweb_translations.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.3 on 2026-04-22 21:02 + +import weblate.utils.render +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('trans', '0068_unit_source_tm_index'), + ] + + operations = [ + migrations.AddField( + model_name='component', + name='repoweb_translations', + field=models.CharField(blank=True, help_text='Link to repository browser for translation files, use {{branch}} for branch, {{filename}} and {{line}} as filename and line placeholders. If left empty, the Repository browser above will be used. You might want to strip leading directory by using {{filename|parentdir}}.', max_length=200, validators=[weblate.utils.render.validate_repoweb], verbose_name='Repository browser for translations'), + ), + ] diff --git a/weblate/trans/models/component.py b/weblate/trans/models/component.py index b92175aea2bd..67692af79b1d 100644 --- a/weblate/trans/models/component.py +++ b/weblate/trans/models/component.py @@ -494,6 +494,18 @@ class Component( validators=[validate_repoweb], blank=True, ) + repoweb_translations = models.CharField( + verbose_name=gettext_lazy("Repository browser for translations"), + max_length=200, + help_text=gettext_lazy( + "Link to repository browser for translation files, use {{branch}} for branch, " + "{{filename}} and {{line}} as filename and line placeholders. " + "If left empty, the Repository browser above will be used. " + "You might want to strip leading directory by using {{filename|parentdir}}." + ), + validators=[validate_repoweb], + blank=True, + ) git_export = models.CharField( verbose_name=gettext_lazy("Exported repository URL"), max_length=60 + PROJECT_NAME_LENGTH + COMPONENT_NAME_LENGTH, @@ -1605,6 +1617,7 @@ def get_repoweb_link( line: str, template: str | None = None, user: User | None = None, + is_translation: bool = False, ): """ Generate link to source code browser for given file and line. @@ -1613,7 +1626,9 @@ def get_repoweb_link( here. """ if not template: - if self.repoweb: + if is_translation and self.repoweb_translations: + template = self.repoweb_translations + elif self.repoweb: template = self.repoweb elif user and user.has_perm("vcs.view", self): template = getattr( diff --git a/weblate/trans/tests/test_component.py b/weblate/trans/tests/test_component.py index 06e6e82d6120..2bd201bf0f89 100644 --- a/weblate/trans/tests/test_component.py +++ b/weblate/trans/tests/test_component.py @@ -1265,3 +1265,31 @@ def test_repo_link_generation_azure(self) -> None: "https://dev.azure.com/f/c/_git/ATEST/blob/main/test.py#L42", self.get_url(), ) + + def test_translation_repoweb(self): + """Test that repoweb_translations is used for translation file links.""" + self.component.repoweb = ( + "https://example.com/source/{{branch}}/f/{{filename}}#_{{line}}" + ) + self.component.repoweb_translations = ( + "https://example.com/translations/{{branch}}/f/{{filename}}#_{{line}}" + ) + self.assertEqual( + "https://example.com/source/main/f/test.py#_42", + self.component.get_repoweb_link("test.py", "42", user=self.user), + ) + self.assertEqual( + "https://example.com/translations/main/f/test.po#_1", + self.component.get_repoweb_link("test.po", "1", user=self.user, is_translation=True), + ) + + def test_translation_repoweb_fallback(self): + """Test that repoweb is used as fallback when repoweb_translations is blank.""" + self.component.repoweb = ( + "https://example.com/source/{{branch}}/f/{{filename}}#_{{line}}" + ) + self.component.repoweb_translations = "" + self.assertEqual( + "https://example.com/source/main/f/test.po#_1", + self.component.get_repoweb_link("test.po", "1", user=self.user, is_translation=True), + ) diff --git a/weblate/trans/views/edit.py b/weblate/trans/views/edit.py index b758b49c7af9..7db06fefbe1e 100644 --- a/weblate/trans/views/edit.py +++ b/weblate/trans/views/edit.py @@ -786,15 +786,14 @@ def translate(request: AuthenticatedHttpRequest, path): unit.translation, user, initial={"variant": unit.pk} ), "screenshot_form": screenshot_form, - "translation_file_link": lambda: try_linkify_filename( - unit.translation.filename, + "translation_file_link": lambda:unit.translation.component.get_repoweb_link( unit.translation.filename, # '1' as a placeholder, because `get_repoweb_link` can't currently # generate links without line specified. Although it's ok to use # '' or '0' on GitHub or GitLab, let's play it safe for now. "1", - unit, - user.profile, + is_translation=True, + user=user, ), }, ) From 7e54b1aa7d6167aa6495bf5f73513a54694b7335 Mon Sep 17 00:00:00 2001 From: Brendan Date: Wed, 22 Apr 2026 19:19:24 -0400 Subject: [PATCH 2/6] Restore tesserocr import --- weblate/screenshots/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/weblate/screenshots/views.py b/weblate/screenshots/views.py index 9874b81fe008..4dd0eaf68826 100644 --- a/weblate/screenshots/views.py +++ b/weblate/screenshots/views.py @@ -18,7 +18,7 @@ from django.views.decorators.http import require_POST from django.views.generic import DetailView, ListView from PIL import Image -# from tesserocr import OEM, PSM, RIL, PyTessBaseAPI, iterate_level +from tesserocr import OEM, PSM, RIL, PyTessBaseAPI, iterate_level from weblate.logger import LOGGER from weblate.screenshots.forms import ScreenshotEditForm, ScreenshotForm, SearchForm From d0b805852d4ba7da9ff7410dd16fd937c7803d58 Mon Sep 17 00:00:00 2001 From: Brendan Date: Wed, 22 Apr 2026 19:49:48 -0400 Subject: [PATCH 3/6] fix: add separate repository browser URL for translation files --- docs/admin/projects.rst | 13 +++++++++++ .../0069_component_repoweb_translations.py | 22 +++++++++++++------ weblate/trans/tests/test_component.py | 10 ++++++--- weblate/trans/views/edit.py | 19 ++++++++-------- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/docs/admin/projects.rst b/docs/admin/projects.rst index 6550d250230b..e5a4a1b5f937 100644 --- a/docs/admin/projects.rst +++ b/docs/admin/projects.rst @@ -454,6 +454,19 @@ might want to strip leading directory by ``parentdir`` filter (see :ref:`markup`): ``https://github.com/WeblateOrg/hello/blob/{{branch}}/{{filename|parentdir}}#L{{line}}`` +.. _component-repoweb-translations: + +Repository browser for translations ++++++++++++++++++++++++++++++++++++++ + +URL of repository browser used to display translation files. When empty, the +:ref:`component-repoweb` URL will be used as a fallback. This is useful when +the source files and translation files are hosted in different repositories. +You can use :ref:`markup`. + +For example on GitHub, use something like: +``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}`` + .. _component-git_export: Exported repository URL diff --git a/weblate/trans/migrations/0069_component_repoweb_translations.py b/weblate/trans/migrations/0069_component_repoweb_translations.py index 6b8315a087d7..37cf1e6cd416 100644 --- a/weblate/trans/migrations/0069_component_repoweb_translations.py +++ b/weblate/trans/migrations/0069_component_repoweb_translations.py @@ -1,19 +1,27 @@ -# Generated by Django 6.0.3 on 2026-04-22 21:02 +# Copyright © Michal Čihař +# +# SPDX-License-Identifier: GPL-3.0-or-later -import weblate.utils.render from django.db import migrations, models +import weblate.utils.render + class Migration(migrations.Migration): - dependencies = [ - ('trans', '0068_unit_source_tm_index'), + ("trans", "0068_unit_source_tm_index"), ] operations = [ migrations.AddField( - model_name='component', - name='repoweb_translations', - field=models.CharField(blank=True, help_text='Link to repository browser for translation files, use {{branch}} for branch, {{filename}} and {{line}} as filename and line placeholders. If left empty, the Repository browser above will be used. You might want to strip leading directory by using {{filename|parentdir}}.', max_length=200, validators=[weblate.utils.render.validate_repoweb], verbose_name='Repository browser for translations'), + model_name="component", + name="repoweb_translations", + field=models.CharField( + blank=True, + help_text="Link to repository browser for translation files, use {{branch}} for branch, {{filename}} and {{line}} as filename and line placeholders. If left empty, the Repository browser above will be used. You might want to strip leading directory by using {{filename|parentdir}}.", + max_length=200, + validators=[weblate.utils.render.validate_repoweb], + verbose_name="Repository browser for translations", + ), ), ] diff --git a/weblate/trans/tests/test_component.py b/weblate/trans/tests/test_component.py index 2bd201bf0f89..755b36ec1867 100644 --- a/weblate/trans/tests/test_component.py +++ b/weblate/trans/tests/test_component.py @@ -1265,7 +1265,7 @@ def test_repo_link_generation_azure(self) -> None: "https://dev.azure.com/f/c/_git/ATEST/blob/main/test.py#L42", self.get_url(), ) - + def test_translation_repoweb(self): """Test that repoweb_translations is used for translation file links.""" self.component.repoweb = ( @@ -1280,7 +1280,9 @@ def test_translation_repoweb(self): ) self.assertEqual( "https://example.com/translations/main/f/test.po#_1", - self.component.get_repoweb_link("test.po", "1", user=self.user, is_translation=True), + self.component.get_repoweb_link( + "test.po", "1", user=self.user, is_translation=True + ), ) def test_translation_repoweb_fallback(self): @@ -1291,5 +1293,7 @@ def test_translation_repoweb_fallback(self): self.component.repoweb_translations = "" self.assertEqual( "https://example.com/source/main/f/test.po#_1", - self.component.get_repoweb_link("test.po", "1", user=self.user, is_translation=True), + self.component.get_repoweb_link( + "test.po", "1", user=self.user, is_translation=True + ), ) diff --git a/weblate/trans/views/edit.py b/weblate/trans/views/edit.py index 7db06fefbe1e..7c890e0d436d 100644 --- a/weblate/trans/views/edit.py +++ b/weblate/trans/views/edit.py @@ -58,7 +58,6 @@ from weblate.trans.models.unit import fill_in_source_translation from weblate.trans.tasks import auto_translate from weblate.trans.templatetags.translations import ( - try_linkify_filename, unit_state_class, unit_state_title, ) @@ -786,14 +785,16 @@ def translate(request: AuthenticatedHttpRequest, path): unit.translation, user, initial={"variant": unit.pk} ), "screenshot_form": screenshot_form, - "translation_file_link": lambda:unit.translation.component.get_repoweb_link( - unit.translation.filename, - # '1' as a placeholder, because `get_repoweb_link` can't currently - # generate links without line specified. Although it's ok to use - # '' or '0' on GitHub or GitLab, let's play it safe for now. - "1", - is_translation=True, - user=user, + "translation_file_link": lambda: ( + unit.translation.component.get_repoweb_link( + unit.translation.filename, + # '1' as a placeholder, because `get_repoweb_link` can't currently + # generate links without line specified. Although it's ok to use + # '' or '0' on GitHub or GitLab, let's play it safe for now. + "1", + is_translation=True, + user=user, + ) ), }, ) From 2f90a0628319ce412fbd54f6f712346942d9aa43 Mon Sep 17 00:00:00 2001 From: Brendan Date: Thu, 23 Apr 2026 19:11:04 -0400 Subject: [PATCH 4/6] updated migration file --- ...b_translations.py => 0074_component_repoweb_translations.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename weblate/trans/migrations/{0069_component_repoweb_translations.py => 0074_component_repoweb_translations.py} (95%) diff --git a/weblate/trans/migrations/0069_component_repoweb_translations.py b/weblate/trans/migrations/0074_component_repoweb_translations.py similarity index 95% rename from weblate/trans/migrations/0069_component_repoweb_translations.py rename to weblate/trans/migrations/0074_component_repoweb_translations.py index 37cf1e6cd416..584eeb208862 100644 --- a/weblate/trans/migrations/0069_component_repoweb_translations.py +++ b/weblate/trans/migrations/0074_component_repoweb_translations.py @@ -9,7 +9,7 @@ class Migration(migrations.Migration): dependencies = [ - ("trans", "0068_unit_source_tm_index"), + ("trans", "0073_alter_change_action"), ] operations = [ From d45be02e4781ec3786879cefe1c23df668dc045e Mon Sep 17 00:00:00 2001 From: Brendan Date: Thu, 23 Apr 2026 19:45:14 -0400 Subject: [PATCH 5/6] fix: address review feedback on translation repoweb --- docs/admin/projects.rst | 5 +++ weblate/trans/templatetags/translations.py | 4 +- weblate/trans/views/edit.py | 50 +++++++++++----------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/admin/projects.rst b/docs/admin/projects.rst index f34b894d2b7a..f13f99ce184e 100644 --- a/docs/admin/projects.rst +++ b/docs/admin/projects.rst @@ -457,6 +457,10 @@ might want to strip leading directory by ``parentdir`` filter (see :ref:`markup`): ``https://github.com/WeblateOrg/hello/blob/{{branch}}/{{filename|parentdir}}#L{{line}}`` +.. seealso:: + + * :setting:`PROJECT_WEB_RESTRICT_PRIVATE` + .. _component-repoweb-translations: Repository browser for translations @@ -469,6 +473,7 @@ You can use :ref:`markup`. For example on GitHub, use something like: ``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}`` + .. seealso:: * :setting:`PROJECT_WEB_RESTRICT_PRIVATE` diff --git a/weblate/trans/templatetags/translations.py b/weblate/trans/templatetags/translations.py index 29290638fa5c..c1c6ec498806 100644 --- a/weblate/trans/templatetags/translations.py +++ b/weblate/trans/templatetags/translations.py @@ -911,7 +911,7 @@ def unit_state_title(unit) -> str: def try_linkify_filename( - text, filename: str, line: str, unit, profile, link_class: str = "" + text, filename: str, line: str, unit, profile, link_class: str = "", is_translation: bool = False ): """ Attempt to convert `text` to a repo link to `filename:line`. @@ -924,7 +924,7 @@ def try_linkify_filename( link = text elif profile: link = unit.translation.component.get_repoweb_link( - filename, line, profile.editor_link + filename, line, profile.editor_link, is_translation=is_translation ) if link: return format_html(SOURCE_LINK, link, text, link_class) diff --git a/weblate/trans/views/edit.py b/weblate/trans/views/edit.py index 007c89d6e23e..3dea4af6d38c 100644 --- a/weblate/trans/views/edit.py +++ b/weblate/trans/views/edit.py @@ -25,7 +25,7 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.html import format_html -from django.utils.translation import gettext, gettext_lazy, ngettext +from django.utils.translation import gettext, ngettext from django.views.decorators.http import require_POST from weblate.checks.models import CHECKS, get_display_checks @@ -58,19 +58,20 @@ from weblate.trans.models.unit import fill_in_source_translation from weblate.trans.tasks import auto_translate from weblate.trans.templatetags.translations import ( + try_linkify_filename, unit_state_class, unit_state_title, ) -from weblate.trans.util import redirect_next, render +from weblate.trans.util import redirect_next, render, split_plural from weblate.utils import messages from weblate.utils.antispam import is_spam from weblate.utils.hash import hash_to_checksum from weblate.utils.html import format_html_join_comma, list_to_tuples -from weblate.utils.lock import WeblateLockTimeoutError from weblate.utils.messages import get_message_kind from weblate.utils.ratelimit import revert_rate_limit, session_ratelimit_post from weblate.utils.state import ( STATE_APPROVED, + STATE_NEEDS_REWRITING, STATE_TRANSLATED, ) from weblate.utils.stats import CategoryLanguage, ProjectLanguage @@ -83,9 +84,6 @@ ) SESSION_SEARCH_CACHE_TTL = 1800 -DELETE_UNIT_LOCKED_MESSAGE = gettext_lazy( - "Could not remove the string because another background operation is in progress. Please try again later." -) def display_fixups(request: AuthenticatedHttpRequest, fixups: list[str]) -> None: @@ -513,11 +511,18 @@ def handle_revert(unit, request: AuthenticatedHttpRequest, next_unit_url): ) return None - if not change.revert( - request.user, change_action=ActionEvents.REVERT, request=request - ): - messages.error(request, gettext("Could not revert the selected change.")) + if not change.can_revert(): + messages.error(request, gettext("Can not revert to empty translation!")) return None + # Store unit + unit.translate( + request.user, + split_plural(change.old), + STATE_NEEDS_REWRITING + if change.action == ActionEvents.MARKED_EDIT + else unit.state, + change_action=ActionEvents.REVERT, + ) # Redirect to next entry return HttpResponseRedirect(next_unit_url) @@ -781,16 +786,16 @@ def translate(request: AuthenticatedHttpRequest, path): unit.translation, user, initial={"variant": unit.pk} ), "screenshot_form": screenshot_form, - "translation_file_link": lambda: ( - unit.translation.component.get_repoweb_link( - unit.translation.filename, - # '1' as a placeholder, because `get_repoweb_link` can't currently - # generate links without line specified. Although it's ok to use - # '' or '0' on GitHub or GitLab, let's play it safe for now. - "1", - is_translation=True, - user=user, - ) + "translation_file_link": lambda: try_linkify_filename( + unit.translation.filename, + unit.translation.filename, + # '1' as a placeholder, because `get_repoweb_link` can't currently + # generate links without line specified. Although it's ok to use + # '' or '0' on GitHub or GitLab, let's play it safe for now. + "1", + unit, + user.profile, + is_translation=True, ), }, ) @@ -860,8 +865,6 @@ def auto_translation(request: AuthenticatedHttpRequest, path): threshold=autoform.cleaned_data["threshold"], ) messages.success(request, result["message"]) - for warning in result.get("warnings", []): - messages.warning(request, warning) else: task = auto_translate.delay( translation_id=translation_id, @@ -1134,9 +1137,6 @@ def delete_unit(request: AuthenticatedHttpRequest, unit_id): try: unit.translation.delete_unit(request, unit) - except WeblateLockTimeoutError: - messages.error(request, DELETE_UNIT_LOCKED_MESSAGE) - return redirect(unit) except FileParseError as error: unit.translation.component.update_import_alerts(delete=False) messages.error(request, gettext("Could not remove the string: %s") % error) From 2ae1b0330e4658b2af94cbd5b891e24193080872 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:48:43 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- weblate/trans/templatetags/translations.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/weblate/trans/templatetags/translations.py b/weblate/trans/templatetags/translations.py index c1c6ec498806..d331533a574f 100644 --- a/weblate/trans/templatetags/translations.py +++ b/weblate/trans/templatetags/translations.py @@ -911,7 +911,13 @@ def unit_state_title(unit) -> str: def try_linkify_filename( - text, filename: str, line: str, unit, profile, link_class: str = "", is_translation: bool = False + text, + filename: str, + line: str, + unit, + profile, + link_class: str = "", + is_translation: bool = False, ): """ Attempt to convert `text` to a repo link to `filename:line`.