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" /> + + + + + + + +