Skip to content

Commit 4656173

Browse files
committed
runtime: add attribute formatting functionality in FluentLocalization
1 parent e4fc3c2 commit 4656173

File tree

4 files changed

+110
-6
lines changed

4 files changed

+110
-6
lines changed

fluent.runtime/docs/usage.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,25 @@ instances to indicate an error or missing data. Otherwise they should
272272
return unicode strings, or instances of a ``FluentType`` subclass as
273273
above.
274274

275+
Attributes
276+
~~~~~~~~~~
277+
When rendering UI elements, it's handy to have a single translation that
278+
contains everything you need in one variable. For example, a Web
279+
Component confirm window with an OK button, a Cancel button, and a
280+
message.
281+
282+
.. code-block:: python
283+
>>> l10n = DemoLocalization("""
284+
... order-cancel-window = Are you sure you want to cancel the order #{ $order }?
285+
... .ok = Yes
286+
... .cancel = No
287+
""")
288+
>>> message, attributes = l10n.format_message("order-cancel-window", {'order': 123})
289+
>>> message
290+
'Are you sure you want to cancel the order #123?'
291+
>>> attributes
292+
{'ok': 'Yes', 'cancel': 'No'}
293+
275294
Known limitations and bugs
276295
~~~~~~~~~~~~~~~~~~~~~~~~~~
277296

fluent.runtime/fluent/runtime/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
from fluent.syntax.ast import Resource
33

44
from .bundle import FluentBundle
5-
from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader
5+
from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader, FormattedMessage
66

77
__all__ = [
88
"FluentLocalization",
99
"AbstractResourceLoader",
1010
"FluentResourceLoader",
1111
"FluentResource",
1212
"FluentBundle",
13+
"FormattedMessage",
1314
]
1415

1516

fluent.runtime/fluent/runtime/fallback.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from fluent.syntax import FluentParser
16+
from typing_extensions import NamedTuple
1617

1718
from .bundle import FluentBundle
1819

@@ -22,6 +23,11 @@
2223
from .types import FluentType
2324

2425

26+
class FormattedMessage(NamedTuple):
27+
message: Union[str, None]
28+
attributes: Dict[str, str]
29+
30+
2531
class FluentLocalization:
2632
"""
2733
Generic API for Fluent applications.
@@ -48,6 +54,35 @@ def __init__(
4854
self._bundle_cache: List[FluentBundle] = []
4955
self._bundle_it = self._iterate_bundles()
5056

57+
def format_message(
58+
self, msg_id: str, args: Union[Dict[str, Any], None] = None
59+
) -> FormattedMessage:
60+
for bundle in self._bundles():
61+
if not bundle.has_message(msg_id):
62+
continue
63+
msg = bundle.get_message(msg_id)
64+
formatted_attrs = None
65+
if msg.attributes:
66+
formatted_attrs = {
67+
attr: cast(
68+
str,
69+
bundle.format_pattern(msg.attributes[attr], args)[0],
70+
)
71+
for attr in msg.attributes
72+
}
73+
if not msg.value and formatted_attrs is None:
74+
continue
75+
elif not msg.value and formatted_attrs:
76+
val = None
77+
else:
78+
val, _errors = bundle.format_pattern(msg.value, args)
79+
return FormattedMessage(
80+
# Never FluentNone when format_pattern called externally
81+
cast(str, val),
82+
formatted_attrs if formatted_attrs else {},
83+
)
84+
return FormattedMessage(msg_id, {})
85+
5186
def format_value(
5287
self, msg_id: str, args: Union[Dict[str, Any], None] = None
5388
) -> str:

fluent.runtime/tests/test_fallback.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,25 @@ def test_init(self):
1212
self.assertTrue(callable(l10n.format_value))
1313

1414
@patch_files({
15-
"de/one.ftl": "one = in German",
16-
"de/two.ftl": "two = in German",
17-
"fr/two.ftl": "three = in French",
18-
"en/one.ftl": "four = exists",
19-
"en/two.ftl": "five = exists",
15+
"de/one.ftl": """one = in German
16+
.foo = one in German
17+
""",
18+
"de/two.ftl": """two = in German
19+
.foo = two in German
20+
""",
21+
"fr/two.ftl": """three = in French
22+
.foo = three in French
23+
""",
24+
"en/one.ftl": """four = exists
25+
.foo = four in English
26+
""",
27+
"en/two.ftl": """
28+
five = exists
29+
.foo = five in English
30+
bar =
31+
.foo = bar in English
32+
baz = baz in English
33+
""",
2034
})
2135
def test_bundles(self):
2236
l10n = FluentLocalization(
@@ -39,6 +53,41 @@ def test_bundles(self):
3953
self.assertEqual(l10n.format_value("three"), "in French")
4054
self.assertEqual(l10n.format_value("four"), "exists")
4155
self.assertEqual(l10n.format_value("five"), "exists")
56+
self.assertEqual(l10n.format_value("bar"), "bar")
57+
self.assertEqual(l10n.format_value("baz"), "baz in English")
58+
self.assertEqual(l10n.format_value("not-exists"), "not-exists")
59+
self.assertEqual(
60+
tuple(l10n.format_message("one")),
61+
("in German", {"foo": "one in German"}),
62+
)
63+
self.assertEqual(
64+
tuple(l10n.format_message("two")),
65+
("in German", {"foo": "two in German"}),
66+
)
67+
self.assertEqual(
68+
tuple(l10n.format_message("three")),
69+
("in French", {"foo": "three in French"}),
70+
)
71+
self.assertEqual(
72+
tuple(l10n.format_message("four")),
73+
("exists", {"foo": "four in English"}),
74+
)
75+
self.assertEqual(
76+
tuple(l10n.format_message("five")),
77+
("exists", {"foo": "five in English"}),
78+
)
79+
self.assertEqual(
80+
tuple(l10n.format_message("bar")),
81+
(None, {"foo": "bar in English"}),
82+
)
83+
self.assertEqual(
84+
tuple(l10n.format_message("baz")),
85+
("baz in English", {}),
86+
)
87+
self.assertEqual(
88+
tuple(l10n.format_message("not-exists")),
89+
("not-exists", {}),
90+
)
4291

4392

4493
class TestResourceLoader(unittest.TestCase):

0 commit comments

Comments
 (0)