Skip to content

Commit

Permalink
feat: use XBlockI18NService js translations
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmadabuwardeh committed Jan 27, 2024
1 parent f2df86c commit b9afb1e
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Please See the `releases tab <https://github.com/openedx/xblock-lti-consumer/rel

Unreleased
~~~~~~~~~~
9.9.0 (2024-01-24)
---------------------------
* XBlockI18NService js translations support

9.8.3 - 2024-01-23
------------------
* Additional NewRelic traces to functions suspected of causing performance issues.
Expand Down
2 changes: 1 addition & 1 deletion lti_consumer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
from .apps import LTIConsumerApp
from .lti_xblock import LtiConsumerXBlock

__version__ = '9.8.3'
__version__ = '9.9.0'
53 changes: 40 additions & 13 deletions lti_consumer/lti_xblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
external_user_id_1p1_launches_enabled,
database_config_enabled,
EXTERNAL_ID_REGEX,
DummyTranslationService,
)

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -257,6 +258,7 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
"""

block_settings_key = 'lti_consumer'
i18n_js_namespace = 'LtiI18N'

display_name = String(
display_name=_("Display Name"),
Expand Down Expand Up @@ -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:
Expand All @@ -678,6 +683,19 @@ 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):
Expand All @@ -688,7 +706,7 @@ def validate_field_data(self, validation, data):

# Validate custom parameters format.
if not all(map(CUSTOM_PARAMETER_REGEX.match, data.custom_parameters)):
_ = self.runtime.service(self, 'i18n').ugettext
_ = self.runtime.service(self, "i18n").ugettext
validation.add(ValidationMessage(ValidationMessage.ERROR, str(
_('Custom Parameters should be strings in "x=y" format.'),
)))
Expand All @@ -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.runtime.service(self, "i18n").ugettext
validation.add(ValidationMessage(ValidationMessage.ERROR, str(
_('Reusable configuration ID must be set when using external config (Example: "x:y").'),
)))
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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.runtime.service(self, "i18n").ugettext("Could not get user data for current request"))

return user.opt_attrs.get('edx-platform.user_role', 'student')

Expand All @@ -878,7 +905,7 @@ def lti_provider_key_secret(self):
raise ValueError
key = ':'.join(key)
except ValueError as err:
msg = self.ugettext(
msg = self.runtime.service(self, "i18n").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
Expand All @@ -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.runtime.service(self, "i18n").ugettext("Could not get user id for current request"))
return user_id

@property
Expand All @@ -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.runtime.service(self, "i18n").ugettext("Could not get user id for current request"))
return str(user_id)

def get_icon_class(self):
Expand All @@ -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.runtime.service(self, "i18n").ugettext("Could not get user id for current request"))
return str(user_id)

def get_lti_1p1_user_id(self):
Expand Down Expand Up @@ -1062,7 +1089,7 @@ def prefixed_custom_parameters(self):
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(
msg = self.runtime.service(self, "i18n").ugettext(
'Could not parse custom parameter: {custom_parameter!r}. Should be "x=y" string.'
).format(custom_parameter=custom_parameter)
raise LtiError(msg) from err
Expand Down Expand Up @@ -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.runtime.service(self, "i18n").ugettext("Could not get user data for current request"))

user_data = {
'user_email': None,
Expand Down Expand Up @@ -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.runtime.service(self, "i18n")
),
)
fragment.add_css(loader.load_unicode('static/css/student.css'))
Expand Down Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions lti_consumer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit b9afb1e

Please sign in to comment.