From dc013ff1dc0c3d9be2793259c0ab07af04671c39 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 | 19 ++-- manage.py | 12 +++ recommender/__init__.py | 4 +- recommender/conf/locale/config.yaml | 3 + .../conf/locale/en/LC_MESSAGES/django.po | 54 ----------- .../conf/locale/en/LC_MESSAGES/text.po | 55 ++++++++++- recommender/recommender.py | 22 +++++ recommender/static/js/src/cats.js | 24 ++++- requirements/base.in | 1 + requirements/base.txt | 28 +++++- requirements/ci.txt | 47 +++++++++- requirements/common_constraints.txt | 5 - requirements/pip.txt | 10 +- requirements/pip_tools.txt | 23 +++-- requirements/test.txt | 47 +++++++++- setup.py | 4 +- translation_settings.py | 92 +++++++++++++++++++ 17 files changed, 346 insertions(+), 104 deletions(-) create mode 100755 manage.py delete mode 100644 recommender/conf/locale/en/LC_MESSAGES/django.po mode change 120000 => 100644 recommender/conf/locale/en/LC_MESSAGES/text.po create mode 100644 translation_settings.py diff --git a/Makefile b/Makefile index ee10bea..f3e6503 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,5 @@ WORKING_DIR := recommender -EXTRACT_DIR := $(WORKING_DIR)/conf/locale/en/LC_MESSAGES -EXTRACTED_DJANGO_PARTIAL := $(EXTRACT_DIR)/django-partial.po -EXTRACTED_DJANGOJS_PARTIAL := $(EXTRACT_DIR)/djangojs-partial.po -EXTRACTED_DJANGO := $(EXTRACT_DIR)/django.po +JS_TARGET := $(WORKING_DIR)/public/js/translations COMMON_CONSTRAINTS_TXT=requirements/common_constraints.txt .PHONY: $(COMMON_CONSTRAINTS_TXT) @@ -22,12 +19,8 @@ upgrade: $(COMMON_CONSTRAINTS_TXT) ## update the requirements/*.txt files with pip-compile --upgrade -o requirements/ci.txt requirements/ci.in extract_translations: ## extract strings to be translated, outputting .po files - cd $(WORKING_DIR) && i18n_tool extract - mv $(EXTRACTED_DJANGO_PARTIAL) $(EXTRACTED_DJANGO) - # Safely concatenate djangojs if it exists - if test -f $(EXTRACTED_DJANGOJS_PARTIAL); then \ - msgcat $(EXTRACTED_DJANGO) $(EXTRACTED_DJANGOJS_PARTIAL) -o $(EXTRACTED_DJANGO) && \ - rm $(EXTRACTED_DJANGOJS_PARTIAL); \ - fi - sed -i'' -e 's/nplurals=INTEGER/nplurals=2/' $(EXTRACTED_DJANGO) - sed -i'' -e 's/plural=EXPRESSION/plural=\(n != 1\)/' $(EXTRACTED_DJANGO) + cd $(WORKING_DIR) && i18n_tool extract --no-segment --merge-po-files + +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..29fa8bf --- /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", + "translation_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/en/LC_MESSAGES/django.po b/recommender/conf/locale/en/LC_MESSAGES/django.po deleted file mode 100644 index 4eccdb1..0000000 --- a/recommender/conf/locale/en/LC_MESSAGES/django.po +++ /dev/null @@ -1,54 +0,0 @@ -msgid "" -msgstr "" -"Project-Id-Version: \n" -"POT-Creation-Date: 2016-05-27 11:04+0500\n" -"PO-Revision-Date: 2016-05-27 11:04+0500\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: recommender.py -msgid "The resource you are attempting to provide already exists" -msgstr "" - -#: recommender.py -msgid "" -"The resource you are attempting to provide has been disallowed by the staff. " -"Reason: " -msgstr "" - -#: recommender.py -msgid "The selected resource does not exist" -msgstr "" - -#: recommender.py -msgid "Size of uploaded file exceeds threshold" -msgstr "" - -#: recommender.py -msgid "The configuration of pyfs is not properly set" -msgstr "" - -#: recommender.py:729 -msgid "Endorse resource without permission" -msgstr "" - -#: recommender.py -msgid "You don't have the permission to remove this resource" -msgstr "" - -#: recommender.py -msgid "Only staff can import resources" -msgstr "" - -#: recommender.py -msgid "Please submit the JSON file obtained with the download resources button" -msgstr "" - -#: recommender.py -msgid "Tried to access flagged resources without staff permission" -msgstr "" diff --git a/recommender/conf/locale/en/LC_MESSAGES/text.po b/recommender/conf/locale/en/LC_MESSAGES/text.po deleted file mode 120000 index 0082074..0000000 --- a/recommender/conf/locale/en/LC_MESSAGES/text.po +++ /dev/null @@ -1 +0,0 @@ -django.po \ No newline at end of file diff --git a/recommender/conf/locale/en/LC_MESSAGES/text.po b/recommender/conf/locale/en/LC_MESSAGES/text.po new file mode 100644 index 0000000..4eccdb1 --- /dev/null +++ b/recommender/conf/locale/en/LC_MESSAGES/text.po @@ -0,0 +1,54 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-05-27 11:04+0500\n" +"PO-Revision-Date: 2016-05-27 11:04+0500\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: recommender.py +msgid "The resource you are attempting to provide already exists" +msgstr "" + +#: recommender.py +msgid "" +"The resource you are attempting to provide has been disallowed by the staff. " +"Reason: " +msgstr "" + +#: recommender.py +msgid "The selected resource does not exist" +msgstr "" + +#: recommender.py +msgid "Size of uploaded file exceeds threshold" +msgstr "" + +#: recommender.py +msgid "The configuration of pyfs is not properly set" +msgstr "" + +#: recommender.py:729 +msgid "Endorse resource without permission" +msgstr "" + +#: recommender.py +msgid "You don't have the permission to remove this resource" +msgstr "" + +#: recommender.py +msgid "Only staff can import resources" +msgstr "" + +#: recommender.py +msgid "Please submit the JSON file obtained with the download resources button" +msgstr "" + +#: recommender.py +msgid "Tried to access flagged resources without staff permission" +msgstr "" 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/requirements/base.in b/requirements/base.in index f3ca6f6..7a8fbb9 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -8,3 +8,4 @@ mako simplejson webob web_fragments +edx-i18n-tools diff --git a/requirements/base.txt b/requirements/base.txt index 804ca0b..6b7d50c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,21 +6,43 @@ # appdirs==1.4.4 # via fs -bleach==6.0.0 +asgiref==3.7.2 + # via django +bleach==6.1.0 + # via -r requirements/base.in +django==3.2.22 + # via + # -c requirements/common_constraints.txt + # edx-i18n-tools +edx-i18n-tools==1.3.0 # via -r requirements/base.in fs==2.4.16 # via -r requirements/base.in +lxml==4.9.3 + # via edx-i18n-tools mako==1.2.4 # via -r requirements/base.in markupsafe==2.1.3 # via mako -simplejson==3.19.1 +path==16.7.1 + # via edx-i18n-tools +polib==1.2.0 + # via edx-i18n-tools +pytz==2023.3.post1 + # via django +pyyaml==6.0.1 + # via edx-i18n-tools +simplejson==3.19.2 # via -r requirements/base.in six==1.16.0 # via # bleach # fs -web-fragments==2.0.0 +sqlparse==0.4.4 + # via django +typing-extensions==4.8.0 + # via asgiref +web-fragments==2.1.0 # via -r requirements/base.in webencodings==0.5.1 # via bleach diff --git a/requirements/ci.txt b/requirements/ci.txt index 5285a01..9ce47fc 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -8,26 +8,65 @@ appdirs==1.4.4 # via # -r requirements/test.txt # fs -bleach==6.0.0 +asgiref==3.7.2 + # via + # -r requirements/test.txt + # django +bleach==6.1.0 + # via -r requirements/test.txt +django==3.2.22 + # via + # -c requirements/common_constraints.txt + # -r requirements/test.txt + # edx-i18n-tools +edx-i18n-tools==1.3.0 # via -r requirements/test.txt fs==2.4.16 # via -r requirements/test.txt +lxml==4.9.3 + # via + # -r requirements/test.txt + # edx-i18n-tools mako==1.2.4 # via -r requirements/test.txt markupsafe==2.1.3 # via # -r requirements/test.txt # mako -pycodestyle==2.10.0 +path==16.7.1 + # via + # -r requirements/test.txt + # edx-i18n-tools +polib==1.2.0 + # via + # -r requirements/test.txt + # edx-i18n-tools +pycodestyle==2.11.0 # via -r requirements/test.txt -simplejson==3.19.1 +pytz==2023.3.post1 + # via + # -r requirements/test.txt + # django +pyyaml==6.0.1 + # via + # -r requirements/test.txt + # edx-i18n-tools +simplejson==3.19.2 # via -r requirements/test.txt six==1.16.0 # via # -r requirements/test.txt # bleach # fs -web-fragments==2.0.0 +sqlparse==0.4.4 + # via + # -r requirements/test.txt + # django +typing-extensions==4.8.0 + # via + # -r requirements/test.txt + # asgiref +web-fragments==2.1.0 # via -r requirements/test.txt webencodings==0.5.1 # via diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 7e39123..afe6aa8 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -25,8 +25,3 @@ django-simple-history==3.0.0 # tox>4.0.0 isn't yet compatible with many tox plugins, causing CI failures in almost all repos. # Details can be found in this discussion: https://github.com/tox-dev/tox/discussions/1810 tox<4.0.0 - -# edx-sphinx-theme is not compatible with latest Sphinx==6.0.0 version -# Pinning Sphinx version unless the compatibility issue gets resolved -# For details, see issue https://github.com/openedx/edx-sphinx-theme/issues/197 -sphinx<6.0.0 diff --git a/requirements/pip.txt b/requirements/pip.txt index c9cbf00..3e7d8f4 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,14 +1,14 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # make upgrade # -wheel==0.40.0 +wheel==0.41.2 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: -pip==23.1.2 +pip==23.2.1 # via -r requirements/pip.in -setuptools==67.8.0 +setuptools==68.2.2 # via -r requirements/pip.in diff --git a/requirements/pip_tools.txt b/requirements/pip_tools.txt index 86c333e..fed3370 100644 --- a/requirements/pip_tools.txt +++ b/requirements/pip_tools.txt @@ -1,23 +1,30 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # make upgrade # -build==0.10.0 +build==1.0.3 # via pip-tools -click==8.1.3 +click==8.1.7 # via pip-tools -packaging==23.1 +importlib-metadata==6.8.0 # via build -pip-tools==6.13.0 +packaging==23.2 + # via build +pip-tools==7.3.0 # via -r requirements/pip_tools.in pyproject-hooks==1.0.0 # via build tomli==2.0.1 - # via build -wheel==0.40.0 + # via + # build + # pip-tools + # pyproject-hooks +wheel==0.41.2 # via pip-tools +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/test.txt b/requirements/test.txt index 5089e85..8cbf13c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,26 +8,65 @@ appdirs==1.4.4 # via # -r requirements/base.txt # fs -bleach==6.0.0 +asgiref==3.7.2 + # via + # -r requirements/base.txt + # django +bleach==6.1.0 + # via -r requirements/base.txt +django==3.2.22 + # via + # -c requirements/common_constraints.txt + # -r requirements/base.txt + # edx-i18n-tools +edx-i18n-tools==1.3.0 # via -r requirements/base.txt fs==2.4.16 # via -r requirements/base.txt +lxml==4.9.3 + # via + # -r requirements/base.txt + # edx-i18n-tools mako==1.2.4 # via -r requirements/base.txt markupsafe==2.1.3 # via # -r requirements/base.txt # mako -pycodestyle==2.10.0 +path==16.7.1 + # via + # -r requirements/base.txt + # edx-i18n-tools +polib==1.2.0 + # via + # -r requirements/base.txt + # edx-i18n-tools +pycodestyle==2.11.0 # via -r requirements/test.in -simplejson==3.19.1 +pytz==2023.3.post1 + # via + # -r requirements/base.txt + # django +pyyaml==6.0.1 + # via + # -r requirements/base.txt + # edx-i18n-tools +simplejson==3.19.2 # via -r requirements/base.txt six==1.16.0 # via # -r requirements/base.txt # bleach # fs -web-fragments==2.0.0 +sqlparse==0.4.4 + # via + # -r requirements/base.txt + # django +typing-extensions==4.8.0 + # via + # -r requirements/base.txt + # asgiref +web-fragments==2.1.0 # via -r requirements/base.txt webencodings==0.5.1 # via 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, }, diff --git a/translation_settings.py b/translation_settings.py new file mode 100644 index 0000000..acc9d54 --- /dev/null +++ b/translation_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'