diff --git a/docs/admin/machine.rst b/docs/admin/machine.rst index d327a7274a1a..72da1cb9a8b7 100644 --- a/docs/admin/machine.rst +++ b/docs/admin/machine.rst @@ -26,6 +26,27 @@ The services translate from the source language as configured at :ref:`machine-translation` +.. _mt-alibaba: + +Alibaba +------- + +:Service ID: ``alibaba`` +:Configuration: +------------+-------------------+--+ + | ``key`` | Access key ID | | + +------------+-------------------+--+ + | ``secret`` | Access key secret | | + +------------+-------------------+--+ + | ``region`` | Region ID | | + +------------+-------------------+--+ + +Alibaba Translate is a neural machine translation service for translating text +and it supports up to 214 language pairs. + +.. seealso:: + + `Alibaba Translate Documentation `_ + .. _mt-amagama: Amagama diff --git a/requirements-optional.txt b/requirements-optional.txt index 588e8090dc65..fbab0d720223 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -1,4 +1,6 @@ -r requirements.txt +# Alibaba +aliyun-python-sdk-alimt>=3.2.0,<4.0.0 # Amazon boto3>=1.28.62,<1.34.0 # LDAP diff --git a/weblate/machinery/alibaba.py b/weblate/machinery/alibaba.py new file mode 100644 index 000000000000..d88a3c321366 --- /dev/null +++ b/weblate/machinery/alibaba.py @@ -0,0 +1,287 @@ +# Copyright © Michal Čihař +# +# SPDX-License-Identifier: GPL-3.0-or-later + + +import json + +from aliyunsdkalimt.request.v20181012 import TranslateGeneralRequest +from aliyunsdkcore.client import AcsClient +from django.utils.functional import cached_property + +from .base import MachineTranslation, MachineTranslationError +from .forms import AlibabaMachineryForm + + +class AlibabaTranslation(MachineTranslation): + """Alibaba API machine translation support.""" + + name = "Alibaba" + max_score = 80 + + language_map = { + "zh_Hans": "zh", + "zh_Hant": "zh-tw", + } + + settings_form = AlibabaMachineryForm + + @cached_property + def client(self): + return AcsClient( + ak=self.settings["key"], + secret=self.settings["secret"], + region_id=self.settings["region"], + ) + + def download_languages(self): + """List of supported languages.""" + return [ + "ab", + "sq", + "ak", + "ar", + "an", + "am", + "as", + "az", + "ast", + "nch", + "ee", + "ay", + "ga", + "et", + "oj", + "oc", + "or", + "om", + "os", + "tpi", + "ba", + "eu", + "be", + "ber", + "bm", + "pag", + "bg", + "se", + "bem", + "byn", + "bi", + "bal", + "is", + "pl", + "bs", + "fa", + "bho", + "br", + "ch", + "cbk", + "cv", + "ts", + "tt", + "da", + "shn", + "tet", + "de", + "nds", + "sco", + "dv", + "kdx", + "dtp", + "ru", + "fo", + "fr", + "sa", + "fil", + "fj", + "fi", + "fur", + "fvr", + "kg", + "km", + "ngu", + "kl", + "ka", + "gos", + "gu", + "gn", + "kk", + "ht", + "ko", + "ha", + "nl", + "cnr", + "hup", + "gil", + "rn", + "quc", + "ky", + "gl", + "ca", + "cs", + "kab", + "kn", + "kr", + "csb", + "kha", + "kw", + "xh", + "co", + "mus", + "crh", + "tlh", + "hbs", + "qu", + "ks", + "ku", + "la", + "ltg", + "lv", + "lo", + "lt", + "li", + "ln", + "lg", + "lb", + "rue", + "rw", + "ro", + "rm", + "rom", + "jbo", + "mg", + "gv", + "mt", + "mr", + "ml", + "ms", + "chm", + "mk", + "mh", + "kek", + "mai", + "mfe", + "mi", + "mn", + "bn", + "my", + "hmn", + "umb", + "nv", + "af", + "ne", + "niu", + "no", + "pmn", + "pap", + "pa", + "pt", + "ps", + "ny", + "tw", + "chr", + "ja", + "sv", + "sm", + "sg", + "si", + "hsb", + "eo", + "sl", + "sw", + "so", + "sk", + "tl", + "tg", + "ty", + "te", + "ta", + "th", + "to", + "toi", + "ti", + "tvl", + "tyv", + "tr", + "tk", + "wa", + "war", + "cy", + "ve", + "vo", + "wo", + "udm", + "ur", + "uz", + "es", + "ie", + "fy", + "szl", + "he", + "hil", + "haw", + "el", + "lfn", + "sd", + "hu", + "sn", + "ceb", + "syr", + "su", + "hy", + "ace", + "iba", + "ig", + "io", + "ilo", + "iu", + "it", + "yi", + "ia", + "hi", + "id", + "inh", + "en", + "yo", + "vi", + "zza", + "jv", + "zh", + "zh-tw", + "yue", + "zu", + ] + + def download_translations( + self, + source, + language, + text: str, + unit, + user, + threshold: int = 75, + ): + """Download list of possible translations from a service.""" + # Create an API request and set the request parameters. + request = TranslateGeneralRequest.TranslateGeneralRequest() + request.set_SourceLanguage(source) # source language + request.set_SourceText(text) # original + request.set_TargetLanguage(language) + request.set_FormatType("text") + request.set_method("POST") + + # Initiate the API request and obtain the response. + response = self.client.do_action_with_exception(request) + payload = json.loads(response) + if "Message" in payload: + raise MachineTranslationError( + f"Error {payload['Code']}: {payload['Message']}" + ) + + yield { + "text": payload["Data"]["Translated"], + "quality": self.max_score, + "service": self.name, + "source": text, + } diff --git a/weblate/machinery/forms.py b/weblate/machinery/forms.py index af633ac8cf5c..d66f2e0217f2 100644 --- a/weblate/machinery/forms.py +++ b/weblate/machinery/forms.py @@ -224,6 +224,18 @@ class AWSMachineryForm(KeySecretMachineryForm): ) +class AlibabaMachineryForm(KeySecretMachineryForm): + key = forms.CharField( + label=pgettext_lazy("Alibaba Translate configuration", "Access key ID") + ) + secret = forms.CharField( + label=pgettext_lazy("Alibaba Translate configuration", "Access key secret") + ) + region = forms.CharField( + label=pgettext_lazy("Alibaba Translate configuration", "Region ID") + ) + + class ModernMTMachineryForm(KeyURLMachineryForm): url = forms.URLField( label=pgettext_lazy("Automatic suggestion service configuration", "API URL"), diff --git a/weblate/machinery/models.py b/weblate/machinery/models.py index ca2db8fba972..9a49488a8f60 100644 --- a/weblate/machinery/models.py +++ b/weblate/machinery/models.py @@ -16,6 +16,7 @@ class WeblateConf(AppConf): WEBLATE_MACHINERY = ( "weblate.machinery.apertium.ApertiumAPYTranslation", "weblate.machinery.aws.AWSTranslation", + "weblate.machinery.alibaba.AlibabaTranslation", "weblate.machinery.baidu.BaiduTranslation", "weblate.machinery.deepl.DeepLTranslation", "weblate.machinery.glosbe.GlosbeTranslation", diff --git a/weblate/machinery/tests.py b/weblate/machinery/tests.py index d36f93e88477..2b79a6a9f512 100644 --- a/weblate/machinery/tests.py +++ b/weblate/machinery/tests.py @@ -13,6 +13,7 @@ import httpx import responses import respx +from aliyunsdkcore.client import AcsClient from botocore.stub import ANY, Stubber from django.core.management import call_command from django.core.management.base import CommandError @@ -28,6 +29,7 @@ from weblate.checks.tests.test_checks import MockUnit from weblate.configuration.models import Setting from weblate.lang.models import Language +from weblate.machinery.alibaba import AlibabaTranslation from weblate.machinery.apertium import ApertiumAPYTranslation from weblate.machinery.aws import AWSTranslation from weblate.machinery.baidu import BAIDU_API, BaiduTranslation @@ -1328,6 +1330,40 @@ def test_batch(self, machine=None): super().test_batch(machine=machine) +class AlibabaTranslationTest(BaseMachineTranslationTest): + MACHINE_CLS = AlibabaTranslation + EXPECTED_LEN = 1 + NOTSUPPORTED = "tog" + CONFIGURATION = { + "key": "key", + "secret": "secret", + "region": "cn-hangzhou", + } + + def mock_empty(self): + raise SkipTest("Not tested") + + def mock_error(self): + raise SkipTest("Not tested") + + def mock_response(self): + patcher = patch.object( + AcsClient, + "do_action_with_exception", + Mock( + return_value=json.dumps( + { + "RequestId": "14E447CA-B93B-4526-ACD7-42AE13CC2AF6", + "Data": {"Translated": "Hello"}, + "Code": 200, + } + ) + ), + ) + patcher.start() + self.addCleanup(patcher.stop) + + class IBMTranslationTest(BaseMachineTranslationTest): MACHINE_CLS = IBMTranslation EXPECTED_LEN = 1