Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new rule django_block_translate_trimmed #137

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions curlylint/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion curlylint/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@

DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES = [
("autoescape", "endautoescape"),
("block", "endblock"),
("blocktranslate", "plural", "endblocktranslate"),
("blocktrans", "plural", "endblocktrans"),
("block", "endblock"),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this re-order is required because the approach is greedy, the first match is used.

So if we want to ensure that blocktranslate gets pulled in and it does not get treated as blocktrans or block we need to ensure that it is first.

Hope that makes sense.

Also blocktranslate is the Django 4.0 variant.

("comment", "endcomment"),
("filter", "endfilter"),
("for", "else", "empty", "endfor"),
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -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 []
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
[
{
"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 %}\n some value\n{% 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 with other options, with trimmed after these options",
"template": "{% blocktrans with book_t=book|title context trimmed %} some value {% endblocktrans %}",
"example": false,
"config": true,
"output": []
},
{
"label": "Using blocktrans without trimmed but with other options",
"template": "{% blocktrans with book_t=book|title %}\nsome value\n{% 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"
}
]
}
]
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions website/build_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,6 +21,7 @@

rules = [
aria_role.RULE,
django_block_translate_trimmed.RULE,
django_forms_rendering.RULE,
html_has_lang.RULE,
image_alt.RULE,
Expand Down
5 changes: 3 additions & 2 deletions website/docs/rules/all.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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): `<html>` elements must have a `lang` attribute, using a [BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) language tag.
- [image_alt](image_alt): `<img>` elements must have a `alt` attribute, either with meaningful text, or an empty string for decorative images
Expand All @@ -32,14 +33,14 @@ Here is a sample configuration with all of Curlylint’s rules enabled. Note **t
>
<TabItem value="toml">
<CodeSnippet
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`}
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`}
annotations={[]}
lang="toml"
/>
</TabItem>
<TabItem value="shell">
<CodeSnippet
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' .`}
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' .`}
annotations={[]}
lang="shell"
/>
Expand Down
104 changes: 104 additions & 0 deletions website/docs/rules/django_block_translate_trimmed.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
# 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:

<Tabs
groupId="config-language"
defaultValue="toml"
values={[
{ label: "TOML", value: "toml" },
{ label: "Shell", value: "shell" },
]}
>
<TabItem value="toml">
<CodeSnippet
snippet={`# Template tags of blocktranslate or blocktrans must use the trimmed option\ndjango_block_translate_trimmed = true`}
annotations={[]}
lang="toml"
/>
</TabItem>
<TabItem value="shell">
<CodeSnippet
snippet={`# Template tags of blocktranslate or blocktrans must use the trimmed option\ncurlylint --rule 'django_block_translate_trimmed: true' .`}
annotations={[]}
lang="shell"
/>
</TabItem>
</Tabs>

## Success

<Tabs
groupId="config-language"
defaultValue="toml"
values={[
{ label: "TOML", value: "toml" },
{ label: "Shell", value: "shell" },
]}
>
<TabItem value="toml">
<CodeSnippet
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 %}`}
annotations={[]}
lang="html"
/>
</TabItem>
<TabItem value="shell">
<CodeSnippet
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 %}`}
annotations={[]}
lang="html"
/>
</TabItem>
</Tabs>

## Fail

<Tabs
groupId="config-language"
defaultValue="toml"
values={[
{ label: "TOML", value: "toml" },
{ label: "Shell", value: "shell" },
]}
>
<TabItem value="toml">
<CodeSnippet
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`}
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"
/>
</TabItem>
<TabItem value="shell">
<CodeSnippet
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`}
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"
/>
</TabItem>
</Tabs>

## Resources

- [Django translations](https://docs.djangoproject.com/en/stable/topics/i18n/translation/)
1 change: 1 addition & 0 deletions website/rules-sidebar.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down