Skip to content

Commit

Permalink
Add LibreTranslate machine translator, documentation and its test cases.
Browse files Browse the repository at this point in the history
  • Loading branch information
drivard authored and zerolab committed Dec 20, 2023
1 parent fcf4ce1 commit a458ed8
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- [LibreTranslate](https://libretranslate.com/) machine translator support ([#753](https://github.com/wagtail/wagtail-localize/pull/753)) @drivard

## [1.7] - 2023-11-15

### Added
Expand Down
19 changes: 19 additions & 0 deletions docs/how-to/integrations/machine-translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ WAGTAILLOCALIZE_MACHINE_TRANSLATOR = {
}
```

## LibreTranslate

Website: [https://libretranslate.com/](https://libretranslate.com/)

!!! note

You will need a subscription to get an API key. Alternatively, you can host your own instance. See more details at [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", # or your self-hosted instance URL
# For self-hosted instances without API key setup, use a random string as the API key.
"API_KEY": "<Your LibreTranslate api key here>",
},
}
```

## Dummy

The dummy translator exists primarily for testing Wagtail Localize and it only reverses the strings that are passed to
Expand Down
50 changes: 50 additions & 0 deletions wagtail_localize/machine_translators/libretranslate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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.
API Documentation:
https://libretranslate.com/docs/
"""

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
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import json

from unittest import mock

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
from wagtail_localize.strings import StringValue


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-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")

def test_language_code(self):
self.assertEqual(
self.translator.language_code(self.english_locale.language_code), "en"
)
self.assertEqual(
self.translator.language_code(self.french_locale.language_code), "fr"
)
self.assertEqual(self.translator.language_code("foo-bar-baz"), "foo")

@mock.patch("wagtail_localize.machine_translators.libretranslate.requests.post")
def test_translate_text(self, mock_post):
# Mock the response of requests.post
mock_response = mock.Mock()
mock_response.json.return_value = {
"translatedText": [
"Bonjour le monde!",
"Ceci est une phrase. Ceci est une autre phrase.",
]
}
mock_response.raise_for_status = mock.Mock()
mock_post.return_value = mock_response

input_strings = [
StringValue("Hello world!"),
StringValue("This is a sentence. This is another sentence."),
]

translations = self.translator.translate(
self.english_locale, self.french_locale, input_strings
)

expected_translations = {
StringValue("Hello world!"): StringValue("Bonjour le monde!"),
StringValue("This is a sentence. This is another sentence."): StringValue(
"Ceci est une phrase. Ceci est une autre phrase."
),
}

# Assertions to check if the translation is as expected
self.assertEqual(translations, expected_translations)

# Assert that requests.post was called with the correct arguments
mock_post.assert_called_once_with(
LIBRETRANSLATE_SETTINGS_ENDPOINT["OPTIONS"]["LIBRETRANSLATE_URL"]
+ "/translate",
data=json.dumps(
{
"q": [
"Hello world!",
"This is a sentence. This is another sentence.",
],
"source": "en",
"target": "fr",
"api_key": LIBRETRANSLATE_SETTINGS_ENDPOINT["OPTIONS"]["API_KEY"],
}
),
headers={"Content-Type": "application/json"},
timeout=10,
)

@mock.patch("wagtail_localize.machine_translators.libretranslate.requests.post")
def test_translate_html(self, mock_post):
# Mock the response of requests.post
mock_response = mock.Mock()
mock_response.json.return_value = {
"translatedText": ["""<a id="a1">Bonjour !</a>. <b>C'est un test</b>."""]
}
mock_response.raise_for_status = mock.Mock()
mock_post.return_value = mock_response

input_string, attrs = StringValue.from_source_html(
'<a href="https://en.wikipedia.org/wiki/World">Hello !</a>. <b>This is a test</b>.'
)

translations = self.translator.translate(
self.english_locale, self.french_locale, [input_string]
)

expected_translation = {
input_string: StringValue(
"""<a id="a1">Bonjour !</a>. <b>C'est un test</b>."""
)
}

# Assertions to check if the translation is as expected
self.assertEqual(translations, expected_translation)

# Additional assertion to check the rendered HTML
translated_string = translations[input_string]
rendered_html = translated_string.render_html(attrs)
expected_rendered_html = '<a href="https://en.wikipedia.org/wiki/World">Bonjour !</a>. <b>C\'est un test</b>.'

self.assertEqual(rendered_html, expected_rendered_html)

# Assert that requests.post was called with the correct arguments
mock_post.assert_called_once_with(
LIBRETRANSLATE_SETTINGS_ENDPOINT["OPTIONS"]["LIBRETRANSLATE_URL"]
+ "/translate",
data=json.dumps(
{
"q": [
'<a id="a1">Hello !</a>. <b>This is a test</b>.'
], # Use the string from StringValue
"source": "en",
"target": "fr",
"api_key": LIBRETRANSLATE_SETTINGS_ENDPOINT["OPTIONS"]["API_KEY"],
}
),
headers={"Content-Type": "application/json"},
timeout=10,
)

def test_can_translate(self):
self.assertIsInstance(self.translator, LibreTranslator)

french_locale = Locale.objects.create(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)
)

0 comments on commit a458ed8

Please sign in to comment.