Skip to content

Commit a05729e

Browse files
committed
Harmonize bundle API, expose get_message and format_pattern as API on bundles.
This change changes a lot of tests, as there's a shift from format() to format_pattern(get_message().value). With the get_message API, the complex lookups format('msg.attr') also expose an API surface that's not inline with other impls, so I removed quite a bit of utility code that served that purpose. And as I changed all lines referencing bundles, I named the local variable `bundle` instead of `ctx`. There's a bit of clean-up sneaking in to this patch, triggered by reshuffling the referencing code. Notably, all resolvers now consistently return fluent types. In some cases, they returned resolvers, which required resolving to get to types.
1 parent 5f6ab74 commit a05729e

17 files changed

+372
-357
lines changed

fluent.runtime/CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ fluent.runtime next
77
* Added ``fluent.runtime.FluentResource`` and
88
``fluent.runtime.FluentBundle.add_resource``.
99
* Removed ``fluent.runtime.FluentBundle.add_messages``.
10+
* Replaced ``bundle.format()`` with ``bundle.format_pattern(bundle.get_message().value)``.
1011

1112
fluent.runtime 0.2 (September 10, 2019)
1213
---------------------------------------

fluent.runtime/docs/usage.rst

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,31 +45,34 @@ file stored on disk, here we will just add them directly:
4545
... welcome = Welcome to this great app!
4646
... greet-by-name = Hello, { $name }!
4747
... """)
48-
>>> bundle.add_resource(resource
48+
>>> bundle.add_resource(resource)
4949
50-
To generate translations, use the ``format`` method, passing a message
51-
ID and an optional dictionary of substitution parameters. If the the
52-
message ID is not found, a ``LookupError`` is raised. Otherwise, as per
53-
the Fluent philosophy, the implementation tries hard to recover from any
54-
formatting errors and generate the most human readable representation of
55-
the value. The ``format`` method therefore returns a tuple containing
56-
``(translated string, errors)``, as below.
50+
To generate translations, use the ``get_message`` method to retrieve
51+
a message from the bundle. If the the message ID is not found, a
52+
``LookupError`` is raised. Then use the ``format_pattern`` method, passing
53+
the message value or one if its attributes and an optional dictionary of
54+
substitution parameters. As per the Fluent philosophy, the implementation
55+
tries hard to recover from any formatting errors and generate the most human
56+
readable representation of the value. The ``format_pattern`` method therefore
57+
returns a tuple containing ``(translated string, errors)``, as below.
5758

5859
.. code-block:: python
5960
60-
>>> translated, errs = bundle.format('welcome')
61+
>>> welcome = bundle.get_message('welcome')
62+
>>> translated, errs = bundle.format_pattern(welcome.value)
6163
>>> translated
6264
"Welcome to this great app!"
6365
>>> errs
6466
[]
6567
66-
>>> translated, errs = bundle.format('greet-by-name', {'name': 'Jane'})
68+
>>> greet = bundle.get_message('greet-by-name')
69+
>>> translated, errs = bundle.format_pattern(greet.value, {'name': 'Jane'})
6770
>>> translated
6871
'Hello, \u2068Jane\u2069!'
6972
70-
>>> translated, errs = bundle.format('greet-by-name', {})
73+
>>> translated, errs = bundle.format_pattern(greet.value, {})
7174
>>> translated
72-
'Hello, \u2068name\u2069!'
75+
'Hello, \u2068{$name}\u2069!'
7376
>>> errs
7477
[FluentReferenceError('Unknown external: name')]
7578
@@ -105,7 +108,8 @@ When rendering translations, Fluent passes any numeric arguments (``int``,
105108
>>> bundle.add_resource(FluentResource(
106109
... "show-total-points = You have { $points } points."
107110
... ))
108-
>>> val, errs = bundle.format("show-total-points", {'points': 1234567})
111+
>>> total_points = bundle.get_message("show-total-points")
112+
>>> val, errs = bundle.format_pattern(total_points.value, {'points': 1234567})
109113
>>> val
110114
'You have 1,234,567 points.'
111115
@@ -117,14 +121,15 @@ by wrapping your numeric arguments with
117121
118122
>>> from fluent.runtime.types import fluent_number
119123
>>> points = fluent_number(1234567, useGrouping=False)
120-
>>> bundle.format("show-total-points", {'points': points})[0]
124+
>>> val, errs = bundle.format_pattern(total_points.value, {'points': points})[0]
121125
'You have 1234567 points.'
122126
123127
>>> amount = fluent_number(1234.56, style="currency", currency="USD")
124128
>>> bundle.add_resource(FluentResource(
125129
... "your-balance = Your balance is { $amount }"
126130
... ))
127-
>>> bundle.format("your-balance", {'amount': amount})[0]
131+
>>> balance = bundle.get_message("your-balance")
132+
>>> bundle.format_pattern(balance.value, {'amount': amount})[0]
128133
'Your balance is $1,234.56'
129134
130135
The options available are defined in the Fluent spec for
@@ -142,7 +147,8 @@ passed through locale aware functions:
142147
143148
>>> from datetime import date
144149
>>> bundle.add_resource(FluentResource("today-is = Today is { $today }"))
145-
>>> val, errs = bundle.format("today-is", {"today": date.today() })
150+
>>> today_is = bundle.get_message("today-is")
151+
>>> val, errs = bundle.format(today_is.value, {"today": date.today() })
146152
>>> val
147153
'Today is Jun 16, 2018'
148154
@@ -171,7 +177,7 @@ To specify options from Python code, use
171177
>>> from fluent.runtime.types import fluent_date
172178
>>> today = date.today()
173179
>>> short_today = fluent_date(today, dateStyle='short')
174-
>>> val, errs = bundle.format("today-is", {"today": short_today })
180+
>>> val, errs = bundle.format_pattern(today_is, {"today": short_today })
175181
>>> val
176182
'Today is 6/17/18'
177183
@@ -201,7 +207,8 @@ ways:
201207
datetime.datetime(2018, 6, 17, 12, 15, 5, 677597)
202208
203209
>>> bundle.add_resource(FluentResource("now-is = Now is { $now }"))
204-
>>> val, errs = bundle.format("now-is",
210+
>>> now_is = bundle.get_message("now-is")
211+
>>> val, errs = bundle.format_pattern(now_is.value,
205212
... {"now": fluent_date(utcnow,
206213
... timeZone="Europe/Moscow",
207214
... dateStyle="medium",
@@ -233,7 +240,7 @@ You can add functions to the ones available to FTL authors by passing a
233240
... *[other] Welcome
234241
... }
235242
... """))
236-
>>> print(bundle.format('welcome')[0]
243+
>>> print(bundle.format_pattern(bundle.get_message('welcome'))[0])
237244
Welcome to Linux
238245
239246
These functions can accept positional and keyword arguments (like the

fluent.runtime/fluent/runtime/__init__.py

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .builtins import BUILTINS
1111
from .prepare import Compiler
1212
from .resolver import ResolverEnvironment, CurrentEnvironment
13-
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, ast_to_id, native_to_fluent
13+
from .utils import native_to_fluent
1414

1515

1616
def FluentResource(source):
@@ -20,12 +20,14 @@ def FluentResource(source):
2020

2121
class FluentBundle(object):
2222
"""
23-
Message contexts are single-language stores of translations. They are
24-
responsible for parsing translation resources in the Fluent syntax and can
23+
Bundles are single-language stores of translations. They are
24+
aggregate parsed Fluent resources in the Fluent syntax and can
2525
format translation units (entities) to strings.
2626
27-
Always use `FluentBundle.format` to retrieve translation units from
28-
a context. Translations can contain references to other entities or
27+
Always use `FluentBundle.get_message` to retrieve translation units from
28+
a bundle. Generate the localized string by using `format_pattern` on
29+
`message.value` or `message.attributes['attr']`.
30+
Translations can contain references to other entities or
2931
external arguments, conditional logic in form of select expressions, traits
3032
which describe their grammatical features, and can use Fluent builtins.
3133
See the documentation of the Fluent syntax for more information.
@@ -38,7 +40,8 @@ def __init__(self, locales, functions=None, use_isolating=True):
3840
_functions.update(functions)
3941
self._functions = _functions
4042
self.use_isolating = use_isolating
41-
self._messages_and_terms = {}
43+
self._messages = {}
44+
self._terms = {}
4245
self._compiled = {}
4346
self._compiler = Compiler()
4447
self._babel_locale = self._get_babel_locale()
@@ -47,30 +50,33 @@ def __init__(self, locales, functions=None, use_isolating=True):
4750
def add_resource(self, resource, allow_overrides=False):
4851
# TODO - warn/error about duplicates
4952
for item in resource.body:
50-
if isinstance(item, (Message, Term)):
51-
full_id = ast_to_id(item)
52-
if full_id not in self._messages_and_terms or allow_overrides:
53-
self._messages_and_terms[full_id] = item
53+
if not isinstance(item, (Message, Term)):
54+
continue
55+
map_ = self._messages if isinstance(item, Message) else self._terms
56+
full_id = item.id.name
57+
if full_id not in map_ or allow_overrides:
58+
map_[full_id] = item
5459

5560
def has_message(self, message_id):
56-
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
57-
return False
58-
return message_id in self._messages_and_terms
59-
60-
def lookup(self, full_id):
61-
if full_id not in self._compiled:
62-
entry_id = full_id.split(ATTRIBUTE_SEPARATOR, 1)[0]
63-
entry = self._messages_and_terms[entry_id]
64-
compiled = self._compiler(entry)
65-
if compiled.value is not None:
66-
self._compiled[entry_id] = compiled.value
67-
for attr in compiled.attributes:
68-
self._compiled[ATTRIBUTE_SEPARATOR.join([entry_id, attr.id.name])] = attr.value
69-
return self._compiled[full_id]
70-
71-
def format(self, message_id, args=None):
72-
if message_id.startswith(TERM_SIGIL):
73-
raise LookupError(message_id)
61+
return message_id in self._messages
62+
63+
def get_message(self, message_id):
64+
return self._lookup(message_id)
65+
66+
def _lookup(self, entry_id, term=False):
67+
if term:
68+
compiled_id = '-' + entry_id
69+
else:
70+
compiled_id = entry_id
71+
try:
72+
return self._compiled[compiled_id]
73+
except LookupError:
74+
pass
75+
entry = self._terms[entry_id] if term else self._messages[entry_id]
76+
self._compiled[compiled_id] = self._compiler(entry)
77+
return self._compiled[compiled_id]
78+
79+
def format_pattern(self, pattern, args=None):
7480
if args is not None:
7581
fluent_args = {
7682
argname: native_to_fluent(argvalue)
@@ -80,12 +86,11 @@ def format(self, message_id, args=None):
8086
fluent_args = {}
8187

8288
errors = []
83-
resolve = self.lookup(message_id)
8489
env = ResolverEnvironment(context=self,
8590
current=CurrentEnvironment(args=fluent_args),
8691
errors=errors)
8792
try:
88-
result = resolve(env)
93+
result = pattern(env)
8994
except ValueError as e:
9095
errors.append(e)
9196
result = '{???}'

fluent.runtime/fluent/runtime/resolver.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from __future__ import absolute_import, unicode_literals
22

33
import contextlib
4-
from datetime import date, datetime
5-
from decimal import Decimal
64

75
import attr
86
import six
@@ -92,12 +90,28 @@ class Literal(BaseResolver):
9290
pass
9391

9492

95-
class Message(FTL.Message, BaseResolver):
96-
pass
93+
class EntryResolver(BaseResolver):
94+
'''Entries (Messages and Terms) have attributes.
95+
In the AST they're a list, the resolver wants a dict. The helper method
96+
here should be called from the constructor.
97+
'''
98+
def _fix_attributes(self):
99+
self.attributes = {
100+
attr.id.name: attr.value
101+
for attr in self.attributes
102+
}
97103

98104

99-
class Term(FTL.Term, BaseResolver):
100-
pass
105+
class Message(FTL.Message, EntryResolver):
106+
def __init__(self, id, **kwargs):
107+
super(Message, self).__init__(id, **kwargs)
108+
self._fix_attributes()
109+
110+
111+
class Term(FTL.Term, EntryResolver):
112+
def __init__(self, id, value, **kwargs):
113+
super(Term, self).__init__(id, value, **kwargs)
114+
self._fix_attributes()
101115

102116

103117
class Pattern(FTL.Pattern, BaseResolver):
@@ -174,12 +188,26 @@ def __call__(self, env):
174188
return self.value
175189

176190

177-
class MessageReference(FTL.MessageReference, BaseResolver):
191+
class EntryReference(BaseResolver):
178192
def __call__(self, env):
179-
return lookup_reference(self, env)(env)
193+
try:
194+
entry = env.context._lookup(self.id.name, term=isinstance(self, FTL.TermReference))
195+
if self.attribute:
196+
pattern = entry.attributes[self.attribute.name]
197+
else:
198+
pattern = entry.value
199+
return pattern(env)
200+
except LookupError:
201+
ref_id = reference_to_id(self)
202+
env.errors.append(unknown_reference_error_obj(ref_id))
203+
return FluentNone('{{{}}}'.format(ref_id))
204+
180205

206+
class MessageReference(FTL.MessageReference, EntryReference):
207+
pass
181208

182-
class TermReference(FTL.TermReference, BaseResolver):
209+
210+
class TermReference(FTL.TermReference, EntryReference):
183211
def __call__(self, env):
184212
if self.arguments:
185213
if self.arguments.positional:
@@ -189,36 +217,7 @@ def __call__(self, env):
189217
else:
190218
kwargs = None
191219
with env.modified_for_term_reference(args=kwargs):
192-
return lookup_reference(self, env)(env)
193-
194-
195-
class FluentNoneResolver(FluentNone, BaseResolver):
196-
def __call__(self, env):
197-
return self.format(env.context._babel_locale)
198-
199-
200-
def lookup_reference(ref, env):
201-
"""
202-
Given a MessageReference, TermReference or AttributeExpression, returns the
203-
AST node, or FluentNone if not found, including fallback logic
204-
"""
205-
ref_id = reference_to_id(ref)
206-
try:
207-
return env.context.lookup(ref_id)
208-
except LookupError:
209-
env.errors.append(unknown_reference_error_obj(ref_id))
210-
211-
if ref.attribute:
212-
# Fallback
213-
parent_id = reference_to_id(ref, ignore_attributes=True)
214-
try:
215-
return env.context.lookup(parent_id)
216-
except LookupError:
217-
# Don't add error here, because we already added error for the
218-
# actual thing we were looking for.
219-
pass
220-
221-
return FluentNoneResolver(ref_id)
220+
return super(TermReference, self).__call__(env)
222221

223222

224223
class VariableReference(FTL.VariableReference, BaseResolver):
@@ -230,7 +229,7 @@ def __call__(self, env):
230229
if env.current.error_for_missing_arg:
231230
env.errors.append(
232231
FluentReferenceError("Unknown external: {0}".format(name)))
233-
return FluentNoneResolver(name)
232+
return FluentNone(name)
234233

235234
if isinstance(arg_val, (FluentType, six.text_type)):
236235
return arg_val
@@ -312,7 +311,7 @@ def __call__(self, env):
312311
return function(*args, **kwargs)
313312
except Exception as e:
314313
env.errors.append(e)
315-
return FluentNoneResolver(function_name + "()")
314+
return FluentNone(function_name + "()")
316315

317316

318317
class NamedArgument(FTL.NamedArgument, BaseResolver):

fluent.runtime/fluent/runtime/utils.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import date, datetime
44
from decimal import Decimal
55

6-
from fluent.syntax.ast import Term, TermReference
6+
from fluent.syntax.ast import TermReference
77

88
from .types import FluentInt, FluentFloat, FluentDecimal, FluentDate, FluentDateTime
99
from .errors import FluentReferenceError
@@ -12,15 +12,6 @@
1212
ATTRIBUTE_SEPARATOR = '.'
1313

1414

15-
def ast_to_id(ast):
16-
"""
17-
Returns a string reference for a Term or Message
18-
"""
19-
if isinstance(ast, Term):
20-
return TERM_SIGIL + ast.id.name
21-
return ast.id.name
22-
23-
2415
def native_to_fluent(val):
2516
"""
2617
Convert a python type to a Fluent Type.
@@ -39,7 +30,7 @@ def native_to_fluent(val):
3930
return val
4031

4132

42-
def reference_to_id(ref, ignore_attributes=False):
33+
def reference_to_id(ref):
4334
"""
4435
Returns a string reference for a MessageReference or TermReference
4536
AST node.
@@ -55,7 +46,7 @@ def reference_to_id(ref, ignore_attributes=False):
5546
else:
5647
start = ref.id.name
5748

58-
if not ignore_attributes and ref.attribute:
49+
if ref.attribute:
5950
return ''.join([start, ATTRIBUTE_SEPARATOR, ref.attribute.name])
6051
return start
6152

0 commit comments

Comments
 (0)