From dce3e2d09661f693cbe7103e2bbfc65224d0ac22 Mon Sep 17 00:00:00 2001 From: LB Johnston Date: Wed, 13 Apr 2022 15:52:03 +1000 Subject: [PATCH] add new rule django_block_translate_trimmed - the rule will enforce the usage of `trimmed` when blocktranslate or blocktrans is in use --- curlylint/check.py | 4 + curlylint/parse.py | 3 +- .../__init__.py | 0 .../django_block_translate_trimmed.py | 69 +++++++++++++ .../django_block_translate_trimmed_test.json | 90 +++++++++++++++++ .../django_block_translate_trimmed_test.py | 10 ++ website/build_rules.py | 4 + website/docs/rules/all.mdx | 5 +- .../rules/django_block_translate_trimmed.mdx | 96 +++++++++++++++++++ website/rules-sidebar.js | 1 + 10 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 curlylint/rules/django_block_translate_trimmed/__init__.py create mode 100644 curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py create mode 100644 curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.json create mode 100644 curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.py create mode 100644 website/docs/rules/django_block_translate_trimmed.mdx diff --git a/curlylint/check.py b/curlylint/check.py index b5c18ce..1391081 100644 --- a/curlylint/check.py +++ b/curlylint/check.py @@ -3,6 +3,9 @@ import click from curlylint.rules.aria_role.aria_role import aria_role +from curlylint.rules.django_block_translate_trimmed.django_block_translate_trimmed import ( + django_block_translate_trimmed, +) from curlylint.rules.django_forms_rendering.django_forms_rendering import ( django_forms_rendering, ) @@ -20,6 +23,7 @@ checks = { "aria_role": aria_role, + "django_block_translate_trimmed": django_block_translate_trimmed, "django_forms_rendering": django_forms_rendering, "html_has_lang": html_has_lang, "image_alt": image_alt, diff --git a/curlylint/parse.py b/curlylint/parse.py index f4eb598..829f796 100644 --- a/curlylint/parse.py +++ b/curlylint/parse.py @@ -129,8 +129,9 @@ DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES = [ ("autoescape", "endautoescape"), - ("block", "endblock"), + ("blocktranslate", "plural", "endblocktranslate"), ("blocktrans", "plural", "endblocktrans"), + ("block", "endblock"), ("comment", "endcomment"), ("filter", "endfilter"), ("for", "else", "empty", "endfor"), diff --git a/curlylint/rules/django_block_translate_trimmed/__init__.py b/curlylint/rules/django_block_translate_trimmed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py new file mode 100644 index 0000000..66e89b6 --- /dev/null +++ b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py @@ -0,0 +1,69 @@ +from curlylint import ast +from curlylint.check_node import CheckNode, build_tree +from curlylint.issue import Issue + +DJANGO_FORMS_RENDERING = "django_block_translate_trimmed" + +RULE = { + "id": "django_block_translate_trimmed", + "type": "internationalisation", + "docs": { + "description": "Enforces the use of Django’s `trimmed` option when using `blocktranslate`/`blocktrans` so that translations do not contain leading or trailing whitespace.", + "url": "https://www.curlylint.org/docs/rules/django_block_translate_trimmed", + "impact": "Serious", + "tags": ["cat:language"], + "resources": [ + "[Django translations](https://docs.djangoproject.com/en/stable/topics/i18n/translation/)", + ], + }, + "schema": { + "$schema": "http://json-schema.org/draft/2019-09/schema#", + "oneOf": [ + { + "const": True, + "title": "Template tags of blocktranslate or blocktrans must use the trimmed option", + "examples": [True], + } + ], + }, +} + +BLOCK_NAMES = ["blocktranslate", "blocktrans"] + + +def find_valid(node, file): + + if isinstance(node.value, ast.JinjaElement): + for part in node.value.parts: + + tag = part.tag + + if tag.name in BLOCK_NAMES: + if "trimmed" not in tag.content.split(" "): + return [ + Issue.from_node( + file, + node, + f"`{tag}` must use the `trimmed` option", + DJANGO_FORMS_RENDERING, + ) + ] + + if not node.children: + return [] + + return sum( + (find_valid(child, file) for child in node.children), + [], + ) + + +def django_block_translate_trimmed(file, target): + root = CheckNode(None) + build_tree(root, file.tree) + src = file.source.lower() + + if "blocktrans" in src or "blocktranslate" in src: + return find_valid(root, file) + + return [] diff --git a/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.json b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.json new file mode 100644 index 0000000..1cc5c4f --- /dev/null +++ b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.json @@ -0,0 +1,90 @@ +[ + { + "label": "Using blocktranslate with trimmed", + "template": "{% blocktranslate trimmed %} some value {% endblocktranslate %}", + "example": true, + "config": true, + "output": [] + }, + { + "label": "Using blocktranslate without trimmed", + "template": "{% blocktranslate %} some value {% endblocktranslate %}", + "example": true, + "config": true, + "output": [ + { + "file": "test.html", + "column": 1, + "line": 1, + "code": "django_block_translate_trimmed", + "message": "`{% blocktranslate %}` must use the `trimmed` option" + } + ] + }, + { + "label": "Using blocktrans with trimmed", + "template": "{% blocktrans trimmed %} some value {% endblocktrans %}", + "example": true, + "config": true, + "output": [] + }, + { + "label": "Using blocktrans without trimmed", + "template": "{% blocktrans %} some value {% endblocktrans %}", + "example": true, + "config": true, + "output": [ + { + "file": "test.html", + "column": 1, + "line": 1, + "code": "django_block_translate_trimmed", + "message": "`{% blocktrans %}` must use the `trimmed` option" + } + ] + }, + { + "label": "Using blocktranslate with trimmed and other options", + "template": "{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}", + "example": true, + "config": true, + "output": [] + }, + { + "label": "Using blocktranslate without trimmed but with other options", + "template": "{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}", + "example": true, + "config": true, + "output": [ + { + "file": "test.html", + "column": 1, + "line": 1, + "code": "django_block_translate_trimmed", + "message": "`{% blocktranslate count counter=list|length %}` must use the `trimmed` option" + } + ] + }, + { + "label": "Using blocktrans with other options", + "template": "{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}", + "example": true, + "config": true, + "output": [] + }, + { + "label": "Using blocktrans without trimmed but with other options", + "template": "{% blocktrans with book_t=book|title %} some value {% endblocktrans %}", + "example": true, + "config": true, + "output": [ + { + "file": "test.html", + "column": 1, + "line": 1, + "code": "django_block_translate_trimmed", + "message": "`{% blocktrans with book_t=book|title %}` must use the `trimmed` option" + } + ] + } +] diff --git a/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.py b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.py new file mode 100644 index 0000000..4f41687 --- /dev/null +++ b/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed_test.py @@ -0,0 +1,10 @@ +import unittest + +from curlylint.rules.rule_test_case import RulesTestMeta + +from .django_block_translate_trimmed import django_block_translate_trimmed + + +class TestRule(unittest.TestCase, metaclass=RulesTestMeta): + fixtures = __file__.replace(".py", ".json") + rule = django_block_translate_trimmed diff --git a/website/build_rules.py b/website/build_rules.py index 66790f4..2cb26e4 100644 --- a/website/build_rules.py +++ b/website/build_rules.py @@ -8,6 +8,9 @@ import toml from curlylint.rules.aria_role import aria_role +from curlylint.rules.django_block_translate_trimmed import ( + django_block_translate_trimmed, +) from curlylint.rules.django_forms_rendering import django_forms_rendering from curlylint.rules.html_has_lang import html_has_lang from curlylint.rules.image_alt import image_alt @@ -18,6 +21,7 @@ rules = [ aria_role.RULE, + django_block_translate_trimmed.RULE, django_forms_rendering.RULE, html_has_lang.RULE, image_alt.RULE, diff --git a/website/docs/rules/all.mdx b/website/docs/rules/all.mdx index caed058..8a395e5 100644 --- a/website/docs/rules/all.mdx +++ b/website/docs/rules/all.mdx @@ -10,6 +10,7 @@ import TabItem from "@theme/TabItem"; import CodeSnippet from "@theme/CodeSnippet"; - [aria_role](aria_role): Elements with ARIA roles must use a valid, non-abstract ARIA role +- [django_block_translate_trimmed](django_block_translate_trimmed): Enforces the use of Django’s `trimmed` option when using `blocktranslate`/`blocktrans` so that translations do not contain leading or trailing whitespace. - [django_forms_rendering](django_forms_rendering): Disallows using Django’s convenience form rendering helpers, for which the markup isn’t screen-reader-friendly - [html_has_lang](html_has_lang): `` elements must have a `lang` attribute, using a [BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) language tag. - [image_alt](image_alt): `` elements must have a `alt` attribute, either with meaningful text, or an empty string for decorative images @@ -32,14 +33,14 @@ Here is a sample configuration with all of Curlylint’s rules enabled. Note **t > diff --git a/website/docs/rules/django_block_translate_trimmed.mdx b/website/docs/rules/django_block_translate_trimmed.mdx new file mode 100644 index 0000000..5c3bfca --- /dev/null +++ b/website/docs/rules/django_block_translate_trimmed.mdx @@ -0,0 +1,96 @@ +--- +# This file is auto-generated, please do not update manually. +id: django_block_translate_trimmed +title: django_block_translate_trimmed +custom_edit_url: https://github.com/thibaudcolas/curlylint/edit/main/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import CodeSnippet from "@theme/CodeSnippet"; + +> Enforces the use of Django’s `trimmed` option when using `blocktranslate`/`blocktrans` so that translations do not contain leading or trailing whitespace. +> +> User impact: **Serious** + +This rule supports the following configuration: + + + + + + + + + + +## Success + + + + \n\n{% blocktranslate trimmed %} some value {% endblocktranslate %}\n\n\n{% blocktrans trimmed %} some value {% endblocktrans %}\n\n\n{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}\n\n\n{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}`} + annotations={[]} + lang="html" + /> + + + \n\n{% blocktranslate trimmed %} some value {% endblocktranslate %}\n\n\n{% blocktrans trimmed %} some value {% endblocktrans %}\n\n\n{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}\n\n\n{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}`} + annotations={[]} + lang="html" + /> + + + +## Fail + + + + \n\n{% blocktranslate %} some value {% endblocktranslate %}\n\n\n{% blocktrans %} some value {% endblocktrans %}\n\n\n{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}\n\n\n{% blocktrans with book_t=book|title %} some value {% endblocktrans %}\n\n`} + annotations={[{"file": "test.html", "column": 1, "line": 3, "code": "django_block_translate_trimmed", "message": "`{% blocktranslate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 6, "code": "django_block_translate_trimmed", "message": "`{% blocktrans %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 9, "code": "django_block_translate_trimmed", "message": "`{% blocktranslate count counter=list|length %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 12, "code": "django_block_translate_trimmed", "message": "`{% blocktrans with book_t=book|title %}` must use the `trimmed` option"}]} + lang="html" + /> + + + \n\n{% blocktranslate %} some value {% endblocktranslate %}\n\n\n{% blocktrans %} some value {% endblocktrans %}\n\n\n{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}\n\n\n{% blocktrans with book_t=book|title %} some value {% endblocktrans %}\n\n`} + annotations={[{"file": "test.html", "column": 1, "line": 3, "code": "django_block_translate_trimmed", "message": "`{% blocktranslate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 6, "code": "django_block_translate_trimmed", "message": "`{% blocktrans %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 9, "code": "django_block_translate_trimmed", "message": "`{% blocktranslate count counter=list|length %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 12, "code": "django_block_translate_trimmed", "message": "`{% blocktrans with book_t=book|title %}` must use the `trimmed` option"}]} + lang="html" + /> + + + +## Resources + +- [Django translations](https://docs.djangoproject.com/en/stable/topics/i18n/translation/) diff --git a/website/rules-sidebar.js b/website/rules-sidebar.js index a863c0f..5e5126d 100644 --- a/website/rules-sidebar.js +++ b/website/rules-sidebar.js @@ -1,5 +1,6 @@ module.exports = [ "rules/aria_role", + "rules/django_block_translate_trimmed", "rules/django_forms_rendering", "rules/html_has_lang", "rules/image_alt",