Skip to content

Commit dce3e2d

Browse files
author
LB Johnston
committed
add new rule django_block_translate_trimmed
- the rule will enforce the usage of `trimmed` when blocktranslate or blocktrans is in use
1 parent 4c36043 commit dce3e2d

File tree

10 files changed

+279
-3
lines changed

10 files changed

+279
-3
lines changed

curlylint/check.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import click
44

55
from curlylint.rules.aria_role.aria_role import aria_role
6+
from curlylint.rules.django_block_translate_trimmed.django_block_translate_trimmed import (
7+
django_block_translate_trimmed,
8+
)
69
from curlylint.rules.django_forms_rendering.django_forms_rendering import (
710
django_forms_rendering,
811
)
@@ -20,6 +23,7 @@
2023

2124
checks = {
2225
"aria_role": aria_role,
26+
"django_block_translate_trimmed": django_block_translate_trimmed,
2327
"django_forms_rendering": django_forms_rendering,
2428
"html_has_lang": html_has_lang,
2529
"image_alt": image_alt,

curlylint/parse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@
129129

130130
DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES = [
131131
("autoescape", "endautoescape"),
132-
("block", "endblock"),
132+
("blocktranslate", "plural", "endblocktranslate"),
133133
("blocktrans", "plural", "endblocktrans"),
134+
("block", "endblock"),
134135
("comment", "endcomment"),
135136
("filter", "endfilter"),
136137
("for", "else", "empty", "endfor"),

curlylint/rules/django_block_translate_trimmed/__init__.py

Whitespace-only changes.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from curlylint import ast
2+
from curlylint.check_node import CheckNode, build_tree
3+
from curlylint.issue import Issue
4+
5+
DJANGO_FORMS_RENDERING = "django_block_translate_trimmed"
6+
7+
RULE = {
8+
"id": "django_block_translate_trimmed",
9+
"type": "internationalisation",
10+
"docs": {
11+
"description": "Enforces the use of Django’s `trimmed` option when using `blocktranslate`/`blocktrans` so that translations do not contain leading or trailing whitespace.",
12+
"url": "https://www.curlylint.org/docs/rules/django_block_translate_trimmed",
13+
"impact": "Serious",
14+
"tags": ["cat:language"],
15+
"resources": [
16+
"[Django translations](https://docs.djangoproject.com/en/stable/topics/i18n/translation/)",
17+
],
18+
},
19+
"schema": {
20+
"$schema": "http://json-schema.org/draft/2019-09/schema#",
21+
"oneOf": [
22+
{
23+
"const": True,
24+
"title": "Template tags of blocktranslate or blocktrans must use the trimmed option",
25+
"examples": [True],
26+
}
27+
],
28+
},
29+
}
30+
31+
BLOCK_NAMES = ["blocktranslate", "blocktrans"]
32+
33+
34+
def find_valid(node, file):
35+
36+
if isinstance(node.value, ast.JinjaElement):
37+
for part in node.value.parts:
38+
39+
tag = part.tag
40+
41+
if tag.name in BLOCK_NAMES:
42+
if "trimmed" not in tag.content.split(" "):
43+
return [
44+
Issue.from_node(
45+
file,
46+
node,
47+
f"`{tag}` must use the `trimmed` option",
48+
DJANGO_FORMS_RENDERING,
49+
)
50+
]
51+
52+
if not node.children:
53+
return []
54+
55+
return sum(
56+
(find_valid(child, file) for child in node.children),
57+
[],
58+
)
59+
60+
61+
def django_block_translate_trimmed(file, target):
62+
root = CheckNode(None)
63+
build_tree(root, file.tree)
64+
src = file.source.lower()
65+
66+
if "blocktrans" in src or "blocktranslate" in src:
67+
return find_valid(root, file)
68+
69+
return []
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[
2+
{
3+
"label": "Using blocktranslate with trimmed",
4+
"template": "{% blocktranslate trimmed %} some value {% endblocktranslate %}",
5+
"example": true,
6+
"config": true,
7+
"output": []
8+
},
9+
{
10+
"label": "Using blocktranslate without trimmed",
11+
"template": "{% blocktranslate %} some value {% endblocktranslate %}",
12+
"example": true,
13+
"config": true,
14+
"output": [
15+
{
16+
"file": "test.html",
17+
"column": 1,
18+
"line": 1,
19+
"code": "django_block_translate_trimmed",
20+
"message": "`{% blocktranslate %}` must use the `trimmed` option"
21+
}
22+
]
23+
},
24+
{
25+
"label": "Using blocktrans with trimmed",
26+
"template": "{% blocktrans trimmed %} some value {% endblocktrans %}",
27+
"example": true,
28+
"config": true,
29+
"output": []
30+
},
31+
{
32+
"label": "Using blocktrans without trimmed",
33+
"template": "{% blocktrans %} some value {% endblocktrans %}",
34+
"example": true,
35+
"config": true,
36+
"output": [
37+
{
38+
"file": "test.html",
39+
"column": 1,
40+
"line": 1,
41+
"code": "django_block_translate_trimmed",
42+
"message": "`{% blocktrans %}` must use the `trimmed` option"
43+
}
44+
]
45+
},
46+
{
47+
"label": "Using blocktranslate with trimmed and other options",
48+
"template": "{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}",
49+
"example": true,
50+
"config": true,
51+
"output": []
52+
},
53+
{
54+
"label": "Using blocktranslate without trimmed but with other options",
55+
"template": "{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}",
56+
"example": true,
57+
"config": true,
58+
"output": [
59+
{
60+
"file": "test.html",
61+
"column": 1,
62+
"line": 1,
63+
"code": "django_block_translate_trimmed",
64+
"message": "`{% blocktranslate count counter=list|length %}` must use the `trimmed` option"
65+
}
66+
]
67+
},
68+
{
69+
"label": "Using blocktrans with other options",
70+
"template": "{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}",
71+
"example": true,
72+
"config": true,
73+
"output": []
74+
},
75+
{
76+
"label": "Using blocktrans without trimmed but with other options",
77+
"template": "{% blocktrans with book_t=book|title %} some value {% endblocktrans %}",
78+
"example": true,
79+
"config": true,
80+
"output": [
81+
{
82+
"file": "test.html",
83+
"column": 1,
84+
"line": 1,
85+
"code": "django_block_translate_trimmed",
86+
"message": "`{% blocktrans with book_t=book|title %}` must use the `trimmed` option"
87+
}
88+
]
89+
}
90+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import unittest
2+
3+
from curlylint.rules.rule_test_case import RulesTestMeta
4+
5+
from .django_block_translate_trimmed import django_block_translate_trimmed
6+
7+
8+
class TestRule(unittest.TestCase, metaclass=RulesTestMeta):
9+
fixtures = __file__.replace(".py", ".json")
10+
rule = django_block_translate_trimmed

website/build_rules.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import toml
99

1010
from curlylint.rules.aria_role import aria_role
11+
from curlylint.rules.django_block_translate_trimmed import (
12+
django_block_translate_trimmed,
13+
)
1114
from curlylint.rules.django_forms_rendering import django_forms_rendering
1215
from curlylint.rules.html_has_lang import html_has_lang
1316
from curlylint.rules.image_alt import image_alt
@@ -18,6 +21,7 @@
1821

1922
rules = [
2023
aria_role.RULE,
24+
django_block_translate_trimmed.RULE,
2125
django_forms_rendering.RULE,
2226
html_has_lang.RULE,
2327
image_alt.RULE,

website/docs/rules/all.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import TabItem from "@theme/TabItem";
1010
import CodeSnippet from "@theme/CodeSnippet";
1111

1212
- [aria_role](aria_role): Elements with ARIA roles must use a valid, non-abstract ARIA role
13+
- [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.
1314
- [django_forms_rendering](django_forms_rendering): Disallows using Django’s convenience form rendering helpers, for which the markup isn’t screen-reader-friendly
1415
- [html_has_lang](html_has_lang): `<html>` elements must have a `lang` attribute, using a [BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) language tag.
1516
- [image_alt](image_alt): `<img>` 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
3233
>
3334
<TabItem value="toml">
3435
<CodeSnippet
35-
snippet={`[tool.curlylint.rules]\n# All role attributes must be valid.\n# See https://www.curlylint.org/docs/rules/aria_role.\naria_role = true\n# Forms cannot be rendered with as_table, as_ul, or as_p\n# See https://www.curlylint.org/docs/rules/django_forms_rendering.\ndjango_forms_rendering = true\n# The \`lang\` attribute must be present.\n# See https://www.curlylint.org/docs/rules/html_has_lang.\nhtml_has_lang = true\n# The \`alt\` attribute must be present.\n# See https://www.curlylint.org/docs/rules/image_alt.\nimage_alt = true\n# Use tabs.\n# See https://www.curlylint.org/docs/rules/indent.\nindent = "tab"\n# \`user-scalable=no\` must not be used, and \`maximum-scale\` should be 2 or above.\n# See https://www.curlylint.org/docs/rules/meta_viewport.\nmeta_viewport = true\n# The \`autofocus\` attribute must not be used.\n# See https://www.curlylint.org/docs/rules/no_autofocus.\nno_autofocus = true\n# Avoid positive \`tabindex\` values, change the order of elements on the page instead.\n# See https://www.curlylint.org/docs/rules/tabindex_no_positive.\ntabindex_no_positive = true`}
36+
snippet={`[tool.curlylint.rules]\n# All role attributes must be valid.\n# See https://www.curlylint.org/docs/rules/aria_role.\naria_role = true\n# Template tags of blocktranslate or blocktrans must use the trimmed option\n# See https://www.curlylint.org/docs/rules/django_block_translate_trimmed.\ndjango_block_translate_trimmed = true\n# Forms cannot be rendered with as_table, as_ul, or as_p\n# See https://www.curlylint.org/docs/rules/django_forms_rendering.\ndjango_forms_rendering = true\n# The \`lang\` attribute must be present.\n# See https://www.curlylint.org/docs/rules/html_has_lang.\nhtml_has_lang = true\n# The \`alt\` attribute must be present.\n# See https://www.curlylint.org/docs/rules/image_alt.\nimage_alt = true\n# Use tabs.\n# See https://www.curlylint.org/docs/rules/indent.\nindent = "tab"\n# \`user-scalable=no\` must not be used, and \`maximum-scale\` should be 2 or above.\n# See https://www.curlylint.org/docs/rules/meta_viewport.\nmeta_viewport = true\n# The \`autofocus\` attribute must not be used.\n# See https://www.curlylint.org/docs/rules/no_autofocus.\nno_autofocus = true\n# Avoid positive \`tabindex\` values, change the order of elements on the page instead.\n# See https://www.curlylint.org/docs/rules/tabindex_no_positive.\ntabindex_no_positive = true`}
3637
annotations={[]}
3738
lang="toml"
3839
/>
3940
</TabItem>
4041
<TabItem value="shell">
4142
<CodeSnippet
42-
snippet={`curlylint --rule 'aria_role: true' --rule 'django_forms_rendering: true' --rule 'html_has_lang: true' --rule 'image_alt: true' --rule 'indent: "tab"' --rule 'meta_viewport: true' --rule 'no_autofocus: true' --rule 'tabindex_no_positive: true' .`}
43+
snippet={`curlylint --rule 'aria_role: true' --rule 'django_block_translate_trimmed: true' --rule 'django_forms_rendering: true' --rule 'html_has_lang: true' --rule 'image_alt: true' --rule 'indent: "tab"' --rule 'meta_viewport: true' --rule 'no_autofocus: true' --rule 'tabindex_no_positive: true' .`}
4344
annotations={[]}
4445
lang="shell"
4546
/>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
# This file is auto-generated, please do not update manually.
3+
id: django_block_translate_trimmed
4+
title: django_block_translate_trimmed
5+
custom_edit_url: https://github.com/thibaudcolas/curlylint/edit/main/curlylint/rules/django_block_translate_trimmed/django_block_translate_trimmed.py
6+
---
7+
8+
import Tabs from "@theme/Tabs";
9+
import TabItem from "@theme/TabItem";
10+
import CodeSnippet from "@theme/CodeSnippet";
11+
12+
> Enforces the use of Django’s `trimmed` option when using `blocktranslate`/`blocktrans` so that translations do not contain leading or trailing whitespace.
13+
>
14+
> User impact: **Serious**
15+
16+
This rule supports the following configuration:
17+
18+
<Tabs
19+
groupId="config-language"
20+
defaultValue="toml"
21+
values={[
22+
{ label: "TOML", value: "toml" },
23+
{ label: "Shell", value: "shell" },
24+
]}
25+
>
26+
<TabItem value="toml">
27+
<CodeSnippet
28+
snippet={`# Template tags of blocktranslate or blocktrans must use the trimmed option\ndjango_block_translate_trimmed = true`}
29+
annotations={[]}
30+
lang="toml"
31+
/>
32+
</TabItem>
33+
<TabItem value="shell">
34+
<CodeSnippet
35+
snippet={`# Template tags of blocktranslate or blocktrans must use the trimmed option\ncurlylint --rule 'django_block_translate_trimmed: true' .`}
36+
annotations={[]}
37+
lang="shell"
38+
/>
39+
</TabItem>
40+
</Tabs>
41+
42+
## Success
43+
44+
<Tabs
45+
groupId="config-language"
46+
defaultValue="toml"
47+
values={[
48+
{ label: "TOML", value: "toml" },
49+
{ label: "Shell", value: "shell" },
50+
]}
51+
>
52+
<TabItem value="toml">
53+
<CodeSnippet
54+
snippet={`<!-- Good: Using blocktranslate with trimmed -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktranslate trimmed %} some value {% endblocktranslate %}\n<!-- Good: Using blocktrans with trimmed -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktrans trimmed %} some value {% endblocktrans %}\n<!-- Good: Using blocktranslate with trimmed and other options -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}\n<!-- Good: Using blocktrans with other options -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}`}
55+
annotations={[]}
56+
lang="html"
57+
/>
58+
</TabItem>
59+
<TabItem value="shell">
60+
<CodeSnippet
61+
snippet={`<!-- Good: Using blocktranslate with trimmed -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktranslate trimmed %} some value {% endblocktranslate %}\n<!-- Good: Using blocktrans with trimmed -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktrans trimmed %} some value {% endblocktrans %}\n<!-- Good: Using blocktranslate with trimmed and other options -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktranslate trimmed with time_period=revision.created_at|timesince_simple %} some value {% endblocktranslate %}\n<!-- Good: Using blocktrans with other options -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktrans trimmed with book_t=book|title %} some value {% endblocktrans %}`}
62+
annotations={[]}
63+
lang="html"
64+
/>
65+
</TabItem>
66+
</Tabs>
67+
68+
## Fail
69+
70+
<Tabs
71+
groupId="config-language"
72+
defaultValue="toml"
73+
values={[
74+
{ label: "TOML", value: "toml" },
75+
{ label: "Shell", value: "shell" },
76+
]}
77+
>
78+
<TabItem value="toml">
79+
<CodeSnippet
80+
snippet={`<!-- Bad: Using blocktranslate without trimmed -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktranslate %} some value {% endblocktranslate %}\n<!-- Bad: Using blocktrans without trimmed -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktrans %} some value {% endblocktrans %}\n<!-- Bad: Using blocktranslate without trimmed but with other options -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}\n<!-- Bad: Using blocktrans without trimmed but with other options -->\n<!-- django_block_translate_trimmed = true -->\n{% blocktrans with book_t=book|title %} some value {% endblocktrans %}\n\n`}
81+
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"}]}
82+
lang="html"
83+
/>
84+
</TabItem>
85+
<TabItem value="shell">
86+
<CodeSnippet
87+
snippet={`<!-- Bad: Using blocktranslate without trimmed -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktranslate %} some value {% endblocktranslate %}\n<!-- Bad: Using blocktrans without trimmed -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktrans %} some value {% endblocktrans %}\n<!-- Bad: Using blocktranslate without trimmed but with other options -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktranslate count counter=list|length %} some value {% endblocktranslate %}\n<!-- Bad: Using blocktrans without trimmed but with other options -->\n<!-- curlylint --rule 'django_block_translate_trimmed: true' . -->\n{% blocktrans with book_t=book|title %} some value {% endblocktrans %}\n\n`}
88+
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"}]}
89+
lang="html"
90+
/>
91+
</TabItem>
92+
</Tabs>
93+
94+
## Resources
95+
96+
- [Django translations](https://docs.djangoproject.com/en/stable/topics/i18n/translation/)

website/rules-sidebar.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = [
22
"rules/aria_role",
3+
"rules/django_block_translate_trimmed",
34
"rules/django_forms_rendering",
45
"rules/html_has_lang",
56
"rules/image_alt",

0 commit comments

Comments
 (0)