diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 9aa655545f..604e7d0369 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -57,8 +57,8 @@ jobs: python3 check_js_templates.py - name: Lint Javascript Files run: | - invoke render-js-files - npx eslint js_tmp/*.js + python InvenTree/manage.py prerender + npx eslint InvenTree/InvenTree/static_i18n/i18n/*.js html: name: Style [HTML] diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index e50c8fceae..d11f0920a0 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -218,18 +218,6 @@ logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'") INSTALLED_APPS = [ - # Core django modules - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'user_sessions', # db user sessions - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - - # Maintenance - 'maintenance_mode', - # InvenTree apps 'build.apps.BuildConfig', 'common.apps.CommonConfig', @@ -243,6 +231,18 @@ INSTALLED_APPS = [ 'plugin.apps.PluginAppConfig', 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last + # Core django modules + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'user_sessions', # db user sessions + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sites', + + # Maintenance + 'maintenance_mode', + # Third part add-ons 'django_filters', # Extended filter functionality 'rest_framework', # DRF (Django Rest Framework) diff --git a/InvenTree/part/templatetags/i18n.py b/InvenTree/part/templatetags/i18n.py new file mode 100644 index 0000000000..fcd9df4628 --- /dev/null +++ b/InvenTree/part/templatetags/i18n.py @@ -0,0 +1,121 @@ +"""This module provides custom translation tags specifically for use with javascript code. + +Translated strings are escaped, such that they can be used as string literals in a javascript file. +""" + +import django.templatetags.i18n +from django import template +from django.template import TemplateSyntaxError +from django.templatetags.i18n import TranslateNode + +import bleach + +register = template.Library() + + +class CustomTranslateNode(TranslateNode): + """Custom translation node class, which sanitizes the translated strings for javascript use""" + + def render(self, context): + """Custom render function overrides / extends default behaviour""" + + result = super().render(context) + + result = bleach.clean(result) + + # Remove any escape sequences + for seq in ['\a', '\b', '\f', '\n', '\r', '\t', '\v']: + result = result.replace(seq, '') + + # Remove other disallowed characters + for c in ['\\', '`', ';', '|', '&']: + result = result.replace(c, '') + + # Escape any quotes contained in the string + result = result.replace("'", r"\'") + result = result.replace('"', r'\"') + + # Return the 'clean' resulting string + return result + + +@register.tag("translate") +@register.tag("trans") +def do_translate(parser, token): + """Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py + + The only difference is that we pass this to our custom rendering node class + """ + + bits = token.split_contents() + if len(bits) < 2: + raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0]) + message_string = parser.compile_filter(bits[1]) + remaining = bits[2:] + + noop = False + asvar = None + message_context = None + seen = set() + invalid_context = {"as", "noop"} + + while remaining: + option = remaining.pop(0) + if option in seen: + raise TemplateSyntaxError( + "The '%s' option was specified more than once." % option, + ) + elif option == "noop": + noop = True + elif option == "context": + try: + value = remaining.pop(0) + except IndexError: + raise TemplateSyntaxError( + "No argument provided to the '%s' tag for the context option." + % bits[0] + ) + if value in invalid_context: + raise TemplateSyntaxError( + "Invalid argument '%s' provided to the '%s' tag for the context " + "option" % (value, bits[0]), + ) + message_context = parser.compile_filter(value) + elif option == "as": + try: + value = remaining.pop(0) + except IndexError: + raise TemplateSyntaxError( + "No argument provided to the '%s' tag for the as option." % bits[0] + ) + asvar = value + else: + raise TemplateSyntaxError( + "Unknown argument for '%s' tag: '%s'. The only options " + "available are 'noop', 'context' \"xxx\", and 'as VAR'." + % ( + bits[0], + option, + ) + ) + seen.add(option) + + return CustomTranslateNode(message_string, noop, asvar, message_context) + + +# Re-register tags which we have not explicitly overridden +register.tag("blocktrans", django.templatetags.i18n.do_block_translate) +register.tag("blocktranslate", django.templatetags.i18n.do_block_translate) + +register.tag("language", django.templatetags.i18n.language) + +register.tag("get_available_languages", django.templatetags.i18n.do_get_available_languages) +register.tag("get_language_info", django.templatetags.i18n.do_get_language_info) +register.tag("get_language_info_list", django.templatetags.i18n.do_get_language_info_list) +register.tag("get_current_language", django.templatetags.i18n.do_get_current_language) +register.tag("get_current_language_bidi", django.templatetags.i18n.do_get_current_language_bidi) + +register.filter("language_name", django.templatetags.i18n.language_name) +register.filter("language_name_translated", django.templatetags.i18n.language_name_translated) +register.filter("language_name_local", django.templatetags.i18n.language_name_local) +register.filter("language_bidi", django.templatetags.i18n.language_bidi)