Skip to content
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
33 changes: 33 additions & 0 deletions fluent.runtime/docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,39 @@ instances to indicate an error or missing data. Otherwise they should
return unicode strings, or instances of a ``FluentType`` subclass as
above.

Attributes
~~~~~~~~~~
When rendering UI elements, it's handy to have a single translation that
contains everything you need in one variable. For example, a HTML
form input may have a value, but also a placeholder attribute, aria-label
attribute, and maybe a title attribute.

.. code-block:: python
>>> l10n = DemoLocalization("""
... login-input = Predefined value
... .placeholder = { $email }
... .aria-label = Login input value
... .title = Type your login email
""")
>>> message, attributes = l10n.format_message(
"login-input", {"placeholder": "email@example.com"}
)
>>> message
'Predefined value'
>>> attributes
{'placeholder': 'email@example.com', 'aria-label': 'Login input value', 'title': 'Type your login email'}

You can also use the formatted message without unpacking it.

.. code-block:: python
>>> fmt_msg = l10n.format_message(
"login-input", {"placeholder": "email@example.com"}
)
>>> fmt_msg.message
'Predefined value'
>>> fmt_msg.attributes
{'placeholder': 'email@example.com', 'aria-label': 'Login input value', 'title': 'Type your login email'}

Known limitations and bugs
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion fluent.runtime/fluent/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
from fluent.syntax.ast import Resource

from .bundle import FluentBundle
from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader
from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader, FormattedMessage

__all__ = [
"FluentLocalization",
"AbstractResourceLoader",
"FluentResourceLoader",
"FluentResource",
"FluentBundle",
"FormattedMessage",
]


Expand Down
31 changes: 31 additions & 0 deletions fluent.runtime/fluent/runtime/fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)

from fluent.syntax import FluentParser
from typing_extensions import NamedTuple

from .bundle import FluentBundle

Expand All @@ -22,6 +23,11 @@
from .types import FluentType


class FormattedMessage(NamedTuple):
message: Union[str, None]
attributes: Dict[str, str]


class FluentLocalization:
"""
Generic API for Fluent applications.
Expand All @@ -48,6 +54,31 @@ def __init__(
self._bundle_cache: List[FluentBundle] = []
self._bundle_it = self._iterate_bundles()

def format_message(
self, msg_id: str, args: Union[Dict[str, Any], None] = None
) -> FormattedMessage:
for bundle in self._bundles():
if not bundle.has_message(msg_id):
continue
msg = bundle.get_message(msg_id)
Comment on lines +60 to +63
Copy link
Member

Choose a reason for hiding this comment

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

Why not something like this?

Suggested change
for bundle in self._bundles():
if not bundle.has_message(msg_id):
continue
msg = bundle.get_message(msg_id)
msg = next((
bundle.get_message(msg_id)
for bundle in self._bundles()
if bundle.has_message(msg_id)
), None)

Copy link
Author

Choose a reason for hiding this comment

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

The first reason is consistency, format_value also uses a for loop. The second reason is simplicity: the code looks linear and less bloated to those seeing it for the first time.

formatted_attrs = {
attr: cast(
str,
bundle.format_pattern(msg.attributes[attr], args)[0],
)
for attr in msg.attributes
}
if not msg.value:
val = None
else:
val, _errors = bundle.format_pattern(msg.value, args)
return FormattedMessage(
# Never FluentNone when format_pattern called externally
cast(str, val),
formatted_attrs,
)
return FormattedMessage(msg_id, {})

def format_value(
self, msg_id: str, args: Union[Dict[str, Any], None] = None
) -> str:
Expand Down
59 changes: 54 additions & 5 deletions fluent.runtime/tests/test_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ def test_init(self):
self.assertTrue(callable(l10n.format_value))

@patch_files({
"de/one.ftl": "one = in German",
"de/two.ftl": "two = in German",
"fr/two.ftl": "three = in French",
"en/one.ftl": "four = exists",
"en/two.ftl": "five = exists",
"de/one.ftl": """one = in German
.foo = one in German
""",
"de/two.ftl": """two = in German
.foo = two in German
""",
"fr/two.ftl": """three = in French
.foo = three in French
""",
"en/one.ftl": """four = exists
.foo = four in English
""",
"en/two.ftl": """
five = exists
.foo = five in English
bar =
.foo = bar in English
baz = baz in English
""",
})
def test_bundles(self):
l10n = FluentLocalization(
Expand All @@ -39,6 +53,41 @@ def test_bundles(self):
self.assertEqual(l10n.format_value("three"), "in French")
self.assertEqual(l10n.format_value("four"), "exists")
self.assertEqual(l10n.format_value("five"), "exists")
self.assertEqual(l10n.format_value("bar"), "bar")
self.assertEqual(l10n.format_value("baz"), "baz in English")
self.assertEqual(l10n.format_value("not-exists"), "not-exists")
self.assertEqual(
tuple(l10n.format_message("one")),
("in German", {"foo": "one in German"}),
)
self.assertEqual(
tuple(l10n.format_message("two")),
("in German", {"foo": "two in German"}),
)
self.assertEqual(
tuple(l10n.format_message("three")),
("in French", {"foo": "three in French"}),
)
self.assertEqual(
tuple(l10n.format_message("four")),
("exists", {"foo": "four in English"}),
)
self.assertEqual(
tuple(l10n.format_message("five")),
("exists", {"foo": "five in English"}),
)
self.assertEqual(
tuple(l10n.format_message("bar")),
(None, {"foo": "bar in English"}),
)
self.assertEqual(
tuple(l10n.format_message("baz")),
("baz in English", {}),
)
self.assertEqual(
tuple(l10n.format_message("not-exists")),
("not-exists", {}),
)


class TestResourceLoader(unittest.TestCase):
Expand Down