diff --git a/news/467.breaking b/news/467.breaking
new file mode 100644
index 000000000..8f446d76f
--- /dev/null
+++ b/news/467.breaking
@@ -0,0 +1 @@
+Reimplement usage of translators as pluggable utilities and remove Google Translate service @erral
diff --git a/setup.py b/setup.py
index 5e22def17..77ef0188e 100644
--- a/setup.py
+++ b/setup.py
@@ -29,6 +29,8 @@
"plone.memoize",
"plone.protect",
"plone.registry",
+ "plone.rest",
+ "plone.restapi",
"plone.schemaeditor",
"plone.supermodel",
"plone.uuid",
@@ -58,6 +60,8 @@
"plone.testing",
"plone.volto",
"robotsuite",
+ "plone.restapi[test]",
+ "plone.volto",
],
},
entry_points="""
diff --git a/src/plone/app/multilingual/browser/configure.zcml b/src/plone/app/multilingual/browser/configure.zcml
index 0f4d6642c..e7f580e5b 100644
--- a/src/plone/app/multilingual/browser/configure.zcml
+++ b/src/plone/app/multilingual/browser/configure.zcml
@@ -5,6 +5,10 @@
>
+
-
-
-
-
-
diff --git a/src/plone/app/multilingual/browser/translate.py b/src/plone/app/multilingual/browser/translate.py
index 3337e204f..754755cea 100644
--- a/src/plone/app/multilingual/browser/translate.py
+++ b/src/plone/app/multilingual/browser/translate.py
@@ -1,91 +1,7 @@
from Acquisition import aq_inner
-from plone.app.multilingual import _
-from plone.app.multilingual.interfaces import IMultiLanguageExtraOptionsSchema
from plone.app.multilingual.interfaces import ITranslationManager
-from plone.app.uuid.utils import uuidToObject
-from plone.base.interfaces import ILanguage
-from plone.registry.interfaces import IRegistry
from plone.uuid.interfaces import IUUID
from Products.Five import BrowserView
-from zope.component import getUtility
-
-import json
-import urllib
-
-
-def google_translate(question, key, lang_target, lang_source):
- length = len(question)
- translated = ""
- url = "https://www.googleapis.com/language/translate/v2"
- temp_question = question
- while length > 400:
- temp_question = question[:399]
- index = temp_question.rfind(" ")
- temp_question = temp_question[:index]
- question = question[index:]
- length = len(question)
- data = {
- "key": key,
- "target": lang_target,
- "source": lang_source,
- "q": temp_question,
- }
- params = urllib.parse.urlencode(data)
-
- retorn = urllib.request.urlopen(url + "?" + params)
- translated += json.loads(retorn.read())["data"]["translations"][0][
- "translatedText"
- ]
-
- data = {
- "key": key,
- "target": lang_target,
- "source": lang_source,
- "q": temp_question,
- }
- params = urllib.parse.urlencode(data)
-
- retorn = urllib.request.urlopen(url + "?" + params)
- translated += json.loads(retorn.read())["data"]["translations"][0]["translatedText"]
- return json.dumps({"data": translated})
-
-
-class gtranslation_service_dexterity(BrowserView):
- def __call__(self):
- if self.request.method != "POST" and not (
- "field" in self.request.form.keys()
- and "lang_source" in self.request.form.keys()
- ):
- return _("Need a field")
- else:
- context_uid = self.request.form.get("context_uid", None)
- if context_uid is None:
- # try with context if no translation uid is present
- manager = ITranslationManager(self.context)
- else:
- context = uuidToObject(context_uid)
- if context is not None:
- manager = ITranslationManager(context)
- else:
- manager = ITranslationManager(self.context)
-
- registry = getUtility(IRegistry)
- settings = registry.forInterface(
- IMultiLanguageExtraOptionsSchema, prefix="plone"
- )
- lang_target = ILanguage(self.context).get_language()
- lang_source = self.request.form["lang_source"]
- orig_object = manager.get_translation(lang_source)
- field = self.request.form["field"].split(".")[-1]
- if hasattr(orig_object, field):
- question = getattr(orig_object, field, "") or ""
- if hasattr(question, "raw"):
- question = question.raw
- else:
- return _("Invalid field")
- return google_translate(
- question, settings.google_translation_key, lang_target, lang_source
- )
class TranslationForm(BrowserView):
diff --git a/src/plone/app/multilingual/browser/utils.py b/src/plone/app/multilingual/browser/utils.py
index 20382fdef..c7748e3dc 100644
--- a/src/plone/app/multilingual/browser/utils.py
+++ b/src/plone/app/multilingual/browser/utils.py
@@ -4,6 +4,7 @@
from Acquisition import aq_parent
from plone.app.i18n.locales.browser.selector import LanguageSelector
from plone.app.multilingual.browser.selector import LanguageSelectorViewlet
+from plone.app.multilingual.interfaces import IExternalTranslationService
from plone.app.multilingual.interfaces import ILanguageIndependentFolder
from plone.app.multilingual.interfaces import IMultiLanguageExtraOptionsSchema
from plone.app.multilingual.interfaces import ITranslationLocator
@@ -15,6 +16,7 @@
from Products.CMFCore.utils import getToolByName
from Products.Five import BrowserView
from zope.component import getMultiAdapter
+from zope.component import getUtilitiesFor
from zope.component import getUtility
from zope.component.hooks import getSite
@@ -51,13 +53,13 @@ def getPortal(self):
def objToTranslate(self):
return self.context
- def gtenabled(self):
- registry = getUtility(IRegistry)
- settings = registry.forInterface(
- IMultiLanguageExtraOptionsSchema, prefix="plone"
- )
- key = settings.google_translation_key
- return key is not None and len(key.strip()) > 0
+ def translations_enabled(self):
+ utilities = [
+ name
+ for name, utility in getUtilitiesFor(IExternalTranslationService)
+ if utility.is_available()
+ ]
+ return len(utilities) > 0
def languages(self):
"""Deprecated"""
diff --git a/src/plone/app/multilingual/configure.zcml b/src/plone/app/multilingual/configure.zcml
index 659345ecd..999463b55 100644
--- a/src/plone/app/multilingual/configure.zcml
+++ b/src/plone/app/multilingual/configure.zcml
@@ -27,6 +27,11 @@
+
+
@@ -198,5 +203,4 @@
handler=".subscriber.change_language_settings"
/>
-
diff --git a/src/plone/app/multilingual/interfaces.py b/src/plone/app/multilingual/interfaces.py
index d70201d67..1b50a17f2 100644
--- a/src/plone/app/multilingual/interfaces.py
+++ b/src/plone/app/multilingual/interfaces.py
@@ -189,7 +189,6 @@ class IMultiLanguageExtraOptionsSchema(ILanguageSchema):
"redirect_babel_view",
"bypass_languageindependent_field_permission_check",
"buttons_babel_view_up_to_nr_translations",
- "google_translation_key",
"selector_lookup_translations_policy",
],
)
@@ -254,15 +253,6 @@ class IMultiLanguageExtraOptionsSchema(ILanguageSchema):
required=False,
)
- google_translation_key = schema.TextLine(
- title=_("heading_google_translation_key", default="Google Translation API Key"),
- description=_(
- "description_google_translation_key",
- default="Is a paying API in order to use google translation " "service",
- ),
- required=False,
- )
-
selector_lookup_translations_policy = schema.Choice(
title=_(
"heading_selector_lookup_translations_policy",
@@ -278,3 +268,33 @@ class IMultiLanguageExtraOptionsSchema(ILanguageSchema):
required=True,
vocabulary=selector_policies,
)
+
+
+class IExternalTranslationService(Interface):
+ """This interface is provided to allow external translation services
+ to be plugged-in in Plone to use them to translate content
+
+ Register a utility to install a new external translation service.
+
+ To control the order of the services, user the 'order' attribute. The lower
+ the sooner this service will be used.
+
+ The available_languages method can also be used to register the utility
+ just to some language pairs.
+
+ """
+
+ order = schema.Int(title="Order")
+
+ def is_available():
+ """return whether this service is available"""
+
+ def available_languages():
+ """return the list of tuples that represents language pairs this utility is enabled for.
+ An empty list means that all languages are supported
+ """
+
+ def translate_content(content, source_language, target_language):
+ """translate the given content from the source to the target language.
+ It should return the translated string
+ """
diff --git a/src/plone/app/multilingual/profiles/default/metadata.xml b/src/plone/app/multilingual/profiles/default/metadata.xml
index acd6fba1c..e7517f7cb 100644
--- a/src/plone/app/multilingual/profiles/default/metadata.xml
+++ b/src/plone/app/multilingual/profiles/default/metadata.xml
@@ -1,6 +1,6 @@
- 1002
+ 1003
profile-plone.app.dexterity:default
diff --git a/src/plone/app/multilingual/profiles/default/registry.xml b/src/plone/app/multilingual/profiles/default/registry.xml
index 9df569696..8fe8bb493 100644
--- a/src/plone/app/multilingual/profiles/default/registry.xml
+++ b/src/plone/app/multilingual/profiles/default/registry.xml
@@ -32,7 +32,6 @@
True
False
7
-
closest
diff --git a/src/plone/app/multilingual/profiles/upgrades/to_1003/registry.xml b/src/plone/app/multilingual/profiles/upgrades/to_1003/registry.xml
new file mode 100644
index 000000000..859069db0
--- /dev/null
+++ b/src/plone/app/multilingual/profiles/upgrades/to_1003/registry.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/multilingual/restapi/__init__.py b/src/plone/app/multilingual/restapi/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/plone/app/multilingual/restapi/configure.zcml b/src/plone/app/multilingual/restapi/configure.zcml
new file mode 100644
index 000000000..1a82587ca
--- /dev/null
+++ b/src/plone/app/multilingual/restapi/configure.zcml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/app/multilingual/restapi/translate_text.py b/src/plone/app/multilingual/restapi/translate_text.py
new file mode 100644
index 000000000..4f7a73eff
--- /dev/null
+++ b/src/plone/app/multilingual/restapi/translate_text.py
@@ -0,0 +1,30 @@
+from plone.app.multilingual import _
+from plone.app.multilingual.translation_utils import translate_text
+from plone.restapi.deserializer import json_body
+from plone.restapi.services import Service
+
+
+class TranslateTextService(Service):
+ """this endpoints tries to translate the given text into the given language, using the previously registered ExternalTranslationServices"""
+
+ def reply(self):
+ body = json_body(self.request)
+ source_language = body.get("source_language")
+ target_language = body.get("target_language")
+ original_text = body.get("original_text")
+ service = body.get("service")
+
+ translation = translate_text(
+ original_text, source_language, target_language, service
+ )
+
+ if translation is None:
+ self.request.response.setStatus(400)
+ return dict(
+ error=dict(
+ type=_("Translation service not available"),
+ message=_("The requested translation service is not available."),
+ )
+ )
+
+ return {"data": translation}
diff --git a/src/plone/app/multilingual/restapi/translation_service.py b/src/plone/app/multilingual/restapi/translation_service.py
new file mode 100644
index 000000000..76ace9489
--- /dev/null
+++ b/src/plone/app/multilingual/restapi/translation_service.py
@@ -0,0 +1,84 @@
+from plone.app.multilingual import _
+from plone.app.multilingual.interfaces import ITranslationManager
+from plone.app.multilingual.translation_utils import translate_text
+from plone.app.uuid.utils import uuidToObject
+from plone.base.interfaces import ILanguage
+from plone.restapi.deserializer import json_body
+from plone.restapi.services import Service
+
+
+class TranslationService(Service):
+
+ def reply(self):
+ body = json_body(self.request)
+ if "field" not in body:
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": _("Invalid parameter error"),
+ "message": _(
+ "The parameter ${field} is required.",
+ mapping={"field": "field"},
+ ),
+ }
+ }
+
+ if "lang_source" not in body:
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": _("Invalid parameter error"),
+ "message": _(
+ "The parameter ${field} is required.",
+ mapping={"field": "lang_source"},
+ ),
+ }
+ }
+
+ context_uid = body.get("context_uid", None)
+ if context_uid is None:
+ # try with context if no translation uid is present
+ manager = ITranslationManager(self.context)
+ else:
+ context = uuidToObject(context_uid)
+ if context is not None:
+ manager = ITranslationManager(context)
+ else:
+ manager = ITranslationManager(self.context)
+
+ lang_target = ILanguage(self.context).get_language()
+ lang_source = body.get("lang_source")
+ orig_object = manager.get_translation(lang_source)
+ if orig_object is None:
+ return {
+ "error": {
+ "type": _("Invalid content object"),
+ "message": _(
+ "The referenced content object is not available in ${language} language.",
+ mapping={"language": lang_source},
+ ),
+ }
+ }
+
+ field = body.get("field", "").split(".")[-1]
+ if hasattr(orig_object, field):
+ question = getattr(orig_object, field, "") or ""
+ if hasattr(question, "raw"):
+ question = question.raw
+ else:
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": _("Invalid field error"),
+ "message": _(
+ "The field ${field} is not valid.",
+ mapping={"field": field},
+ ),
+ }
+ }
+
+ translation = translate_text(question, lang_source, lang_target)
+ if translation is None:
+ return {"data": ""}
+
+ return {"data": translation}
diff --git a/src/plone/app/multilingual/restapi/translation_services.py b/src/plone/app/multilingual/restapi/translation_services.py
new file mode 100644
index 000000000..315a422e9
--- /dev/null
+++ b/src/plone/app/multilingual/restapi/translation_services.py
@@ -0,0 +1,21 @@
+from plone.app.multilingual.interfaces import IExternalTranslationService
+from plone.restapi.services import Service
+from zope.component import getUtilitiesFor
+
+
+class TranslationServices(Service):
+ """an endpoint to return all translation services registered in a portal that provide the IExternalTranslationService interface"""
+
+ def reply(self):
+ result = []
+
+ for name, adapter in getUtilitiesFor(IExternalTranslationService):
+ item = {}
+ item["order"] = adapter.order
+ item["is_available"] = adapter.is_available()
+ item["available_languages"] = adapter.available_languages()
+ item["name"] = name
+
+ result.append(item)
+
+ return sorted(result, key=lambda x: x["order"], reverse=True)
diff --git a/src/plone/app/multilingual/testing.py b/src/plone/app/multilingual/testing.py
index ce0c132dd..8237f9e26 100644
--- a/src/plone/app/multilingual/testing.py
+++ b/src/plone/app/multilingual/testing.py
@@ -41,6 +41,46 @@ def disableCSRFProtection():
pass
+class NiTranslator:
+ order = 30
+
+ def is_available(self):
+ return True
+
+ def available_languages(self):
+ # All
+ return []
+
+ def translate_content(self, content, source_language, target_language):
+ return f"{content} NI!"
+
+
+class DisabledTranslator:
+ order = 20
+
+ def is_available(self):
+ return False
+
+ def available_languages(self):
+ return []
+
+ def translate_content(self, content, source_language, target_language):
+ return "translation"
+
+
+class CaEsTranslator:
+ order = 5
+
+ def is_available(self):
+ return True
+
+ def available_languages(self):
+ return [("ca", "es")]
+
+ def translate_content(self, content, source_language, target_language):
+ return "text español"
+
+
class PloneAppMultilingualLayer(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
diff --git a/src/plone/app/multilingual/testing.zcml b/src/plone/app/multilingual/testing.zcml
index ae4fbf82a..bffb452a1 100644
--- a/src/plone/app/multilingual/testing.zcml
+++ b/src/plone/app/multilingual/testing.zcml
@@ -5,5 +5,6 @@
+
diff --git a/src/plone/app/multilingual/tests/test_external_translation_utilities.py b/src/plone/app/multilingual/tests/test_external_translation_utilities.py
new file mode 100644
index 000000000..66817dc50
--- /dev/null
+++ b/src/plone/app/multilingual/tests/test_external_translation_utilities.py
@@ -0,0 +1,65 @@
+from plone.app.multilingual.interfaces import IExternalTranslationService
+from plone.app.multilingual.interfaces import IPloneAppMultilingualInstalled
+from plone.app.multilingual.interfaces import ITranslationManager
+from plone.app.multilingual.testing import CaEsTranslator
+from plone.app.multilingual.testing import DisabledTranslator
+from plone.app.multilingual.testing import NiTranslator
+from plone.app.multilingual.testing import PAM_FUNCTIONAL_TESTING
+from plone.app.multilingual.translation_utils import translate_text
+from plone.dexterity.utils import createContentInContainer
+from zope.component import provideUtility
+from zope.interface import alsoProvides
+
+import transaction
+import unittest
+
+
+class TestExternalServicesUtilities(unittest.TestCase):
+ layer = PAM_FUNCTIONAL_TESTING
+
+ def setUp(self):
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ alsoProvides(self.layer["request"], IPloneAppMultilingualInstalled)
+
+ self.a_ca = createContentInContainer(
+ self.portal["ca"], "Document", title="Test document CA"
+ )
+
+ self.a_es = createContentInContainer(
+ self.portal["es"], "Document", title="Test document ES"
+ )
+
+ manager = ITranslationManager(self.a_ca)
+ manager.register_translation("es", self.a_es)
+
+ provideUtility(
+ NiTranslator(), IExternalTranslationService, name="ni_translator"
+ )
+ provideUtility(
+ DisabledTranslator(),
+ IExternalTranslationService,
+ name="disabled_translator",
+ )
+ provideUtility(
+ CaEsTranslator(), IExternalTranslationService, name="ca_es_translator"
+ )
+
+ transaction.commit()
+
+ def test_translation_ca_es(self):
+ """In this case the CaEsTranslator should be applied
+ because it has a smaller number in the order
+ and it has the ca-es language pair translation
+ availability
+ """
+ result = translate_text(self.a_ca.Title(), "ca", "es")
+ self.assertEqual(result, "text español")
+
+ def test_translation_es_ca(self):
+ """In this case the NiTranslator should be applied
+ because the previous translators have not this language
+ pair available or are disabled
+ """
+ result = translate_text(self.a_es.Title(), "es", "ca")
+ self.assertEqual(result, "Test document ES NI!")
diff --git a/src/plone/app/multilingual/tests/test_restapi.py b/src/plone/app/multilingual/tests/test_restapi.py
new file mode 100644
index 000000000..6a8a1a4c6
--- /dev/null
+++ b/src/plone/app/multilingual/tests/test_restapi.py
@@ -0,0 +1,251 @@
+from plone.app.multilingual.interfaces import IExternalTranslationService
+from plone.app.multilingual.interfaces import ITranslationManager
+from plone.app.multilingual.testing import CaEsTranslator
+from plone.app.multilingual.testing import DisabledTranslator
+from plone.app.multilingual.testing import NiTranslator
+from plone.app.multilingual.testing import PAM_ROBOT_TESTING
+from plone.app.testing import setRoles
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
+from plone.app.testing import TEST_USER_ID
+from plone.dexterity.utils import createContentInContainer
+from plone.restapi.testing import RelativeSession
+from zope.component import provideUtility
+
+import transaction
+import unittest
+
+
+class TestDefaultTranslationServices(unittest.TestCase):
+ """Test the default translation services provided by plone.app.multilingual"""
+
+ layer = PAM_ROBOT_TESTING
+
+ def setUp(self):
+ self.app = self.layer["app"]
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ self.portal_url = self.portal.absolute_url()
+ setRoles(self.portal, TEST_USER_ID, ["Manager"])
+
+ self.api_session = RelativeSession(self.portal_url)
+ self.api_session.headers.update({"Accept": "application/json"})
+ self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+
+ def test_available_services(self):
+ """test that by default we have just one available service"""
+ api_result = self.api_session.get("/@translation-services")
+ self.assertEqual(len(api_result.json()), 0)
+
+
+class TestSeveralTranslationServices(unittest.TestCase):
+ """Test that when we register several translation services, those are correctly exposed
+ in the REST API
+ """
+
+ layer = PAM_ROBOT_TESTING
+
+ def setUp(self):
+ self.app = self.layer["app"]
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ self.portal_url = self.portal.absolute_url()
+ setRoles(self.portal, TEST_USER_ID, ["Manager"])
+
+ self.api_session = RelativeSession(self.portal_url)
+ self.api_session.headers.update({"Accept": "application/json"})
+ self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ provideUtility(
+ NiTranslator(), IExternalTranslationService, name="ni_translator"
+ )
+ provideUtility(
+ DisabledTranslator(),
+ IExternalTranslationService,
+ name="disabled_translator",
+ )
+ provideUtility(
+ CaEsTranslator(), IExternalTranslationService, name="ca_es_translator"
+ )
+
+ transaction.commit()
+
+ def test_available_services(self):
+ """test that by default we have just one available service"""
+ api_result = self.api_session.get("/@translation-services")
+ self.assertEqual(len(api_result.json()), 3)
+
+ def test_that_one_is_disabled(self):
+ """we have registered an adapter that is disabled, check that we get that information correctly"""
+ api_result = self.api_session.get("/@translation-services")
+ results = api_result.json()
+
+ disabled_adapters = [
+ adapter for adapter in results if not adapter["is_available"]
+ ]
+ self.assertEqual(len(disabled_adapters), 1)
+
+ disabled_adapter_names = [adapter["name"] for adapter in disabled_adapters]
+ self.assertIn("disabled_translator", disabled_adapter_names)
+
+
+class TestTranslateTextServices(unittest.TestCase):
+ layer = PAM_ROBOT_TESTING
+
+ def setUp(self):
+ self.app = self.layer["app"]
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ self.portal_url = self.portal.absolute_url()
+ setRoles(self.portal, TEST_USER_ID, ["Manager"])
+
+ self.api_session = RelativeSession(self.portal_url)
+ self.api_session.headers.update({"Accept": "application/json"})
+ self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ provideUtility(
+ NiTranslator(), IExternalTranslationService, name="ni_translator"
+ )
+ provideUtility(
+ DisabledTranslator(),
+ IExternalTranslationService,
+ name="disabled_translator",
+ )
+ provideUtility(
+ CaEsTranslator(), IExternalTranslationService, name="ca_es_translator"
+ )
+
+ transaction.commit()
+
+ def test_translation_ca_es(self):
+ """In this case the CaEsTranslator should be applied
+ because it has a smaller number in the order
+ and it has the ca-es language pair translation
+ availability
+ """
+
+ api_result = self.api_session.post(
+ "/@translate-text",
+ json={
+ "original_text": "Some text",
+ "source_language": "ca",
+ "target_language": "es",
+ },
+ )
+ result = api_result.json()
+
+ self.assertEqual(result.get("data"), "text español")
+
+ def test_translation_es_fr(self):
+ """In this case the NiTranslator should be applied
+ because the previous translators have not this language
+ pair available or are disabled
+ """
+ api_result = self.api_session.post(
+ "/@translate-text",
+ json={
+ "original_text": "Some text",
+ "source_language": "es",
+ "target_language": "fr",
+ },
+ )
+ result = api_result.json()
+
+ self.assertEqual(result.get("data"), "Some text NI!")
+
+ def test_translation_given_service(self):
+ """test that using a given translation works, we are going to request the NI translator
+ in a context when in normal circumstances another different translator would be used
+ """
+ api_result = self.api_session.post(
+ "/@translate-text",
+ json={
+ "original_text": "Some other text",
+ "source_language": "ca",
+ "target_language": "es",
+ "service": "ni_translator",
+ },
+ )
+ result = api_result.json()
+
+ self.assertEqual(result.get("data"), "Some other text NI!")
+
+
+class TestTranslationsForBabelEdit(unittest.TestCase):
+ """Here we test the endpoint that is used in the babel_edit view"""
+
+ layer = PAM_ROBOT_TESTING
+
+ def setUp(self):
+ self.app = self.layer["app"]
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ self.portal_url = self.portal.absolute_url()
+ setRoles(self.portal, TEST_USER_ID, ["Manager"])
+
+ self.api_session = RelativeSession(self.portal_url)
+ self.api_session.headers.update({"Accept": "application/json"})
+ self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ provideUtility(
+ NiTranslator(), IExternalTranslationService, name="ni_translator"
+ )
+ provideUtility(
+ DisabledTranslator(),
+ IExternalTranslationService,
+ name="disabled_translator",
+ )
+ provideUtility(
+ CaEsTranslator(), IExternalTranslationService, name="ca_es_translator"
+ )
+
+ self.a_ca = createContentInContainer(
+ self.portal["ca"], "Document", title="Test document CA"
+ )
+
+ self.a_es = createContentInContainer(
+ self.portal["es"], "Document", title="Test document ES"
+ )
+
+ manager = ITranslationManager(self.a_ca)
+ manager.register_translation("es", self.a_es)
+
+ provideUtility(
+ NiTranslator(), IExternalTranslationService, name="ni_translator"
+ )
+ provideUtility(
+ DisabledTranslator(),
+ IExternalTranslationService,
+ name="disabled_translator",
+ )
+ provideUtility(
+ CaEsTranslator(), IExternalTranslationService, name="ca_es_translator"
+ )
+
+ transaction.commit()
+
+ def test_translation_ca_es(self):
+ """In this case the CaEsTranslator should be applied
+ because it has a smaller number in the order
+ and it has the ca-es language pair translation
+ availability
+ """
+ api_result = self.api_session.post(
+ f"{self.a_es.absolute_url()}/@translation-service",
+ json={"field": "IDublinCore.title", "lang_source": "ca"},
+ )
+ self.assertTrue(api_result.ok)
+ result = api_result.json()
+
+ self.assertEqual(result.get("data"), "text español")
+
+ def test_translation_es_ca(self):
+ """In this case the NiTranslator should be applied
+ because the previous translators have not this language
+ pair available or are disabled
+ """
+
+ api_result = self.api_session.post(
+ f"{self.a_es.absolute_url()}/@translation-service",
+ json={"field": "IDublinCore.title", "lang_source": "es"},
+ )
+ self.assertTrue(api_result.ok)
+ result = api_result.json()
+ self.assertEqual(result.get("data"), "Test document ES NI!")
diff --git a/src/plone/app/multilingual/translation_utils.py b/src/plone/app/multilingual/translation_utils.py
new file mode 100644
index 000000000..6f1d5ec60
--- /dev/null
+++ b/src/plone/app/multilingual/translation_utils.py
@@ -0,0 +1,44 @@
+from plone.app.multilingual.interfaces import IExternalTranslationService
+from zope.component import getUtilitiesFor
+from zope.component import getUtility
+
+
+def translate_text(original_text, source_language, target_language, service=None):
+ """translate the text"""
+
+ if original_text:
+ # Initial shortcut: translate only non-empty values
+
+ if service is not None:
+ # if an specific adapter is requested, use it if available
+
+ utility = getUtility(IExternalTranslationService, name=service)
+ if not utility.is_available():
+ return None
+
+ utilities = [utility]
+
+ else:
+ # Get all available adapters
+ utilities = [
+ utility
+ for name, utility in getUtilitiesFor(IExternalTranslationService)
+ if utility.is_available()
+ ]
+
+ sorted_adapters = sorted(utilities, key=lambda x: int(x.order))
+
+ for adapter in sorted_adapters:
+ available_languages = adapter.available_languages()
+ if (
+ not available_languages
+ or (source_language, target_language) in available_languages
+ ):
+ translation = adapter.translate_content(
+ original_text, source_language, target_language
+ )
+
+ if translation:
+ return translation
+
+ return None
diff --git a/src/plone/app/multilingual/upgrades.zcml b/src/plone/app/multilingual/upgrades.zcml
index b5130e650..494089ff0 100644
--- a/src/plone/app/multilingual/upgrades.zcml
+++ b/src/plone/app/multilingual/upgrades.zcml
@@ -91,4 +91,25 @@
handler=".upgrades.fix_indonesian_language"
/>
+
+
+
+
+
+
+
+