From 16bf96ab58e2290e77431ff3701052262205a56d Mon Sep 17 00:00:00 2001 From: ahmadabuwardeh Date: Sat, 20 Jan 2024 07:04:15 -0800 Subject: [PATCH] feat: use XBlockI18NService js translations --- CHANGELOG.rst | 5 ++++ lti_consumer/__init__.py | 2 +- lti_consumer/lti_xblock.py | 59 +++++++++++++++++++++++++++----------- lti_consumer/utils.py | 16 +++++++++++ 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f6fe13f0..35b592d2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,11 @@ Unreleased ------------------ * Add NewRelic traces to functions suspected of causing performance issues. +Version 9.9.0 (2024-01-24) +--------------------------- + +* XBlockI18NService js translations support + 9.8.1 - 2023-11-17 ------------------ * Fix custom_parameters xblock field validation. diff --git a/lti_consumer/__init__.py b/lti_consumer/__init__.py index 02aa0744..0ba711e2 100644 --- a/lti_consumer/__init__.py +++ b/lti_consumer/__init__.py @@ -4,4 +4,4 @@ from .apps import LTIConsumerApp from .lti_xblock import LtiConsumerXBlock -__version__ = '9.8.2' +__version__ = '9.9.0' diff --git a/lti_consumer/lti_xblock.py b/lti_consumer/lti_xblock.py index bf18b4b2..fb8cb16e 100644 --- a/lti_consumer/lti_xblock.py +++ b/lti_consumer/lti_xblock.py @@ -87,6 +87,7 @@ external_user_id_1p1_launches_enabled, database_config_enabled, EXTERNAL_ID_REGEX, + DummyTranslationService, ) log = logging.getLogger(__name__) @@ -257,6 +258,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock): """ block_settings_key = 'lti_consumer' + i18n_js_namespace = 'LtiI18N' display_name = String( display_name=_("Display Name"), @@ -663,10 +665,13 @@ def workbench_scenarios(): return scenarios @staticmethod - def _get_statici18n_js_url(loader): # pragma: no cover + def _get_deprecated_i18n_js_url(loader): """ - Returns the Javascript translation file for the currently selected language, if any found by + Returns the deprecated JavaScript translation file for the currently selected language, if any found by `pkg_resources` + + This method returns pre-OEP-58 i18n files and is deprecated in favor + of `get_javascript_i18n_catalog_url`. """ lang_code = translation.get_language() if not lang_code: @@ -678,17 +683,30 @@ def _get_statici18n_js_url(loader): # pragma: no cover return text_js.format(lang_code=code) return None + def _get_statici18n_js_url(self, loader): + """ + Return the JavaScript translation file provided by the XBlockI18NService. + """ + if url_getter_func := getattr(self.i18n_service, 'get_javascript_i18n_catalog_url', None): + if javascript_url := url_getter_func(self): + return javascript_url + + if deprecated_url := self._get_deprecated_i18n_js_url(loader): + return self.runtime.local_resource_url(self, deprecated_url) + + return None + def validate_field_data(self, validation, data): # Validate custom parameters is a list. if not isinstance(data.custom_parameters, list): - _ = self.runtime.service(self, "i18n").ugettext + _ = self.i18n_service.ugettext validation.add(ValidationMessage(ValidationMessage.ERROR, str( _("Custom Parameters must be a list") ))) # Validate custom parameters format. if not all(map(CUSTOM_PARAMETER_REGEX.match, data.custom_parameters)): - _ = self.runtime.service(self, 'i18n').ugettext + _ = self.i18n_service.ugettext validation.add(ValidationMessage(ValidationMessage.ERROR, str( _('Custom Parameters should be strings in "x=y" format.'), ))) @@ -698,7 +716,7 @@ def validate_field_data(self, validation, data): data.config_type == 'external' and not (data.external_config and EXTERNAL_ID_REGEX.match(str(data.external_config))) ): - _ = self.runtime.service(self, 'i18n').ugettext + _ = self.i18n_service.ugettext validation.add(ValidationMessage(ValidationMessage.ERROR, str( _('Reusable configuration ID must be set when using external config (Example: "x:y").'), ))) @@ -714,7 +732,7 @@ def validate(self): Validate this XBlock's configuration """ validation = super().validate() - _ = self.runtime.service(self, "i18n").ugettext + _ = self.i18n_service.ugettext # Check if lti_id exists in the LTI passports of the current course. (LTI 1.1 only) # This validation is just for the Unit page in Studio; we don't want to block users from saving # a new LTI ID before they've added it to advanced settings, but we do want to warn them about it. @@ -776,6 +794,15 @@ def get_pii_sharing_enabled(self): # because the service is not defined in those contexts. return True + @property + def i18n_service(self): + """ Obtains translation service """ + i18n_service = self.runtime.service(self, "i18n") + if i18n_service: + return i18n_service + else: + return DummyTranslationService() + @property def editable_fields(self): """ @@ -852,7 +879,7 @@ def role(self): """ user = self.runtime.service(self, 'user').get_current_user() if not user.opt_attrs["edx-platform.is_authenticated"]: - raise LtiError(self.ugettext("Could not get user data for current request")) + raise LtiError(self.i18n_service.ugettext("Could not get user data for current request")) return user.opt_attrs.get('edx-platform.user_role', 'student') @@ -878,7 +905,7 @@ def lti_provider_key_secret(self): raise ValueError key = ':'.join(key) except ValueError as err: - msg = self.ugettext( + msg = self.i18n_service.ugettext( 'Could not parse LTI passport: {lti_passport!r}. Should be "id:key:secret" string.' ).format(lti_passport=lti_passport) raise LtiError(msg) from err @@ -897,7 +924,7 @@ def lms_user_id(self): 'edx-platform.user_id', None) if user_id is None: - raise LtiError(self.ugettext("Could not get user id for current request")) + raise LtiError(self.i18n_service.ugettext("Could not get user id for current request")) return user_id @property @@ -911,7 +938,7 @@ def anonymous_user_id(self): 'edx-platform.anonymous_user_id', None) if user_id is None: - raise LtiError(self.ugettext("Could not get user id for current request")) + raise LtiError(self.i18n_service.ugettext("Could not get user id for current request")) return str(user_id) def get_icon_class(self): @@ -927,7 +954,7 @@ def external_user_id(self): """ user_id = self.runtime.service(self, 'user').get_external_user_id('lti') if user_id is None: - raise LtiError(self.ugettext("Could not get user id for current request")) + raise LtiError(self.i18n_service.ugettext("Could not get user id for current request")) return str(user_id) def get_lti_1p1_user_id(self): @@ -1061,8 +1088,8 @@ def prefixed_custom_parameters(self): try: param_name, param_value = [p.strip() for p in custom_parameter.split('=', 1)] except ValueError as err: - _ = self.runtime.service(self, "i18n").ugettext - msg = self.ugettext( + _ = self.i18n_service.ugettext + msg = self.i18n_service.ugettext( 'Could not parse custom parameter: {custom_parameter!r}. Should be "x=y" string.' ).format(custom_parameter=custom_parameter) raise LtiError(msg) from err @@ -1130,7 +1157,7 @@ def extract_real_user_data(self): user = self.runtime.service(self, 'user').get_current_user() if not user.opt_attrs["edx-platform.is_authenticated"]: - raise LtiError(self.ugettext("Could not get user data for current request")) + raise LtiError(self.i18n_service.ugettext("Could not get user data for current request")) user_data = { 'user_email': None, @@ -1192,7 +1219,7 @@ def author_view(self, context): loader.render_django_template( '/templates/html/lti_1p3_studio.html', context, - i18n_service=self.runtime.service(self, 'i18n') + i18n_service=self.i18n_service ), ) fragment.add_css(loader.load_unicode('static/css/student.css')) @@ -1226,7 +1253,7 @@ def student_view(self, context): fragment.add_javascript(loader.load_unicode('static/js/xblock_lti_consumer.js')) statici18n_js_url = self._get_statici18n_js_url(loader) if statici18n_js_url: - fragment.add_javascript_url(self.runtime.local_resource_url(self, statici18n_js_url)) + fragment.add_javascript_url(statici18n_js_url) fragment.initialize_js('LtiConsumerXBlock') return fragment diff --git a/lti_consumer/utils.py b/lti_consumer/utils.py index a6acbd4d..fb0a36d4 100644 --- a/lti_consumer/utils.py +++ b/lti_consumer/utils.py @@ -31,6 +31,14 @@ def _(text): return text +def ngettext_fallback(text_singular, text_plural, number): + """ Dummy `ngettext` replacement to make string extraction tools scrape strings marked for translation """ + if number == 1: + return text_singular + else: + return text_plural + + def get_lti_api_base(): """ Returns base url to be used as issuer on OAuth2 flows @@ -361,3 +369,11 @@ def model_to_dict(model_object, exclude=None): return object_fields except (AttributeError, TypeError): return {} + + +class DummyTranslationService: + """ + Dummy drop-in replacement for i18n XBlock service + """ + gettext = _ + ngettext = ngettext_fallback