add i18n minus blocktrans

This commit is contained in:
danhanson committed 2026-09-07 15:27:33 -05:00
1 parent 709a33f52d
commit d4ef9fc20a
8 files changed
+51090 -38773

No files matched your search

+4 -1
View File
@@ -106,7 +106,8 @@ which Django itself enforces while it parses the template ("add requires 2 argum
### First-party libraries ### First-party libraries
Django ships `i18n`, `l10n`, `static`, `cache` and `tz` as libraries rather than builtins. Their tags are Django ships `i18n`, `l10n`, `static`, `cache` and `tz` as libraries rather than builtins. Their tags are
modelled as ordinary tags (so far: `static`, `l10n`, `tz`, `cache`), and the grammar **never requires the `{% load %}`**, because modelled as ordinary tags (so far: `static`, `l10n`, `tz`, `cache`, and `i18n` apart from
`blocktrans`/`blocktranslate`), and the grammar **never requires the `{% load %}`**, because
an engine can preload a library through `OPTIONS: {"builtins": [...]}`, which makes `{% static "a" %}` valid an engine can preload a library through `OPTIONS: {"builtins": [...]}`, which makes `{% static "a" %}` valid
with no load at all. with no load at all.
@@ -210,5 +211,7 @@ documentation or memory. Both checks are worth repeating whenever tags, filters
`{% cache 500 sidebar.name %}` names a fragment there, `{% cache 500 using="c" %}` names a fragment `{% cache 500 sidebar.name %}` names a fragment there, `{% cache 500 using="c" %}` names a fragment
called `using="c"` rather than choosing a cache, and `{% cache 500 sidebar using= %}` takes an empty called `using="c"` rather than choosing a cache, and `{% cache 500 sidebar using= %}` takes an empty
name. All three are errors here. name. All three are errors here.
- `do_translate` refuses `as` and `noop` as the value of its `context` option, so
`{% trans "hi" context noop %}` is an error there and a context named by the variable `noop` here.
- `partial`/`partialdef` are Django builtins as of Django 6; `elif`/`else`/`empty` are modelled as parts of - `partial`/`partialdef` are Django builtins as of Django 6; `elif`/`else`/`empty` are modelled as parts of
their enclosing tag rather than as separate tags. their enclosing tag rather than as separate tags.
+58
View File
@@ -233,6 +233,14 @@ const L10N_FILTERS_WITHOUT_ARGUMENT = ["localize", "unlocalize"];
const TZ_FILTERS_WITHOUT_ARGUMENT = ["localtime", "utc"]; const TZ_FILTERS_WITHOUT_ARGUMENT = ["localtime", "utc"];
const TZ_FILTERS_WITH_ARGUMENT = ["timezone"]; const TZ_FILTERS_WITH_ARGUMENT = ["timezone"];
/** django.templatetags.i18n. */
const I18N_FILTERS_WITHOUT_ARGUMENT = [
"language_bidi",
"language_name",
"language_name_local",
"language_name_translated",
];
/** /**
* The words that head an end tag or a part of a tag group. Each is a token * The words that head an end tag or a part of a tag group. Each is a token
* only inside the group it belongs to, so anywhere else the lexer reads it as * only inside the group it belongs to, so anywhere else the lexer reads it as
@@ -253,6 +261,7 @@ const NAMES_INSIDE_A_TAG_GROUP = [
"endfor", "endfor",
"endif", "endif",
"endifchanged", "endifchanged",
"endlanguage",
"endlocalize", "endlocalize",
"endlocaltime", "endlocaltime",
"endpartialdef", "endpartialdef",
@@ -390,12 +399,18 @@ const django = grammar({
$.filter_group, $.filter_group,
$.firstof, $.firstof,
$.for_group, $.for_group,
$.get_available_languages,
$.get_current_language,
$.get_current_language_bidi,
$.get_current_timezone, $.get_current_timezone,
$.get_language_info,
$.get_language_info_list,
$.get_media_prefix, $.get_media_prefix,
$.get_static_prefix, $.get_static_prefix,
$.if_group, $.if_group,
$.ifchanged_group, $.ifchanged_group,
$.include, $.include,
$.language_group,
$.load, $.load,
$.localize_group, $.localize_group,
$.localtime_group, $.localtime_group,
@@ -410,6 +425,7 @@ const django = grammar({
$.static, $.static,
$.template_tag_block, $.template_tag_block,
$.timezone_group, $.timezone_group,
$.translate,
$.url_block, $.url_block,
$.verbatim_group, $.verbatim_group,
$.width_ratio, $.width_ratio,
@@ -427,6 +443,7 @@ const django = grammar({
"name", "name",
choice( choice(
...FILTERS_WITHOUT_ARGUMENT, ...FILTERS_WITHOUT_ARGUMENT,
...I18N_FILTERS_WITHOUT_ARGUMENT,
...L10N_FILTERS_WITHOUT_ARGUMENT, ...L10N_FILTERS_WITHOUT_ARGUMENT,
...TZ_FILTERS_WITHOUT_ARGUMENT, ...TZ_FILTERS_WITHOUT_ARGUMENT,
), ),
@@ -549,6 +566,23 @@ const django = grammar({
optional(seq(block("empty"), optional($.template))), optional(seq(block("empty"), optional($.template))),
block("endfor"), block("endfor"),
), ),
/**
* django.templatetags.i18n. Each of these requires exactly "as <name>",
* or "for <expression> as <name>", and rejects anything further.
*/
get_available_languages: ($) =>
block("get_available_languages", asVariable($)),
get_current_language: ($) => block("get_current_language", asVariable($)),
get_current_language_bidi: ($) =>
block("get_current_language_bidi", asVariable($)),
get_language_info: ($) =>
block("get_language_info", part("for", $.filtered_value), asVariable($)),
get_language_info_list: ($) =>
block(
"get_language_info_list",
part("for", $.filtered_value),
asVariable($),
),
/** /**
* django.templatetags.tz. get_current_timezone_tag requires exactly * django.templatetags.tz. get_current_timezone_tag requires exactly
* "as <name>", so unlike get_static_prefix the clause is not optional and * "as <name>", so unlike get_static_prefix the clause is not optional and
@@ -574,6 +608,13 @@ const django = grammar({
block("endifchanged"), block("endifchanged"),
), ),
include: ($) => block("include", part($.filtered_value)), include: ($) => block("include", part($.filtered_value)),
/** django.templatetags.i18n. The language tag takes the one argument. */
language_group: ($) =>
seq(
block("language", part($.filtered_value)),
optional($.template),
block("endlanguage"),
),
library: ($) => seq($.identifier, optional(seq(".", $.identifier))), library: ($) => seq($.identifier, optional(seq(".", $.identifier))),
load: ($) => load: ($) =>
block( block(
@@ -653,6 +694,23 @@ const django = grammar({
optional($.template), optional($.template),
block("endtimezone"), block("endtimezone"),
), ),
/**
* django.templatetags.i18n, registered under both spellings. do_translate
* pops its options off one at a time and refuses one it has already seen,
* so they may be written in any order but none of them twice.
*/
translate: ($) =>
block(
choice("trans", "translate"),
part($.filtered_value),
optional(
arrangements([
part("noop"),
part("context", $.filtered_value),
asVariable($),
]),
),
),
url_block: ($) => url_block: ($) =>
block( block(
"url", "url",
+13
View File
@@ -68,7 +68,12 @@ tag: [
("for") ("for")
("empty") ("empty")
("endfor") ("endfor")
("get_available_languages")
("get_current_language")
("get_current_language_bidi")
("get_current_timezone") ("get_current_timezone")
("get_language_info")
("get_language_info_list")
("get_media_prefix") ("get_media_prefix")
("get_static_prefix") ("get_static_prefix")
("if") ("if")
@@ -78,6 +83,8 @@ tag: [
("ifchanged") ("ifchanged")
("endifchanged") ("endifchanged")
("include") ("include")
("language")
("endlanguage")
("lorem") ("lorem")
("load") ("load")
("localize") ("localize")
@@ -97,6 +104,8 @@ tag: [
("templatetag") ("templatetag")
("timezone") ("timezone")
("endtimezone") ("endtimezone")
("trans")
("translate")
("url") ("url")
("verbatim") ("verbatim")
("endverbatim") ("endverbatim")
@@ -127,6 +136,10 @@ tag: [
("iriencode") ("iriencode")
("join") ("join")
("json_script") ("json_script")
("language_bidi")
("language_name")
("language_name_local")
("language_name_translated")
("last") ("last")
("length") ("length")
("linebreaks") ("linebreaks")
+30
View File
@@ -24,6 +24,36 @@
(get_media_prefix (get_media_prefix
tag: ("get_media_prefix") @library.static) tag: ("get_media_prefix") @library.static)
(translate
tag: [
("trans")
("translate")] @library.i18n)
(language_group
tag: ("language") @library.i18n)
(get_available_languages
tag: ("get_available_languages") @library.i18n)
(get_current_language
tag: ("get_current_language") @library.i18n)
(get_current_language_bidi
tag: ("get_current_language_bidi") @library.i18n)
(get_language_info
tag: ("get_language_info") @library.i18n)
(get_language_info_list
tag: ("get_language_info_list") @library.i18n)
(filter
name: [
("language_bidi")
("language_name")
("language_name_local")
("language_name_translated")] @library.i18n)
(cache_group (cache_group
tag: ("cache") @library.cache) tag: ("cache") @library.cache)
+1494
View File
File diff suppressed because it is too large. Load diff
+328
View File
@@ -55,10 +55,30 @@
"type": "for_group", "type": "for_group",
"named": true "named": true
}, },
{
"type": "get_available_languages",
"named": true
},
{
"type": "get_current_language",
"named": true
},
{
"type": "get_current_language_bidi",
"named": true
},
{ {
"type": "get_current_timezone", "type": "get_current_timezone",
"named": true "named": true
}, },
{
"type": "get_language_info",
"named": true
},
{
"type": "get_language_info_list",
"named": true
},
{ {
"type": "get_media_prefix", "type": "get_media_prefix",
"named": true "named": true
@@ -79,6 +99,10 @@
"type": "include", "type": "include",
"named": true "named": true
}, },
{
"type": "language_group",
"named": true
},
{ {
"type": "load", "type": "load",
"named": true "named": true
@@ -135,6 +159,10 @@
"type": "timezone_group", "type": "timezone_group",
"named": true "named": true
}, },
{
"type": "translate",
"named": true
},
{ {
"type": "url_block", "type": "url_block",
"named": true "named": true
@@ -606,6 +634,22 @@
"type": "json_script", "type": "json_script",
"named": false "named": false
}, },
{
"type": "language_bidi",
"named": false
},
{
"type": "language_name",
"named": false
},
{
"type": "language_name_local",
"named": false
},
{
"type": "language_name_translated",
"named": false
},
{ {
"type": "last", "type": "last",
"named": false "named": false
@@ -948,6 +992,84 @@
] ]
} }
}, },
{
"type": "get_available_languages",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "get_available_languages",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "get_current_language",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "get_current_language",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "get_current_language_bidi",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "get_current_language_bidi",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{ {
"type": "get_current_timezone", "type": "get_current_timezone",
"named": true, "named": true,
@@ -974,6 +1096,78 @@
} }
} }
}, },
{
"type": "get_language_info",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "get_language_info",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "filtered_value",
"named": true
}
]
}
},
{
"type": "get_language_info_list",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "get_language_info_list",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "filtered_value",
"named": true
}
]
}
},
{ {
"type": "get_media_prefix", "type": "get_media_prefix",
"named": true, "named": true,
@@ -1132,6 +1326,40 @@
] ]
} }
}, },
{
"type": "language_group",
"named": true,
"fields": {
"tag": {
"multiple": true,
"required": true,
"types": [
{
"type": "endlanguage",
"named": false
},
{
"type": "language",
"named": false
}
]
}
},
"children": {
"multiple": true,
"required": true,
"types": [
{
"type": "filtered_value",
"named": true
},
{
"type": "template",
"named": true
}
]
}
},
{ {
"type": "library", "type": "library",
"named": true, "named": true,
@@ -1667,6 +1895,46 @@
] ]
} }
}, },
{
"type": "translate",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": true,
"types": [
{
"type": "trans",
"named": false
},
{
"type": "translate",
"named": false
}
]
},
"variable": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
}
]
}
},
"children": {
"multiple": true,
"required": true,
"types": [
{
"type": "filtered_value",
"named": true
}
]
}
},
{ {
"type": "translated_string", "type": "translated_string",
"named": true, "named": true,
@@ -2014,6 +2282,10 @@
"type": "content", "type": "content",
"named": true "named": true
}, },
{
"type": "context",
"named": false
},
{ {
"type": "csp_nonce_attr", "type": "csp_nonce_attr",
"named": false "named": false
@@ -2102,6 +2374,10 @@
"type": "endifchanged", "type": "endifchanged",
"named": false "named": false
}, },
{
"type": "endlanguage",
"named": false
},
{ {
"type": "endlocalize", "type": "endlocalize",
"named": false "named": false
@@ -2178,6 +2454,18 @@
"type": "from", "type": "from",
"named": false "named": false
}, },
{
"type": "get_available_languages",
"named": false
},
{
"type": "get_current_language",
"named": false
},
{
"type": "get_current_language_bidi",
"named": false
},
{ {
"type": "get_current_timezone", "type": "get_current_timezone",
"named": false "named": false
@@ -2186,6 +2474,14 @@
"type": "get_digit", "type": "get_digit",
"named": false "named": false
}, },
{
"type": "get_language_info",
"named": false
},
{
"type": "get_language_info_list",
"named": false
},
{ {
"type": "get_media_prefix", "type": "get_media_prefix",
"named": false "named": false
@@ -2234,6 +2530,26 @@
"type": "json_script", "type": "json_script",
"named": false "named": false
}, },
{
"type": "language",
"named": false
},
{
"type": "language_bidi",
"named": false
},
{
"type": "language_name",
"named": false
},
{
"type": "language_name_local",
"named": false
},
{
"type": "language_name_translated",
"named": false
},
{ {
"type": "last", "type": "last",
"named": false "named": false
@@ -2286,6 +2602,10 @@
"type": "media", "type": "media",
"named": false "named": false
}, },
{
"type": "noop",
"named": false
},
{ {
"type": "not", "type": "not",
"named": false "named": false
@@ -2458,6 +2778,14 @@
"type": "title", "type": "title",
"named": false "named": false
}, },
{
"type": "trans",
"named": false
},
{
"type": "translate",
"named": false
},
{ {
"type": "truncatechars", "type": "truncatechars",
"named": false "named": false
+48675 -38772
View File
File diff suppressed because it is too large. Load diff
+488
View File
@@ -0,0 +1,488 @@
==================
Trans
==================
{% trans "hi" %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string)))))
(content))
==================
Translate
==================
{% translate "hi" %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string)))))
(content))
==================
Trans With A Variable
==================
{% trans greeting %}
---
(template
(content)
(translate
(filtered_value
(value
(variable_attribute
(identifier)))))
(content))
==================
Trans With A Filtered Message
==================
{% trans "hi"|upper %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string)))
(filter_expression
(filter))))
(content))
==================
Trans Noop
==================
{% trans "hi" noop %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string)))))
(content))
==================
Trans With As
==================
{% trans "hi" as greeting %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
variable: (identifier))
(content))
==================
Trans With Context
==================
{% trans "hi" context "menu" %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
(filtered_value
(value
(literal
(string)))))
(content))
==================
Trans With Every Option
==================
{% trans "hi" context "menu" noop as greeting %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
(filtered_value
(value
(literal
(string))))
variable: (identifier))
(content))
==================
Trans With Its Options Reordered
==================
{% trans "hi" as greeting noop context "menu" %}
---
(template
(content)
(translate
(filtered_value
(value
(literal
(string))))
variable: (identifier)
(filtered_value
(value
(literal
(string)))))
(content))
==================
Trans Without A Message
:error
==================
{% trans %}
---
==================
Trans With An Option Twice
:error
==================
{% trans "hi" noop noop %}
---
==================
Trans With As Twice
:error
==================
{% trans "hi" as x as y %}
---
==================
Trans With An Unknown Option
:error
==================
{% trans "hi" bogus %}
---
==================
Trans With Context And No Value
:error
==================
{% trans "hi" context %}
---
==================
Get Available Languages
==================
{% get_available_languages as languages %}
---
(template
(content)
(get_available_languages
variable: (identifier))
(content))
==================
Get Available Languages Without As
:error
==================
{% get_available_languages %}
---
==================
Get Current Language
==================
{% get_current_language as language %}
---
(template
(content)
(get_current_language
variable: (identifier))
(content))
==================
Get Current Language Bidi
==================
{% get_current_language_bidi as bidi %}
---
(template
(content)
(get_current_language_bidi
variable: (identifier))
(content))
==================
Get Language Info
==================
{% get_language_info for "de" as info %}
---
(template
(content)
(get_language_info
(filtered_value
(value
(literal
(string))))
variable: (identifier))
(content))
==================
Get Language Info For A Variable
==================
{% get_language_info for code as info %}
---
(template
(content)
(get_language_info
(filtered_value
(value
(variable_attribute
(identifier))))
variable: (identifier))
(content))
==================
Get Language Info Without As
:error
==================
{% get_language_info for "de" %}
---
==================
Get Language Info Without For
:error
==================
{% get_language_info "de" as info %}
---
==================
Get Language Info List
==================
{% get_language_info_list for codes as info %}
---
(template
(content)
(get_language_info_list
(filtered_value
(value
(variable_attribute
(identifier))))
variable: (identifier))
(content))
==================
Language
==================
{% language "de" %}x{% endlanguage %}
---
(template
(content)
(language_group
(filtered_value
(value
(literal
(string))))
(template
(content)))
(content))
==================
Language With A Variable
==================
{% language code %}x{% endlanguage %}
---
(template
(content)
(language_group
(filtered_value
(value
(variable_attribute
(identifier))))
(template
(content)))
(content))
==================
Language Without An Argument
:error
==================
{% language %}x{% endlanguage %}
---
==================
Language With Two Arguments
:error
==================
{% language "a" "b" %}x{% endlanguage %}
---
==================
Language Name Filter
==================
{{ "de"|language_name }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(string)))
(filter_expression
(filter))))
(content))
==================
Language Bidi Filter
==================
{{ "de"|language_bidi }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(string)))
(filter_expression
(filter))))
(content))
==================
Language Name Local Filter
==================
{{ "de"|language_name_local }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(string)))
(filter_expression
(filter))))
(content))
==================
Language Name Translated Filter
==================
{{ "de"|language_name_translated }}
---
(template
(content)
(template_variable
(filtered_value
(value
(literal
(string)))
(filter_expression
(filter))))
(content))
==================
Language Name Filter With An Argument
:error
==================
{{ "de"|language_name:1 }}
---