Skip to content
Merged
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
19 changes: 19 additions & 0 deletions integreat_cms/cms/models/abstract_content_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,25 @@ def create_new_version_copy(

return new_translation

def save_new_version(
self,
user: User | None = None,
) -> AbstractContentTranslation:
"""
Create and save a new minor-edit version of this translation
"""
from ..utils.content_translation_utils import save_new_version_with_retry
from ..utils.link_ignore_preservation import preserve_ignored_links

new_version = self.create_new_version_copy(user)

with transaction.atomic(), preserve_ignored_links(self):
self.links.all().delete()
return save_new_version_with_retry(
new_version,
new_version.save,
)

def replace_urls(
self,
urls_to_replace: dict[str, str],
Expand Down
92 changes: 68 additions & 24 deletions integreat_cms/core/utils/machine_translation_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from ...cms.constants.machine_translatable_fields import TRANSLATABLE_FIELDS
from ...cms.constants.machine_translation_budget import MINIMAL
from ...cms.models.abstract_content_translation import AbstractContentTranslation
from ...cms.utils.stringify_list import iter_to_string
from ...textlab_api.utils import check_hix_score
from .word_count import word_count
Expand All @@ -31,7 +32,6 @@
Region,
)
from ...cms.models.abstract_content_model import AbstractContentModel
from ...cms.models.abstract_content_translation import AbstractContentTranslation


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -59,6 +59,7 @@ class MachineTranslationApiClient(ABC):
form_class: ModelFormMetaclass
#: Successful translations
successful_translations: list[TranslationContext] = []
refreshed_translations: list[TranslationContext] = []
#: Translations with an attached API failure
failed_translations: list[str] = []
#: Content objects untranslatable since no actual changes were made
Expand Down Expand Up @@ -98,6 +99,7 @@ def reset(self) -> None:
shown to the user.
"""
self.successful_translations = []
self.refreshed_translations = []
self.failed_translations = []
self.failed_because_no_changes_made = []
self.failed_because_no_source_translation = []
Expand Down Expand Up @@ -172,15 +174,30 @@ def translate_queryset(
self.model_name = meta.verbose_name.title()
self.model_name_plural = meta.verbose_name_plural

# Prepare all content objects
all_contexts = self.prepare_content_objects()

# Separate contexts into those with changes and those without
changed_contexts = []
unchanged_contexts = []

for ctx in all_contexts:
if bool(ctx.translatable_attributes):
changed_contexts.append(ctx)
else:
unchanged_contexts.append(ctx)

self.handle_unchanged_contexts(unchanged_contexts)

# Filter out content objects which can not be translated
context = self.prepare_content_objects()
context = self.filter_no_source_translation(context)
context = self.filter_unchanged_translations(context)
context = self.filter_insufficient_hix_score(context)
context = self.filter_exceeds_limit(context)
contexts_to_translate = self.filter_no_source_translation(changed_contexts)
contexts_to_translate = self.filter_insufficient_hix_score(
contexts_to_translate
)
contexts_to_translate = self.filter_exceeds_limit(contexts_to_translate)

# Provider-API-specific implementation
self.invoke_translation_api(context)
# Provider-API-specific implementation for changed contexts
self.invoke_translation_api(contexts_to_translate)

# Update remaining budget of the region
region.mt_budget_used += sum(
Expand All @@ -190,38 +207,42 @@ def translate_queryset(

# Show success/error messages to the user
self.alert_successful_translations()
self.alert_refreshed_translations()
self.alert_failed_translations()
self.alert_no_changes_made()
self.alert_no_source_translation()
self.alert_insufficient_hix_score()
self.alert_exceeds_limit()
self.alert_too_long_text()

def filter_unchanged_translations(
self, context: list[TranslationContext]
) -> list[TranslationContext]:
def handle_unchanged_contexts(
self, unchanged_contexts: list[TranslationContext]
) -> None:
"""
This method filters out entries from the context list
if there have been no changes made to the source_translation.
Handle content objects without changes.

The removed entries are stored in order to show users
batched error messages after all objects have been handled.
If there are no changes but the target translation is outdated, a new
translation version is created with ``minor_edit=True`` to refresh its
status without flagging it as a content change.
Otherwise, the translation is not updated because no changes were detected.

:param context: The list of translation contexts to filter
:return: The filtered list of translation contexts
Push notifications are excluded from this processing because they do not
use ``AbstractContentTranslation`` and therefore have no outdated status
to refresh.
"""
filtered_context = []

for ctx in context:
if bool(ctx.translatable_attributes):
filtered_context.append(ctx)
for ctx in unchanged_contexts:
target_translation = ctx.existing_target_translation
if (
isinstance(target_translation, AbstractContentTranslation)
and target_translation.is_outdated
):
target_translation.save_new_version(self.request.user)
self.refreshed_translations.append(ctx)
else:
self.failed_because_no_changes_made.append(
ctx.instance.best_translation.title
)

return filtered_context

def filter_no_source_translation(
self, context: list[TranslationContext]
) -> list[TranslationContext]:
Expand Down Expand Up @@ -469,6 +490,29 @@ def save_translation(self, ctx: TranslationContext, translation_data: dict) -> N
else:
self.mark_unsuccessful(ctx, content_translation_form.errors)

def alert_refreshed_translations(self) -> None:
"""
Add messages informing the user about refreshed translations
"""
if self.refreshed_translations:
messages.info(
self.request,
ngettext_lazy(
"{model_name} {object_names}: Translation into '{target_language}' was not necessary -> no changes detected. The translation date has been refreshed.",
"{model_name} {object_names}: Translation into '{target_language}' was not necessary -> no changes detected. The translation dates have been refreshed.",
len(self.refreshed_translations),
).format(
model_name=self.model_name,
model_name_plural=self.model_name_plural,
target_language=self.target_language,
object_names=iter_to_string(
ctx.source_translation.title
for ctx in self.refreshed_translations
if ctx.source_translation is not None
),
),
)

def alert_successful_translations(self) -> None:
"""
Add messages informing the user about successful translations
Expand Down
17 changes: 17 additions & 0 deletions integreat_cms/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -11766,6 +11766,23 @@ msgstr "Englisch"
msgid "Dutch"
msgstr "Niederländisch"

#: core/utils/machine_translation_api_client.py
#, python-brace-format
msgid ""
"{model_name} {object_names}: Translation into '{target_language}' was not "
"necessary -> no changes detected. The translation date has been refreshed."
msgid_plural ""
"{model_name} {object_names}: Translation into '{target_language}' was not "
"necessary -> no changes detected. The translation dates have been refreshed."
msgstr[0] ""
"{model_name} {object_names}: Übersetzung ins '{target_language}' war nicht "
"erforderlich -> keine Änderungen festgestellt. Das Übersetzungsdatum wurde "
"aktualisiert."
msgstr[1] ""
"{model_name} {object_names}: Übersetzung ins '{target_language}' war nicht "
"erforderlich -> keine Änderungen festgestellt. Die Übersetzungsdaten wurden "
"aktualisiert."

#: core/utils/machine_translation_api_client.py
#, python-brace-format
msgid ""
Expand Down
2 changes: 2 additions & 0 deletions integreat_cms/release_notes/current/unreleased/4320.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
en: Update translation state to MACHINE_TRANSLATED when an outdated translation is machine translated without changes
de: Setze den Übersetzungsstatus auf MACHINE_TRANSLATED, wenn eine veraltete Übersetzung ohne Änderungen maschinell übersetzt wird
88 changes: 88 additions & 0 deletions tests/mt_api/mt_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from integreat_cms.cms.constants.translation_status import (
MACHINE_TRANSLATED,
OUTDATED,
UP_TO_DATE,
)

Expand Down Expand Up @@ -570,6 +571,93 @@ def test_manual_update_mt_page(
assert en_translation.minor_edit is True


@pytest.mark.django_db
@pytest.mark.parametrize("login_role_user", [EDITOR], indirect=True)
Comment thread
seluianova marked this conversation as resolved.
def test_mt_update_refreshes_outdated_translation_no_changes(
load_test_data: None,
login_role_user: tuple[Client, str],
settings: SettingsWrapper,
mock_server: MockServer,
caplog: LogCaptureFixture,
) -> None:
"""
When a source translation is changed and reverted with MT update enabled:
- target translation state is changed from OUTDATED to MACHINE_TRANSLATED
- the minor_edit flag for the new translation version is set to True
- no request is sent to the MT API
"""
mt_setup(["de"], ["en-gb", "en-us"], [], [], settings, mock_server)

client, _role = login_role_user

region = Region.objects.get(slug=REGION_SLUG)

# Create a page with empty content
page_id = _create_page(client, REGION_SLUG, "Titel", "titel", "")

# Enable automatic translation and create a new English translation
_edit_translation(
client,
page_id,
language_slug="de",
title="Titel",
content="",
region=region,
mt_translations_to_create="en",
)

en_translation = PageTranslation.objects.filter(
page__id=page_id, language__slug="en"
).first()

assert en_translation
assert en_translation.content == ""
assert en_translation.translation_state == MACHINE_TRANSLATED
assert mock_server.requests_counter == 1

# Change content of the German translation, without automatic translation
_edit_translation(
client,
page_id,
language_slug="de",
title="Titel",
content="<p>Inhalt</p>",
)

en_translation = PageTranslation.objects.filter(
page__id=page_id, language__slug="en"
).first()

assert en_translation
assert en_translation.translation_state == OUTDATED

# Clear the German content again and update the English translation
_edit_translation(
client,
page_id,
language_slug="de",
title="Titel",
content="",
region=region,
mt_translations_to_update="en",
)

en_translation = PageTranslation.objects.filter(
page__id=page_id, language__slug="en"
).first()

assert en_translation
assert en_translation.content == ""
assert en_translation.minor_edit is True
assert en_translation.translation_state == MACHINE_TRANSLATED
assert mock_server.requests_counter == 1 # No new MT request should have been made

assert_message_in_log(
"INFO Page \"Titel\": Translation into 'English' was not necessary -> no changes detected. The translation date has been refreshed.",
caplog,
)


def _create_page(
client: Client,
region_slug: str,
Expand Down
Loading