From 8d0d62b09ef287352e7179e45ac33adc42237725 Mon Sep 17 00:00:00 2001 From: Dominick Rivard <643002+drivard@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:16:45 -0500 Subject: [PATCH] Add LibreTranslate machine translator, documentation and its test cases. --- .../integrations/machine-translation.md | 17 ++++ .../machine_translators/libretranslate.py | 47 ++++++++++ .../tests/test_libretranslate_translator.py | 93 +++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 wagtail_localize/machine_translators/libretranslate.py create mode 100644 wagtail_localize/machine_translators/tests/test_libretranslate_translator.py diff --git a/docs/how-to/integrations/machine-translation.md b/docs/how-to/integrations/machine-translation.md index ced048834..d26106261 100644 --- a/docs/how-to/integrations/machine-translation.md +++ b/docs/how-to/integrations/machine-translation.md @@ -128,6 +128,23 @@ WAGTAILLOCALIZE_MACHINE_TRANSLATOR = { } ``` +## LibreTranslate + +Website: [https://libretranslate.com/](https://libretranslate.com/) + +Note that You will need a subscription to get an API key. Or you can host your own instance. +More details are available on the github page [https://github.com/LibreTranslate/LibreTranslate](https://github.com/LibreTranslate/LibreTranslate). + +```python +WAGTAILLOCALIZE_MACHINE_TRANSLATOR = { + "CLASS": "wagtail_localize.machine_translators.libretranslate.LibreTranslator", + "OPTIONS": { + "LIBRETRANSLATE_URL": "https://libretranslate.org", + "API_KEY": "", # Optional on self-hosted instance by providing a random string + }, +} +``` + ## Dummy The dummy translator exists primarily for testing Wagtail Localize and it only reverses the strings that are passed to diff --git a/wagtail_localize/machine_translators/libretranslate.py b/wagtail_localize/machine_translators/libretranslate.py new file mode 100644 index 000000000..6a9a77565 --- /dev/null +++ b/wagtail_localize/machine_translators/libretranslate.py @@ -0,0 +1,47 @@ +import json + +import requests + +from wagtail_localize.machine_translators.base import BaseMachineTranslator +from wagtail_localize.strings import StringValue + + +class LibreTranslator(BaseMachineTranslator): + """ + A machine translator that uses the LibreTranslate API. + """ + + display_name = "LibreTranslate" + + def get_api_endpoint(self): + return self.options["LIBRETRANSLATE_URL"] + + def language_code(self, code): + return code.split("-")[0] + + def translate(self, source_locale, target_locale, strings): + translations = [item.data for item in list(strings)] + response = requests.post( + self.get_api_endpoint() + "/translate", + data=json.dumps( + { + "q": translations, + "source": self.language_code(source_locale.language_code), + "target": self.language_code(target_locale.language_code), + "api_key": self.options["API_KEY"], + } + ), + headers={"Content-Type": "application/json"}, + timeout=10, + ) + response.raise_for_status() + + return { + string: StringValue(translation) + for string, translation in zip(strings, response.json()["translatedText"]) + } + + def can_translate(self, source_locale, target_locale): + return self.language_code(source_locale.language_code) != self.language_code( + target_locale.language_code + ) diff --git a/wagtail_localize/machine_translators/tests/test_libretranslate_translator.py b/wagtail_localize/machine_translators/tests/test_libretranslate_translator.py new file mode 100644 index 000000000..798818c1a --- /dev/null +++ b/wagtail_localize/machine_translators/tests/test_libretranslate_translator.py @@ -0,0 +1,93 @@ +from django.test import TestCase, override_settings +from wagtail.models import Locale + +from wagtail_localize.machine_translators import get_machine_translator +from wagtail_localize.machine_translators.libretranslate import LibreTranslator + + +LIBRETRANSLATE_SETTINGS_ENDPOINT = { + "CLASS": "wagtail_localize.machine_translators.libretranslate.LibreTranslator", + "OPTIONS": { + "LIBRETRANSLATE_URL": "https://libretranslate.org", + "API_KEY": "test-api-key", + }, +} + + +class TestLibreTranslator(TestCase): + @override_settings( + WAGTAILLOCALIZE_MACHINE_TRANSLATOR=LIBRETRANSLATE_SETTINGS_ENDPOINT + ) + def setUp(self): + self.english_locale = Locale.objects.get() + self.french_locale = Locale.objects.create(language_code="fr") + self.translator = get_machine_translator() + + def test_api_endpoint(self): + self.assertIsInstance(self.translator, LibreTranslator) + api_endpoint = self.translator.get_api_endpoint() + self.assertEqual(api_endpoint, "https://libretranslate.org") + + # This probably requires a request to use the API but the test works against my local instance + # def test_translate_text(self): + # self.assertIsInstance(self.translator, LibreTranslator) + + # translations = self.translator.translate( + # self.english_locale, + # self.french_locale, + # { + # StringValue("Hello world!"), + # StringValue("This is a sentence. This is another sentence."), + # }, + # ) + + # self.assertEqual( + # translations, + # { + # StringValue("Hello world!"): StringValue("Bonjour !"), + # StringValue( + # "This is a sentence. This is another sentence." + # ): StringValue("C'est une phrase. C'est une autre phrase."), + # }, + # ) + + # This has been commented out because after a while the public API started + # to return different results for the same input. + # This probably requires a request to use the API but the test works against my local instance + # def test_translate_html(self): + # self.assertIsInstance(self.translator, LibreTranslator) + + # string, attrs = StringValue.from_source_html( + # 'Hello !. This is a test.' + # ) + + # translations = self.translator.translate( + # self.english_locale, self.french_locale, [string] + # ) + + # self.assertEqual( + # translations[string].render_html(attrs), + # "Bonjour ! C'est un test enregistré/b.", + # ) + + def test_can_translate(self): + self.assertIsInstance(self.translator, LibreTranslator) + + french_locale = Locale.objects.get(language_code="fr") + + self.assertTrue( + self.translator.can_translate(self.english_locale, self.french_locale) + ) + self.assertTrue( + self.translator.can_translate(self.english_locale, french_locale) + ) + + # Can't translate the same language + self.assertFalse( + self.translator.can_translate(self.english_locale, self.english_locale) + ) + + # Can't translate two variants of the same language + self.assertFalse( + self.translator.can_translate(self.french_locale, french_locale) + )