Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 19 additions & 0 deletions fluent.runtime/docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ 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 Web
Component confirm window with an OK button, a Cancel button, and a
message.

.. code-block:: python
>>> l10n = DemoLocalization("""
... order-cancel-window = Are you sure you want to cancel the order #{ $order }?
... .ok = Yes
... .cancel = No
""")
>>> message, attributes = l10n.format_message("order-cancel-window", {'order': 123})
>>> message
'Are you sure you want to cancel the order #123?'
>>> attributes
{'ok': 'Yes', 'cancel': 'No'}

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
35 changes: 35 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,35 @@ 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)
formatted_attrs = None
if msg.attributes:
formatted_attrs = {
attr: cast(
str,
bundle.format_pattern(msg.attributes[attr], args)[0],
)
for attr in msg.attributes
}
if not msg.value and formatted_attrs is None:
continue
elif not msg.value and formatted_attrs:
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 if formatted_attrs else {},
)
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