Skip to content

Commit ba386c2

Browse files
authored
runtime: Add FluentLocalization.format_message() (#212)
1 parent 8c10de0 commit ba386c2

File tree

4 files changed

+135
-17
lines changed

4 files changed

+135
-17
lines changed

fluent.runtime/docs/usage.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,41 @@ 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 HTML
279+
form input may have a value, but also a placeholder attribute, aria-label
280+
attribute, and maybe a title attribute.
281+
282+
.. code-block:: python
283+
284+
>>> l10n = DemoLocalization("""
285+
... login-input = Predefined value
286+
... .placeholder = { $email }
287+
... .aria-label = Login input value
288+
... .title = Type your login email
289+
... """)
290+
>>> value, attributes = l10n.format_message(
291+
... "login-input", {"email": "[email protected]"}
292+
... )
293+
>>> value
294+
'Predefined value'
295+
>>> attributes
296+
{'placeholder': '[email protected]', 'aria-label': 'Login input value', 'title': 'Type your login email'}
297+
298+
You can also use the formatted message without unpacking it.
299+
300+
.. code-block:: python
301+
302+
>>> fmt_msg = l10n.format_message(
303+
... "login-input", {"email": "[email protected]"}
304+
... )
305+
>>> fmt_msg.value
306+
'Predefined value'
307+
>>> fmt_msg.attributes
308+
{'placeholder': '[email protected]', 'aria-label': 'Login input value', 'title': 'Type your login email'}
309+
275310
Known limitations and bugs
276311
~~~~~~~~~~~~~~~~~~~~~~~~~~
277312

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: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from fluent.syntax import FluentParser
16+
from typing 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+
value: Union[str, None]
28+
attributes: Dict[str, str]
29+
30+
2531
class FluentLocalization:
2632
"""
2733
Generic API for Fluent applications.
@@ -48,20 +54,47 @@ 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+
bundle, msg = next((
61+
(bundle, bundle.get_message(msg_id))
62+
for bundle in self._bundles()
63+
if bundle.has_message(msg_id)
64+
), (None, None))
65+
if not bundle or not msg:
66+
return FormattedMessage(msg_id, {})
67+
formatted_attrs = {
68+
attr: cast(
69+
str,
70+
bundle.format_pattern(msg.attributes[attr], args)[0],
71+
)
72+
for attr in msg.attributes
73+
}
74+
if not msg.value:
75+
val = None
76+
else:
77+
val, _errors = bundle.format_pattern(msg.value, args)
78+
return FormattedMessage(
79+
# Never FluentNone when format_pattern called externally
80+
cast(str, val),
81+
formatted_attrs,
82+
)
83+
5184
def format_value(
5285
self, msg_id: str, args: Union[Dict[str, Any], None] = None
5386
) -> str:
54-
for bundle in self._bundles():
55-
if not bundle.has_message(msg_id):
56-
continue
57-
msg = bundle.get_message(msg_id)
58-
if not msg.value:
59-
continue
60-
val, _errors = bundle.format_pattern(msg.value, args)
61-
return cast(
62-
str, val
63-
) # Never FluentNone when format_pattern called externally
64-
return msg_id
87+
bundle, msg = next((
88+
(bundle, bundle.get_message(msg_id))
89+
for bundle in self._bundles()
90+
if bundle.has_message(msg_id)
91+
), (None, None))
92+
if not bundle or not msg or not msg.value:
93+
return msg_id
94+
val, _errors = bundle.format_pattern(msg.value, args)
95+
return cast(
96+
str, val
97+
) # Never FluentNone when format_pattern called externally
6598

6699
def _create_bundle(self, locales: List[str]) -> FluentBundle:
67100
return self.bundle_class(

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)