diff --git a/CHANGELOG.md b/CHANGELOG.md index 542b1e936d..3415a7eea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ UNRELEASED ---------- * [ [#1319](https://github.com/digitalfabrik/integreat-cms/issues/1319) ] Fix error on Imprint API +* [ [#1103](https://github.com/digitalfabrik/integreat-cms/issues/1103) ] Add automatic translations via DeepL API 2022.3.6 diff --git a/Pipfile.lock b/Pipfile.lock index 73f46b72ba..d166edd24d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -323,6 +323,14 @@ "markers": "python_version >= '3.7'", "version": "==0.5.0" }, + "deepl": { + "hashes": [ + "sha256:6f60d107707d2692a12020dd9b490a16d7be56afe2e853d2d768635acc7b7700", + "sha256:d7272aed1dff7bc703ab8a52db9418aed91cb32e4b858a9cb1b7e180ead197e2" + ], + "markers": "python_full_version >= '3.6.2' and python_version < '4'", + "version": "==1.4.1" + }, "deprecated": { "hashes": [ "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", diff --git a/integreat_cms/cms/urls/protected.py b/integreat_cms/cms/urls/protected.py index 1e54970fc0..bbbdbd9eb3 100644 --- a/integreat_cms/cms/urls/protected.py +++ b/integreat_cms/cms/urls/protected.py @@ -5,7 +5,14 @@ """ from django.urls import include, path -from ..forms import LanguageForm, OfferTemplateForm, OrganizationForm, RegionForm +from ..forms import ( + LanguageForm, + OfferTemplateForm, + OrganizationForm, + RegionForm, + EventTranslationForm, + POITranslationForm, +) from ..models import Event, Language, OfferTemplate, Organization, Page, POI, Role from ..views import ( @@ -835,7 +842,7 @@ path( "auto-translate/", bulk_action_views.BulkAutoTranslateView.as_view( - model=Event + model=Event, form=EventTranslationForm ), name="automatic_translation_events", ), @@ -918,7 +925,7 @@ path( "auto-translate/", bulk_action_views.BulkAutoTranslateView.as_view( - model=POI + model=POI, form=POITranslationForm ), name="automatic_translation_pois", ), diff --git a/integreat_cms/cms/views/bulk_action_views.py b/integreat_cms/cms/views/bulk_action_views.py index a157fc8270..4ae8788ee7 100644 --- a/integreat_cms/cms/views/bulk_action_views.py +++ b/integreat_cms/cms/views/bulk_action_views.py @@ -13,6 +13,7 @@ from django.views.generic.list import MultipleObjectMixin from cacheops import invalidate_model +from ...deepl_api.utils import DeepLApi logger = logging.getLogger(__name__) @@ -100,6 +101,9 @@ class BulkAutoTranslateView(BulkActionView): #: Whether the public translation objects should be prefetched prefetch_translations = True + #: the form of this bulk action + form = None + def post(self, request, *args, **kwargs): r""" Translate multiple objects automatically @@ -116,14 +120,23 @@ def post(self, request, *args, **kwargs): :return: The redirect :rtype: ~django.http.HttpResponseRedirect """ + if not settings.DEEPL_ENABLED: + messages.error(request, _("Automatic translations are disabled")) + return super().post(request, *args, **kwargs) + # Collect the corresponding objects logger.debug("Automatic translation for: %r", self.get_queryset()) - - if settings.DEEPL_ENABLED: - messages.warning( - request, _("Automatic translations are not fully implemented yet") + deepl = DeepLApi() + if deepl.check_availability(request, kwargs.get("language_slug")): + deepl.deepl_translation( + request, self.get_queryset(), kwargs.get("language_slug"), self.form ) else: - messages.error(request, _("Automatic translations are disabled")) + messages.warning( + request, + _( + "This language is not supported by DeepL. Please try another language" + ), + ) # Let the base view handle the redirect return super().post(request, *args, **kwargs) diff --git a/integreat_cms/core/settings.py b/integreat_cms/core/settings.py index 1e9892b048..01f97f916d 100644 --- a/integreat_cms/core/settings.py +++ b/integreat_cms/core/settings.py @@ -446,6 +446,10 @@ "handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL, }, + "deepl": { + "handlers": ["console", "logfile"], + "level": DEPS_LOG_LEVEL, + }, "django": { "handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL, diff --git a/integreat_cms/deepl_api/__init__.py b/integreat_cms/deepl_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integreat_cms/deepl_api/utils.py b/integreat_cms/deepl_api/utils.py new file mode 100644 index 0000000000..d34a16fa9d --- /dev/null +++ b/integreat_cms/deepl_api/utils.py @@ -0,0 +1,146 @@ +import logging +import deepl + +from django.conf import settings +from django.contrib import messages +from django.utils.translation import ugettext as _ + +logger = logging.getLogger(__name__) + + +class DeepLApi: + """ + DeepL API to auto translate selected posts. + """ + + def __init__(self): + """ + Initialize the DeepL client + """ + self.translator = deepl.Translator(settings.DEEPL_AUTH_KEY) + + def check_availability(self, request, language_slug): + """ + This function checks, if the selected language is supported by DeepL + + :param request: request that was sent to the server + :type request: ~django.http.HttpRequest + + :param language_slug: current language slug + :type language_slug: str + + :return: true or false + :rtype: bool + """ + supported_source_languages = [ + source_language.code.lower() + for source_language in self.translator.get_source_languages() + ] + supported_target_languages = [ + target_languages.code.lower()[:2] + for target_languages in self.translator.get_target_languages() + ] + source_language = request.region.get_source_language(language_slug) + return ( + source_language + and source_language.slug in supported_source_languages + and language_slug in supported_target_languages + ) + + def deepl_translation(self, request, content_objects, language_slug, form_class): + """ + This functions gets the translation from DeepL + + :param request: passed request + :type request: ~django.http.HttpRequest + + :param content_objects: passed content objects + :type content_objects: ~django.db.models.query.QuerySet [ ~integreat_cms.cms.models.abstract_content_model.AbstractContentModel ] + + :param language_slug: current GUI language slug + :type language_slug: str + + :param form_class: passed Form class of content type + :type form_class: ~integreat_cms.cms.forms.custom_content_model_form.CustomContentModelForm + """ + # Get target language + target_language = request.region.get_language_or_404(language_slug) + source_language = request.region.get_source_language(language_slug) + for content_object in content_objects: + source_translation = content_object.get_translation(source_language.slug) + if not source_translation: + messages.error( + request, + _('No source translation could be found for {} "{}".').format( + type(content_object)._meta.verbose_name.title(), + content_object.best_translation.title, + ), + ) + continue + existing_target_translation = content_object.get_translation( + target_language.slug + ) + # For some languages, the DeepL client expects the BCP tag instead of the short language code + if target_language.slug in ("en", "pt"): + target_language_key = target_language.bcp47_tag + else: + target_language_key = target_language.slug + target_title = self.translator.translate_text( + source_translation.title, + source_lang=source_language.slug, + target_lang=target_language_key, + ) + data = { + "title": target_title, + "status": existing_target_translation.status + if existing_target_translation + else source_translation.status, + } + if source_translation.content: + data["content"] = self.translator.translate_text( + source_translation.content, + source_lang=source_language.slug, + target_lang=target_language_key, + ) + # for pois adds a short description + if hasattr(source_translation, "short_description"): + data["short_description"] = self.translator.translate_text( + source_translation.short_description, + source_lang=source_language.slug, + target_lang=target_language_key, + ) + content_translation_form = form_class( + data=data, + instance=existing_target_translation, + additional_instance_attributes={ + "creator": request.user, + "language": target_language, + source_translation.foreign_field(): content_object, + }, + ) + # Validate event translation + if content_translation_form.is_valid(): + content_translation_form.save() + logger.debug( + "Successfully translated for: %r", content_translation_form.instance + ) + messages.success( + request, + _('{} "{}" has been successfully translated.').format( + type(content_object)._meta.verbose_name.title(), + source_translation.title, + ), + ) + else: + logger.error( + "Automatic translation for %r could not be created because of %r", + content_object, + content_translation_form.errors, + ) + messages.error( + request, + _('{} "{}" could not be automatically translated.').format( + type(content_object)._meta.verbose_name.title(), + source_translation.title, + ), + ) diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index 14378d1da3..7697d12851 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-03-20 18:58+0000\n" +"POT-Creation-Date: 2022-03-23 12:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Integreat \n" "Language-Team: Integreat \n" @@ -3121,7 +3121,7 @@ msgstr "Feedback" #: cms/templates/_base.html:181 #: cms/templates/push_notifications/push_notification_list.html:9 -#: core/settings.py:712 +#: core/settings.py:716 msgid "News" msgstr "Nachrichten" @@ -5699,19 +5699,21 @@ msgstr "" "Adresse eingegeben haben, mit der Sie sich registriert haben, und überprüfen " "Sie Ihren Spam-Ordner." -#: cms/views/bulk_action_views.py:123 -msgid "Automatic translations are not fully implemented yet" -msgstr "Automatische Übersetzungen sind noch nicht vollständig implementiert" - -#: cms/views/bulk_action_views.py:126 +#: cms/views/bulk_action_views.py:124 msgid "Automatic translations are disabled" msgstr "Automatische Übersetzungen sind deaktiviert" -#: cms/views/bulk_action_views.py:164 +#: cms/views/bulk_action_views.py:137 +msgid "This language is not supported by DeepL. Please try another language" +msgstr "" +"Diese Sprache wird von DeepL nicht unterstützt. Bitte versuchen Sie eine " +"andere Sprache" + +#: cms/views/bulk_action_views.py:177 msgid "The selected {} were successfully archived" msgstr "Die ausgewählten {} wurden erfolgreich archiviert" -#: cms/views/bulk_action_views.py:203 +#: cms/views/bulk_action_views.py:216 msgid "The selected {} were successfully restored" msgstr "Die ausgewählten {} wurden erfolgreich wiederhergestellt" @@ -6599,6 +6601,18 @@ msgid "Superuser permissions need to be set by another superuser." msgstr "" "Administratorrechte müssen von einem anderen Administrator vergeben werden." +#: deepl_api/utils.py:74 +msgid "No source translation could be found for {} \"{}\"." +msgstr "Es konnte kein Quelltext für {} \"{}\" gefunden werden." + +#: deepl_api/utils.py:129 +msgid "{} \"{}\" has been successfully translated." +msgstr "{} \"{}\" wurde erfolgreich übersetzt" + +#: deepl_api/utils.py:142 +msgid "{} \"{}\" could not be automatically translated." +msgstr "{} \"{}\" konnte nicht automatisch übersetzt werden." + #: xliff/utils.py:152 msgid "" "Page {} does not have a source translation in {} and therefore cannot be " @@ -6650,6 +6664,10 @@ msgstr "" "Diese Seite konnte nicht importiert werden, da sie zu einer anderen Region " "gehört ({})." +#~ msgid "Automatic translations are not fully implemented yet" +#~ msgstr "" +#~ "Automatische Übersetzungen sind noch nicht vollständig implementiert" + #~ msgid "Filetype:" #~ msgstr "Dateityp:" @@ -6663,6 +6681,9 @@ msgstr "" #~ msgid "Export XLIFF for translation to" #~ msgstr "Exportiere XLIFF für Übersetzung nach" +#~ msgid "POI has been successfully translated" +#~ msgstr "POI wurde erfolgreich übersetzt." + #~ msgid "Access token to update the page content" #~ msgstr "Zugangs-Token um Seiten-Inhalte zu aktualisieren" @@ -6930,9 +6951,6 @@ msgstr "" #~ msgid "Event was successfully created and published" #~ msgstr "Veranstaltung wurde erfolgreich erstellt und veröffentlicht" -#~ msgid "Event was successfully created" -#~ msgstr "Veranstaltung wurde erfolgreich erstellt" - #~ msgid "Event translation was successfully created and published" #~ msgstr "" #~ "Veranstaltungsübersetzung wurde erfolgreich erstellt und veröffentlicht" @@ -8000,9 +8018,6 @@ msgstr "" #~ msgid "POI was successfully created and published." #~ msgstr "POI wurde erfolgreich erstellt und veröffentlicht." -#~ msgid "POI was successfully created." -#~ msgstr "POI wurde erfolgreich erstellt." - #~ msgid "POI was successfully published." #~ msgstr "POI wurde erfolgreich veröffentlicht." diff --git a/setup.cfg b/setup.cfg index 4d6a4098e8..87777779e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,7 @@ install_requires = argon2-cffi bcrypt cffi + deepl Django>=3.2,<4.0 django-cacheops django-cors-headers