From 0372561a00747d6b8ab1ff7b94bc9f7c41790ee1 Mon Sep 17 00:00:00 2001 From: Shadi Naif Date: Fri, 26 May 2023 19:35:41 +0300 Subject: [PATCH] feat: Add `gettext` for JS translation JS translations will not be shown fetched without directing (gettext) to the correct javascript file Refs: FC-0012 OEP-58 --- Makefile | 5 ++ manage.py | 12 ++++ recommender/__init__.py | 4 +- recommender/conf/locale/config.yaml | 3 + recommender/conf/locale/settings.py | 92 +++++++++++++++++++++++++++++ recommender/recommender.py | 22 +++++++ recommender/static/js/src/cats.js | 24 ++++++-- setup.py | 4 +- 8 files changed, 159 insertions(+), 7 deletions(-) create mode 100755 manage.py create mode 100644 recommender/conf/locale/settings.py diff --git a/Makefile b/Makefile index ee10bea..f4339a1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ WORKING_DIR := recommender EXTRACT_DIR := $(WORKING_DIR)/conf/locale/en/LC_MESSAGES +JS_TARGET := $(WORKING_DIR)/public/js/translations EXTRACTED_DJANGO_PARTIAL := $(EXTRACT_DIR)/django-partial.po EXTRACTED_DJANGOJS_PARTIAL := $(EXTRACT_DIR)/djangojs-partial.po EXTRACTED_DJANGO := $(EXTRACT_DIR)/django.po @@ -31,3 +32,7 @@ extract_translations: ## extract strings to be translated, outputting .po files fi sed -i'' -e 's/nplurals=INTEGER/nplurals=2/' $(EXTRACTED_DJANGO) sed -i'' -e 's/plural=EXPRESSION/plural=\(n != 1\)/' $(EXTRACTED_DJANGO) + +compile_translations: ## compile translation files, outputting .mo files for each supported language + cd $(WORKING_DIR) && i18n_tool generate -v + python manage.py compilejsi18n --namespace RecommenderXBlockI18N --output $(JS_TARGET) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..e665f81 --- /dev/null +++ b/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +import os +import sys +from django.core.management import execute_from_command_line + +if __name__ == "__main__": + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + "recommender.conf.locale.settings" + ) + + execute_from_command_line(sys.argv) diff --git a/recommender/__init__.py b/recommender/__init__.py index 6e5ec82..40dad5f 100644 --- a/recommender/__init__.py +++ b/recommender/__init__.py @@ -2,6 +2,8 @@ This XBlock will show a set of recommended resources which may be helpful to students solving a given problem. """ -from .recommender import RecommenderXBlock +# We avoid importing RecommenderXBlock here, because it's importing Filesystem from xblock.reference.plugins +# which is not loaded when running `manage.py` commands (which is used by `make compile_translations`) +# from .recommender import RecommenderXBlock __version__ = '2.1.0' diff --git a/recommender/conf/locale/config.yaml b/recommender/conf/locale/config.yaml index a968d94..c0fb690 100644 --- a/recommender/conf/locale/config.yaml +++ b/recommender/conf/locale/config.yaml @@ -2,3 +2,6 @@ locales: - en # English - Source Language + +ignore_dirs: + - public diff --git a/recommender/conf/locale/settings.py b/recommender/conf/locale/settings.py new file mode 100644 index 0000000..acc9d54 --- /dev/null +++ b/recommender/conf/locale/settings.py @@ -0,0 +1,92 @@ +""" +Django settings for xblock-drag-and-drop-v2 project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +from __future__ import absolute_import +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +# This is just a container for running tests, it's okay to allow it to be +# defaulted here if not present in environment settings +SECRET_KEY = os.environ.get('SECRET_KEY', '&=@m=qyqg#l!f99ouuuinpbsv0ah001unk@q^7)bkr^^n5@q1=') + +# SECURITY WARNING: don't run with debug turned on in production! +# This is just a container for running tests +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'statici18n', + 'recommender', +) + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' + +# statici18n +# http://django-statici18n.readthedocs.io/en/latest/settings.html + +LANGUAGES = [ + ('ar', 'Arabic'), + ('de', 'German'), + ('en', 'English - Source Language'), + ('eo', 'Esperanto'), + ('es_419', 'Spanish (Latin America)'), + ('fr', 'French'), + ('he', 'Hebrew'), + ('hi', 'Hindi'), + ('it', 'Italian'), + ('ja', 'Japanese'), + ('ko', 'Korean (Korea)'), + ('nl', 'Dutch'), + ('pl', 'Polski'), + ('pt_BR', 'Portuguese (Brazil)'), + ('pt_PT', 'Portuguese (Portugal)'), + ('ru', 'Russian'), + ('tr', 'Turkish'), + ('zh_CN', 'Chinese (China)'), +] + +LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] + +STATICI18N_DOMAIN = 'text' +STATICI18N_NAMESPACE = 'RecommenderXBlockI18N' +STATICI18N_PACKAGES = ( + 'recommender', +) +STATICI18N_ROOT = 'recommender/public/js' +STATICI18N_OUTPUT_DIR = 'translations' diff --git a/recommender/recommender.py b/recommender/recommender.py index 9173beb..d1e2ad2 100644 --- a/recommender/recommender.py +++ b/recommender/recommender.py @@ -18,6 +18,7 @@ import bleach from webob.response import Response +from django.utils import translation from xblock.core import XBlock from xblock.exceptions import JsonHandlerError @@ -944,6 +945,21 @@ def _construct_view_resource(self, resource): return result + @staticmethod + def _get_statici18n_js_url(): # pragma: no cover + """ + Returns the Javascript translation file for the currently selected language, if any found by `pkg_resources` + """ + lang_code = translation.get_language() + if not lang_code: + return None + text_js = 'public/js/translations/{lang_code}/text.js' + country_code = lang_code.split('-')[0] + for code in (translation.to_locale(lang_code), lang_code, country_code): + if pkg_resources.resource_exists(resource_loader.module_name, text_js.format(lang_code=code)): + return text_js.format(lang_code=code) + return None + def student_view(self, _context=None): # pylint: disable=unused-argument """ The primary view of the RecommenderXBlock, shown to students @@ -990,6 +1006,9 @@ def student_view(self, _context=None): # pylint: disable=unused-argument frag.add_css(self.resource_string("static/css/recommender.css")) frag.add_css(self.resource_string("static/css/introjs.css")) frag.add_javascript(self.resource_string("static/js/src/jquery.tooltipster.min.js")) + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript(self.resource_string(statici18n_js_url)) frag.add_javascript(self.resource_string("static/js/src/cats.js")) frag.add_javascript(self.resource_string("static/js/src/recommender.js")) frag.initialize_js('RecommenderXBlock', self.get_client_configuration()) @@ -1007,6 +1026,9 @@ def studio_view(self, _context=None): # pylint: disable=unused-argument )) frag.add_css(load("static/css/recommenderstudio.css")) frag.add_javascript_url("//ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js") + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript(self.resource_string(statici18n_js_url)) frag.add_javascript(load("static/js/src/recommenderstudio.js")) frag.initialize_js('RecommenderXBlock') return frag diff --git a/recommender/static/js/src/cats.js b/recommender/static/js/src/cats.js index 8e58a0e..7d77f87 100644 --- a/recommender/static/js/src/cats.js +++ b/recommender/static/js/src/cats.js @@ -3,10 +3,26 @@ // // Note: The global `window.*` variables should be converted into local ones to avoid over-using global variables. This is a 2014-era legacy XBlock coding standard that should be refactored -- @OmarIthawi at Apr 4, 2023 // - var gettext = window.gettext || (function (string) { - // Shim Django's `gettext` if unavailable. - return string; - }); + var gettext; + if ('RecommenderXBlockI18N' in window) { + // Use Recommender's local translations + gettext = function(string) { + var translated = window.RecommenderXBlockI18N.gettext(string); + // if Recommender's translation is the same as the input, check if global has a different value + // This is useful for overriding the XBlock's string by themes (only for English) + if (string === translated && 'gettext' in window) { + translated = window.gettext(string); + } + return translated; + }; + } else if ('gettext' in window) { + // Use edxapp's global translations + gettext = window.gettext; + } + if (typeof gettext == "undefined") { + // No translations -- used by test environment + gettext = function(string) { return string; }; + } var span = function(text) { // Surround text with a span. diff --git a/setup.py b/setup.py index 0da9bb8..a8dc26c 100644 --- a/setup.py +++ b/setup.py @@ -78,10 +78,10 @@ def get_version(file_path): license='AGPL 3.0', entry_points={ 'xblock.v1': [ - 'recommender = recommender:RecommenderXBlock', + 'recommender = recommender.recommender:RecommenderXBlock', ] }, - package_data=package_data("recommender", ["static", "templates", "translations"]), + package_data=package_data("recommender", ["static", "templates", "translations", "public"]), cmdclass={ 'install': XBlockInstall, },