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/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..22eeebf
--- /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 = ["translate", "trans"]
+
+
+def find_valid(node, file):
+
+ if isinstance(node.value, ast.JinjaElement):
+ for part in node.value.parts:
+ tag = part.tag
+ content_parts = tag.content.split(" ")
+ if tag.name == "block" and content_parts[0] in BLOCK_NAMES:
+ if "trimmed" not in content_parts:
+ 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()
+
+ print("django_block_translate_trimmed", src)
+ 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..7b5cb36
--- /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": "`{% block translate %}` 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": "`{% block trans %}` 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": "`{% block translate 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": "`{% block trans 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..e674603
--- /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": "`{% block translate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 6, "code": "django_block_translate_trimmed", "message": "`{% block trans %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 9, "code": "django_block_translate_trimmed", "message": "`{% block translate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 12, "code": "django_block_translate_trimmed", "message": "`{% block trans %}` 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": "`{% block translate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 6, "code": "django_block_translate_trimmed", "message": "`{% block trans %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 9, "code": "django_block_translate_trimmed", "message": "`{% block translate %}` must use the `trimmed` option"}, {"file": "test.html", "column": 1, "line": 12, "code": "django_block_translate_trimmed", "message": "`{% block trans %}` 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",