Skip to content

Commit

Permalink
Include i18n and i10n tags and filters
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Mar 11, 2025
1 parent ff6681c commit 2509311
Show file tree
Hide file tree
Showing 23 changed files with 3,354 additions and 20 deletions.
4 changes: 4 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class MyLiquidEnvironment(Environment):
return dict(self.globals)
```

## Extra tags and filters

TODO:

## Tolerance

Templates are parsed and rendered in strict mode by default. Where syntax and render-time type errors raise an exception as soon as possible. You can change the error tolerance mode with the `tolerance` argument to [`Environment`](api/environment.md).
Expand Down
66 changes: 54 additions & 12 deletions liquid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pathlib import Path
from typing import Iterable
from typing import Iterator
from typing import Optional
from typing import TextIO
from typing import Union

from markupsafe import Markup
Expand Down Expand Up @@ -44,6 +47,9 @@
from .analyze_tags import TagAnalysis
from .analyze_tags import DEFAULT_INNER_TAG_MAP

from .messages import MessageTuple
from .messages import Translations
from .messages import extract_from_template

from .stream import TokenStream
from .tag import Tag
Expand All @@ -54,18 +60,22 @@

__all__ = (
"AwareBoundTemplate",
"BlockNode",
"BoundTemplate",
"CachingChoiceLoader",
"CachingDictLoader",
"CachingFileSystemLoader",
"CachingLoaderMixin",
"ChoiceLoader",
"RenderContext",
"ConditionalBlockNode",
"DebugUndefined",
"DEFAULT_INNER_TAG_MAP",
"DictLoader",
"Environment",
"escape",
"Expression",
"CachingDictLoader",
"extract_from_template",
"FalsyStrictUndefined",
"FileSystemLoader",
"future",
"FutureAwareBoundTemplate",
Expand All @@ -74,26 +84,25 @@
"is_undefined",
"make_choice_loader",
"make_file_system_loader",
"CachingLoaderMixin",
"Markup",
"MessageTuple",
"Mode",
"Node",
"PackageLoader",
"parse",
"render_async",
"render",
"RenderContext",
"soft_str",
"StrictDefaultUndefined",
"StrictUndefined",
"Tag",
"TagAnalysis",
"Template",
"Token",
"Undefined",
"TokenStream",
"parse",
"render",
"render_async",
"Node",
"BlockNode",
"ConditionalBlockNode",
"Tag",
"FalsyStrictUndefined",
"Translations",
"Undefined",
)

DEFAULT_ENVIRONMENT = Environment()
Expand Down Expand Up @@ -202,3 +211,36 @@ def make_choice_loader(
)

return ChoiceLoader(loaders=loaders)


def extract_liquid(
fileobj: TextIO,
keywords: list[str],
comment_tags: Optional[list[str]] = None,
options: Optional[dict[object, object]] = None, # noqa: ARG001
) -> Iterator[MessageTuple]:
"""A babel compatible translation message extraction method for Liquid templates.
See https://babel.pocoo.org/en/latest/messages.html
Keywords are the names of Liquid filters or tags operating on translatable
strings. For a filter to contribute to message extraction, it must also
appear as a child of a `FilteredExpression` and be a `TranslatableFilter`.
Similarly, tags must produce a node that is a `TranslatableTag`.
Where a Liquid comment contains a prefix in `comment_tags`, the comment
will be attached to the translatable filter or tag immediately following
the comment. Python Liquid's non-standard shorthand comments are not
supported.
Options are arguments passed to the `liquid.Template` constructor with the
contents of `fileobj` as the template's source. Use `extract_from_template`
to extract messages from an existing template bound to an existing
environment.
"""
template = Environment(extra=True).parse(fileobj.read())
return extract_from_template(
template=template,
keywords=keywords,
comment_tags=comment_tags,
)
2 changes: 2 additions & 0 deletions liquid/builtin/expressions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .arguments import Parameter
from .arguments import PositionalArgument
from .arguments import parse_arguments
from .filtered import Filter
from .filtered import FilteredExpression
from .filtered import TernaryFilteredExpression
from .logical import BooleanExpression
Expand Down Expand Up @@ -42,4 +43,5 @@
"tokenize",
"parse_name",
"Parameter",
"Filter",
)
3 changes: 3 additions & 0 deletions liquid/builtin/expressions/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def location(self) -> Location:
"""
return tuple([s.location() if isinstance(s, Path) else s for s in self.path])

def head(self) -> int | str | Path:
return self.path[0]

def evaluate(self, context: RenderContext) -> object:
return context.get(
[p.evaluate(context) if isinstance(p, Path) else p for p in self.path],
Expand Down
5 changes: 5 additions & 0 deletions liquid/builtin/tags/comment_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class CommentTag(Tag):

def parse(self, stream: TokenStream) -> CommentNode:
"""Parse tokens from _stream_ into an AST node."""
if stream.current.kind == "COMMENT":
# A `{# #}` style comment
return self.node_class(stream.current, text=stream.current.value)

# A block `{% comment %}`
token = stream.eat(TOKEN_TAG)
text = []

Expand Down
16 changes: 16 additions & 0 deletions liquid/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,22 @@ class LocalNamespaceLimitError(ResourceLimitError):
"""Exception raised when a local namespace limit has been exceeded."""


class TranslationError(Error):
"""Base exception for translation errors."""


class TranslationSyntaxError(LiquidSyntaxError):
"""Exception raised when a syntax error is found within a translation block."""


class TranslationValueError(TranslationError):
"""Exception raised when message interpolation fails with a ValueError."""


class TranslationKeyError(TranslationError):
"""Exception raised when message interpolation fails with a KeyError."""


# LiquidValueError inheriting from LiquidSyntaxError does not make complete sense.
# The alternative is to have multiple to_int functions that raise more appropriate
# exceptions depending on whether we are parsing or rendering when attempting to
Expand Down
35 changes: 35 additions & 0 deletions liquid/extra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
from typing import TYPE_CHECKING

from .filters import JSON
from .filters import Currency
from .filters import DateTime
from .filters import GetText
from .filters import NGetText
from .filters import NPGetText
from .filters import Number
from .filters import PGetText
from .filters import Translate
from .filters import Unit
from .filters import index
from .filters import script_tag
from .filters import sort_numeric
Expand All @@ -11,6 +20,7 @@
from .tags import CallTag
from .tags import ExtendsTag
from .tags import MacroTag
from .tags import TranslateTag
from .tags import WithTag

if TYPE_CHECKING:
Expand All @@ -31,6 +41,13 @@
"sort_numeric",
"stylesheet_tag",
"WithTag",
"Unit",
"Number",
"GetText",
"PGetText",
"PGetText",
"Currency",
"DateTime",
)


Expand All @@ -41,6 +58,7 @@ def add_tags(env: Environment) -> None:
env.add_tag(MacroTag)
env.add_tag(CallTag)
env.add_tag(WithTag)
env.add_tag(TranslateTag)


def add_filters(env: Environment) -> None:
Expand All @@ -51,6 +69,23 @@ def add_filters(env: Environment) -> None:
env.add_filter("sort_numeric", sort_numeric)
env.add_filter("stylesheet_tag", stylesheet_tag)

env.filters[GetText.name] = GetText(autoescape_message=env.autoescape)
env.filters[NGetText.name] = NGetText(autoescape_message=env.autoescape)
env.filters[NPGetText.name] = NPGetText(autoescape_message=env.autoescape)
env.filters[PGetText.name] = PGetText(autoescape_message=env.autoescape)
env.filters[Translate.name] = Translate(autoescape_message=env.autoescape)
env.filters["currency"] = Currency()
env.filters["money"] = Currency()
env.filters["money_with_currency"] = Currency(default_format="¤#,##0.00 ¤¤")
env.filters["money_without_currency"] = Currency(default_format="#,##0.00")
env.filters["money_without_trailing_zeros"] = Currency(
default_format="¤#,###",
currency_digits=False,
)
env.filters["datetime"] = DateTime()
env.filters["decimal"] = Number()
env.filters["unit"] = Unit()


def add_tags_and_filters(env: Environment) -> None: # pragma: no cover
"""Register all extra tags and filters with an environment."""
Expand Down
20 changes: 20 additions & 0 deletions liquid/extra/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
from ._json import JSON # noqa: D104
from .array import index
from .array import sort_numeric
from .babel import Currency
from .babel import DateTime
from .babel import Number
from .babel import Unit
from .html import script_tag
from .html import stylesheet_tag
from .translate import BaseTranslateFilter
from .translate import GetText
from .translate import NGetText
from .translate import NPGetText
from .translate import PGetText
from .translate import Translate

__all__ = (
"index",
"JSON",
"script_tag",
"sort_numeric",
"stylesheet_tag",
"Currency",
"DateTime",
"Number",
"Unit",
"BaseTranslateFilter",
"GetText",
"NGetText",
"NPGetText",
"PGetText",
"Translate",
)
Loading

0 comments on commit 2509311

Please sign in to comment.