Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/admin/projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,23 @@ 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
+++++++++++++++++++++++++++++++++++++

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}}``
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will break rendering without newline:

Suggested change
``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}``
``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}``

Also, the seealso should be present in both of thse.

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RST directives generally need a blank line before them. Here .. seealso:: directly follows the example URL line, which can trigger Sphinx/reST warnings or mis-parsing. Add a blank line between the example and the .. seealso:: directive.

Suggested change
``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}``
``https://github.com/WeblateOrg/translations/blob/{{branch}}/{{filename}}#L{{line}}``

Copilot uses AI. Check for mistakes.

.. seealso::

* :setting:`PROJECT_WEB_RESTRICT_PRIVATE`
Expand Down
3 changes: 3 additions & 0 deletions weblate/trans/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1691,6 +1691,7 @@ class Meta:
"push",
"push_branch",
"repoweb",
"repoweb_translations",
"push_on_commit",
"commit_pending_age",
"merge_style",
Expand Down Expand Up @@ -1791,6 +1792,7 @@ def __init__(self, request: AuthenticatedHttpRequest, *args, **kwargs) -> None:
"push",
"push_branch",
"repoweb",
"repoweb_translations",
),
Fieldset(
gettext("Version control settings"),
Expand Down Expand Up @@ -1948,6 +1950,7 @@ class Meta:
"push",
"push_branch",
"repoweb",
"repoweb_translations",
"file_format",
"file_format_params",
"filemask",
Expand Down
27 changes: 27 additions & 0 deletions weblate/trans/migrations/0074_component_repoweb_translations.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration is not based on the current main:

Conflicting migrations detected; multiple leaf nodes in the migration graph: (0069_component_repoweb_translations, 0073_alter_change_action in trans).

Please rename it and adjust dependencies.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from django.db import migrations, models

import weblate.utils.render


class Migration(migrations.Migration):
dependencies = [
("trans", "0073_alter_change_action"),
]

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",
),
),
]
17 changes: 16 additions & 1 deletion weblate/trans/models/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,18 @@ class Component( # noqa: PLR0904
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,
Expand Down Expand Up @@ -1807,6 +1819,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.
Expand All @@ -1815,7 +1828,9 @@ def get_repoweb_link(
here.
"""
Comment on lines 1824 to 1829
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring for get_repoweb_link now says it generates a link to the “source code browser”, but the new is_translation flag makes it generate translation file links as well. Update the docstring to reflect the broader behavior so callers understand when to use is_translation vs. the default.

Copilot uses AI. Check for mistakes.
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):
Comment on lines 1830 to 1835
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_translation influences template selection here, but the method later delegates to linked_component.get_repoweb_link(...) without passing is_translation. If template is still falsy at delegation time (e.g., relying on the linked component’s settings), translation links will be generated using the linked component’s non-translation fallback. Ensure the delegation path preserves the is_translation intent so linked repositories can use repoweb_translations correctly.

Copilot uses AI. Check for mistakes.
template = getattr(
Expand Down
10 changes: 8 additions & 2 deletions weblate/trans/templatetags/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,13 @@ 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`.
Expand All @@ -924,7 +930,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)
Expand Down
32 changes: 32 additions & 0 deletions weblate/trans/tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -2180,3 +2180,35 @@ 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
),
)
30 changes: 15 additions & 15 deletions weblate/trans/views/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,16 +62,16 @@
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
Expand All @@ -84,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:
Expand Down Expand Up @@ -514,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)

Expand Down Expand Up @@ -791,6 +795,7 @@ def translate(request: AuthenticatedHttpRequest, path):
"1",
unit,
user.profile,
is_translation=True,
),
},
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down