@@ -30,6 +30,251 @@ you're a tool author you may be interested in the formal [EBNF grammar][].
3030[ EBNF grammar ] : https://github.com/projectfluent/fluent/tree/master/spec
3131
3232
33+ Installation
34+ ------------
35+
36+ python-fluent consists of two packages:
37+
38+ * ` fluent.syntax ` - includes AST classes and parser. Most end users will not
39+ need this directly. Documentation coming soon!
40+
41+ To install:
42+
43+ pip install fluent.syntax
44+
45+
46+ * ` fluent.runtime ` - methods for generating translations from FTL files.
47+ Documentation below.
48+
49+ To install:
50+
51+ pip install fluent.runtime
52+
53+ (The correct version of `` fluent.syntax `` will be installed automatically)
54+
55+
56+ PyPI also contains an old ` fluent ` package which is an older version of just
57+ ` fluent.syntax ` .
58+
59+ Usage
60+ -----
61+
62+ To generate translations using the `` fluent.runtime `` package, you start with
63+ the ` FluentBundle ` class:
64+
65+ >>> from fluent.runtime import FluentBundle
66+
67+ You pass a list of locales to the constructor - the first being the desired
68+ locale, with fallbacks after that:
69+
70+ >>> bundle = FluentBundle(["en-US"])
71+
72+
73+ You must then add messages. These would normally come from a ` .ftl ` file stored
74+ on disk, here we will just add them directly:
75+
76+ >>> bundle.add_messages("""
77+ ... welcome = Welcome to this great app!
78+ ... greet-by-name = Hello, { $name }!
79+ ... """)
80+
81+ To generate translations, use the ` format ` method, passing a message ID and an
82+ optional dictionary of substitution parameters. If the the message ID is not
83+ found, a ` LookupError ` is raised. Otherwise, as per the Fluent philosophy, the
84+ implementation tries hard to recover from any formatting errors and generate the
85+ most human readable representation of the value. The ` format ` method therefore
86+ returns a tuple containing ` (translated string, errors) ` , as below.
87+
88+ >>> translated, errs = bundle.format('welcome')
89+ >>> translated
90+ "Welcome to this great app!"
91+ >>> errs
92+ []
93+
94+ >>> translated, errs = bundle.format('greet-by-name', {'name': 'Jane'})
95+ >>> translated
96+ 'Hello, \u2068Jane\u2069!'
97+
98+ >>> translated, errs = bundle.format('greet-by-name', {})
99+ >>> translated
100+ 'Hello, \u2068name\u2069!'
101+ >>> errs
102+ [FluentReferenceError('Unknown external: name')]
103+
104+ You will notice the extra characters ` \u2068 ` and ` \u2069 ` in the output. These
105+ are Unicode bidi isolation characters that help to ensure that the interpolated
106+ strings are handled correctly in the situation where the text direction of the
107+ substitution might not match the text direction of the localized text. These
108+ characters can be disabled if you are sure that is not possible for your app by
109+ passing ` use_isolating=False ` to the ` FluentBundle ` constructor.
110+
111+ Python 2
112+ --------
113+
114+ The above examples assume Python 3. Since Fluent uses unicode everywhere
115+ internally (and doesn't accept bytestrings), if you are using Python 2 you will
116+ need to make adjustments to the above example code. Either add ` u ` unicode
117+ literal markers to strings or add this at the top of the module or the start of
118+ your repl session:
119+
120+ from __future__ import unicode_literals
121+
122+
123+ Numbers
124+ -------
125+
126+ When rendering translations, Fluent passes any numeric arguments (int or float)
127+ through locale-aware formatting functions:
128+
129+ >>> bundle.add_messages("show-total-points = You have { $points } points.")
130+ >>> val, errs = bundle.format("show-total-points", {'points': 1234567})
131+ >>> val
132+ 'You have 1,234,567 points.'
133+
134+
135+ You can specify your own formatting options on the arguments passed in by
136+ wrapping your numeric arguments with ` fluent.runtime.types.fluent_number ` :
137+
138+ >>> from fluent.runtime.types import fluent_number
139+ >>> points = fluent_number(1234567, useGrouping=False)
140+ >>> bundle.format("show-total-points", {'points': points})[0]
141+ 'You have 1234567 points.'
142+
143+ >>> amount = fluent_number(1234.56, style="currency", currency="USD")
144+ >>> bundle.add_messages("your-balance = Your balance is { $amount }")
145+ >>> bundle.format("your-balance", {'amount': amount})[0]
146+ 'Your balance is $1,234.56'
147+
148+ Thee options available are defined in the Fluent spec for
149+ [ NUMBER] ( https://projectfluent.org/fluent/guide/functions.html#number ) . Some of
150+ these options can also be defined in the FTL files, as described in the Fluent
151+ spec, and the options will be merged.
152+
153+ Date and time
154+ -------------
155+
156+ Python ` datetime.datetime ` and ` datetime.date ` objects are also passed through
157+ locale aware functions:
158+
159+ >>> from datetime import date
160+ >>> bundle.add_messages("today-is = Today is { $today }")
161+ >>> val, errs = bundle.format("today-is", {"today": date.today() })
162+ >>> val
163+ 'Today is Jun 16, 2018'
164+
165+ You can explicitly call the ` DATETIME ` builtin to specify options:
166+
167+ >>> bundle.add_messages('today-is = Today is { DATETIME($today, dateStyle: "short") }')
168+
169+ See the [ DATETIME
170+ docs] ( https://projectfluent.org/fluent/guide/functions.html#datetime ) . However,
171+ currently the only supported options to ` DATETIME ` are:
172+
173+ * ` timeZone `
174+ * ` dateStyle ` and ` timeStyle ` which are [ proposed
175+ additions] ( https://github.com/tc39/proposal-ecma402-datetime-style ) to the ECMA i18n spec.
176+
177+ To specify options from Python code, use ` fluent.runtime.types.fluent_date ` :
178+
179+ >>> from fluent.runtime.types import fluent_date
180+ >>> today = date.today()
181+ >>> short_today = fluent_date(today, dateStyle='short')
182+ >>> val, errs = bundle.format("today-is", {"today": short_today })
183+ >>> val
184+ 'Today is 6/17/18'
185+
186+ You can also specify timezone for displaying ` datetime ` objects in two ways:
187+
188+ * Create timezone aware ` datetime ` objects, and pass these to the ` format ` call
189+ e.g.:
190+
191+ >>> import pytz
192+ >>> from datetime import datetime
193+ >>> utcnow = datime.utcnow().replace(tzinfo=pytz.utc)
194+ >>> moscow_timezone = pytz.timezone('Europe/Moscow')
195+ >>> now_in_moscow = utcnow.astimezone(moscow_timezone)
196+
197+ * Or, use timezone naive ` datetime ` objects, or ones with a UTC timezone, and
198+ pass the ` timeZone ` argument to ` fluent_date ` as a string:
199+
200+ >>> utcnow = datetime.utcnow()
201+ >>> utcnow
202+ datetime.datetime(2018, 6, 17, 12, 15, 5, 677597)
203+
204+ >>> bundle.add_messages("now-is = Now is { $now }")
205+ >>> val, errs = bundle.format("now-is",
206+ ... {"now": fluent_date(utcnow,
207+ ... timeZone="Europe/Moscow",
208+ ... dateStyle="medium",
209+ ... timeStyle="medium")})
210+ >>> val
211+ 'Now is Jun 17, 2018, 3:15:05 PM'
212+
213+
214+ Custom functions
215+ ----------------
216+
217+ You can add functions to the ones available to FTL authors by passing
218+ a ` functions ` dictionary to the ` FluentBundle ` constructor:
219+
220+
221+ >>> import platform
222+ >>> def os_name():
223+ ... """Returns linux/mac/windows/other"""
224+ ... return {'Linux': 'linux',
225+ ... 'Darwin': 'mac',
226+ ... 'Windows': 'windows'}.get(platform.system(), 'other')
227+
228+ >>> bundle = FluentBundle(['en-US'], functions={'OS': os_name})
229+ >>> bundle.add_messages("""
230+ ... welcome = { OS() ->
231+ ... [linux] Welcome to Linux
232+ ... [mac] Welcome to Mac
233+ ... [windows] Welcome to Windows
234+ ... *[other] Welcome
235+ ... }
236+ ... """)
237+ >>> print(bundle.format('welcome')[0]
238+ Welcome to Linux
239+
240+ These functions can accept positioal and keyword arguments (like the ` NUMBER `
241+ and ` DATETIME ` builtins), and in this case must accept the following types of
242+ arguments:
243+
244+ * unicode strings (i.e. ` unicode ` on Python 2, ` str ` on Python 3)
245+ * ` fluent.runtime.types.FluentType ` subclasses, namely:
246+ * ` FluentNumber ` - ` int ` , ` float ` or ` Decimal ` objects passed in externally,
247+ or expressed as literals, are wrapped in these. Note that these objects also
248+ subclass builtin ` int ` , ` float ` or ` Decimal ` , so can be used as numbers in
249+ the normal way.
250+ * ` FluentDateType ` - ` date ` or ` datetime ` objects passed in are wrapped in
251+ these. Again, these classes also subclass ` date ` or ` datetime ` , and can be
252+ used as such.
253+ * ` FluentNone ` - in error conditions, such as a message referring to an argument
254+ that hasn't been passed in, objects of this type are passed in.
255+
256+ Custom functions should not throw errors, but return ` FluentNone ` instances to
257+ indicate an error or missing data. Otherwise they should return unicode strings,
258+ or instances of a ` FluentType ` subclass as above.
259+
260+
261+ Known limitations and bugs
262+ --------------------------
263+
264+ * We do not yet support ` NUMBER(..., currencyDisplay="name") ` - see [ this python-babel
265+ pull request] ( https://github.com/python-babel/babel/pull/585 ) which needs to
266+ be merged and released.
267+
268+ * Most options to ` DATETIME ` are not yet supported. See the [ MDN docs for
269+ Intl.DateTimeFormat] ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat ) ,
270+ the [ ECMA spec for
271+ BasicFormatMatcher] ( http://www.ecma-international.org/ecma-402/1.0/#BasicFormatMatcher )
272+ and the [ Intl.js
273+ polyfill] ( https://github.com/andyearnshaw/Intl.js/blob/master/src/12.datetimeformat.js ) .
274+
275+ Help with the above would be welcome!
276+
277+
33278Discuss
34279-------
35280
0 commit comments