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
35 changes: 35 additions & 0 deletions fluent.runtime/docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,41 @@ 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
... """)
>>> value, attributes = l10n.format_message(
... "login-input", {"email": "email@example.com"}
... )
>>> value
'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", {"email": "email@example.com"}
... )
>>> fmt_msg.value
'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
55 changes: 44 additions & 11 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 import NamedTuple

from .bundle import FluentBundle

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


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


class FluentLocalization:
"""
Generic API for Fluent applications.
Expand All @@ -48,20 +54,47 @@ 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:
bundle, msg = next((
(bundle, bundle.get_message(msg_id))
for bundle in self._bundles()
if bundle.has_message(msg_id)
), (None, None))
if not msg:
return FormattedMessage(msg_id, {})
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,
)

def format_value(
self, msg_id: str, args: Union[Dict[str, Any], None] = None
) -> str:
for bundle in self._bundles():
if not bundle.has_message(msg_id):
continue
msg = bundle.get_message(msg_id)
if not msg.value:
continue
val, _errors = bundle.format_pattern(msg.value, args)
return cast(
str, val
) # Never FluentNone when format_pattern called externally
return msg_id
bundle, msg = next((
(bundle, bundle.get_message(msg_id))
for bundle in self._bundles()
if bundle.has_message(msg_id)
), (None, None))
if not msg or not msg.value:
return msg_id
val, _errors = bundle.format_pattern(msg.value, args)
return cast(
str, val
) # Never FluentNone when format_pattern called externally

def _create_bundle(self, locales: List[str]) -> FluentBundle:
return self.bundle_class(
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
Loading