messageformat2 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {messageformat2-1.0.0 → messageformat2-1.1.0}/PKG-INFO +42 -17
- {messageformat2-1.0.0 → messageformat2-1.1.0}/README.md +41 -16
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/builtins.md +70 -8
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/explanation.md +33 -9
- messageformat2-1.1.0/docs/getting_started.md +85 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/datamodel.md +1 -1
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/errors.md +2 -2
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/formatter.md +22 -1
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/selector.md +18 -14
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/index.md +9 -6
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/__init__.py +1 -1
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/builtins.py +396 -58
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/message.py +20 -4
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/runtime.py +136 -67
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/README.md +1 -2
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_builtins.py +181 -3
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_runtime.py +95 -1
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_spec.py +10 -9
- messageformat2-1.0.0/docs/getting_started.md +0 -59
- messageformat2-1.0.0/docs/mf2.md +0 -13
- {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/dependabot.yml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/workflows/ci.yml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/workflows/release.yml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/.gitignore +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/.readthedocs.yaml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/CONTRIBUTING.md +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/LICENSE +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/datamodel.md +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/errors.md +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/message.md +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/cli.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/datamodel.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/errors.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/parser.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/mkdocs.yml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/pyproject.toml +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/__init__.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_1.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_2.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_3.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_1.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_10.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_11.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_12.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_13.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_14.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_15.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_16.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_17.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_2.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_3.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_4.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_5.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_6.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_7.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_8.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_9.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_1.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_2.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_3.txt +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/bidi.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/data-model-errors.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/fallback.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/currency.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/date.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/datetime.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/integer.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/number.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/offset.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/percent.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/string.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/time.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/pattern-selection.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/syntax-errors.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/syntax.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/u-options.json +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_parser.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_regex.py +0 -0
- {messageformat2-1.0.0 → messageformat2-1.1.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: messageformat2
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python implementation of Unicode MessageFormat 2.0
|
|
5
5
|
Project-URL: Homepage, https://github.com/tomasr8/messageformat2
|
|
6
6
|
Project-URL: Github, https://github.com/tomasr8/messageformat2
|
|
@@ -46,11 +46,11 @@ Description-Content-Type: text/markdown
|
|
|
46
46
|
|
|
47
47
|
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
48
48
|
|
|
49
|
-
This is a Python implementation of the [Unicode
|
|
50
|
-
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
49
|
+
This is a Python implementation of the [Unicode MessageFormat
|
|
50
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
more expressive and flexible than both
|
|
52
|
+
Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
|
|
53
|
+
more expressive and flexible than both MessageFormat 1.0 and, in the context of
|
|
54
54
|
Python, gettext.
|
|
55
55
|
|
|
56
56
|
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
@@ -60,16 +60,17 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
|
60
60
|
|
|
61
61
|
The library includes:
|
|
62
62
|
|
|
63
|
-
- A message parser and a formatter
|
|
64
|
-
-
|
|
63
|
+
- A message parser and a formatter, passing the official
|
|
64
|
+
[test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
|
|
65
|
+
- All default functions described by the
|
|
65
66
|
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
67
|
+
(`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
|
|
66
68
|
and the possibility to write custom formatters/selectors
|
|
69
|
+
- Formatting with fallback values and error reporting, optional bidi isolation of placeholders
|
|
67
70
|
- Helper classes to inspect and transform the message [data
|
|
68
71
|
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
69
72
|
|
|
70
|
-
## Documentation
|
|
71
|
-
|
|
72
|
-
Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
|
|
73
|
+
## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
|
|
73
74
|
|
|
74
75
|
## Installation
|
|
75
76
|
|
|
@@ -104,15 +105,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
|
104
105
|
There are several builtin locale-aware formatters available.
|
|
105
106
|
|
|
106
107
|
```python
|
|
107
|
-
from datetime import
|
|
108
|
+
from datetime import date
|
|
108
109
|
from messageformat2 import Message
|
|
109
110
|
|
|
110
|
-
message = Message("Today's date is {$
|
|
111
|
-
|
|
112
|
-
message.format({"
|
|
113
|
-
# -> Today's date is
|
|
114
|
-
message.format({"
|
|
115
|
-
# -> Today's date is 10
|
|
111
|
+
message = Message("Today's date is {$today :date}")
|
|
112
|
+
today = date(2024, 5, 10)
|
|
113
|
+
message.format({"today": today}, locale="en_US")
|
|
114
|
+
# -> "Today's date is May 10, 2024"
|
|
115
|
+
message.format({"today": today}, locale="en_GB")
|
|
116
|
+
# -> "Today's date is 10 May 2024"
|
|
117
|
+
|
|
118
|
+
Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
|
|
119
|
+
# -> "42,00\xa0€"
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
### Plural support
|
|
@@ -133,4 +137,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
|
133
137
|
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
134
138
|
```
|
|
135
139
|
|
|
140
|
+
### Error handling
|
|
141
|
+
|
|
142
|
+
By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
|
|
143
|
+
missing variable, etc.) and collect the errors, pass an `on_error` callback:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
errors = []
|
|
147
|
+
Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
|
|
148
|
+
errors # -> [UnresolvedVariable('Unresolved variable: name')]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Bidirectional text
|
|
152
|
+
|
|
153
|
+
The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
|
|
154
|
+
text displays correctly) is available with `bidi_isolation=True`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
|
|
158
|
+
# -> "Hello, \u2068Alice\u2069!"
|
|
159
|
+
```
|
|
160
|
+
|
|
136
161
|
Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
4
4
|
|
|
5
|
-
This is a Python implementation of the [Unicode
|
|
6
|
-
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
5
|
+
This is a Python implementation of the [Unicode MessageFormat
|
|
6
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
more expressive and flexible than both
|
|
8
|
+
Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
|
|
9
|
+
more expressive and flexible than both MessageFormat 1.0 and, in the context of
|
|
10
10
|
Python, gettext.
|
|
11
11
|
|
|
12
12
|
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
@@ -16,16 +16,17 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
|
16
16
|
|
|
17
17
|
The library includes:
|
|
18
18
|
|
|
19
|
-
- A message parser and a formatter
|
|
20
|
-
-
|
|
19
|
+
- A message parser and a formatter, passing the official
|
|
20
|
+
[test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
|
|
21
|
+
- All default functions described by the
|
|
21
22
|
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
23
|
+
(`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
|
|
22
24
|
and the possibility to write custom formatters/selectors
|
|
25
|
+
- Formatting with fallback values and error reporting, optional bidi isolation of placeholders
|
|
23
26
|
- Helper classes to inspect and transform the message [data
|
|
24
27
|
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
25
28
|
|
|
26
|
-
## Documentation
|
|
27
|
-
|
|
28
|
-
Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
|
|
29
|
+
## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
|
|
@@ -60,15 +61,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
|
60
61
|
There are several builtin locale-aware formatters available.
|
|
61
62
|
|
|
62
63
|
```python
|
|
63
|
-
from datetime import
|
|
64
|
+
from datetime import date
|
|
64
65
|
from messageformat2 import Message
|
|
65
66
|
|
|
66
|
-
message = Message("Today's date is {$
|
|
67
|
-
|
|
68
|
-
message.format({"
|
|
69
|
-
# -> Today's date is
|
|
70
|
-
message.format({"
|
|
71
|
-
# -> Today's date is 10
|
|
67
|
+
message = Message("Today's date is {$today :date}")
|
|
68
|
+
today = date(2024, 5, 10)
|
|
69
|
+
message.format({"today": today}, locale="en_US")
|
|
70
|
+
# -> "Today's date is May 10, 2024"
|
|
71
|
+
message.format({"today": today}, locale="en_GB")
|
|
72
|
+
# -> "Today's date is 10 May 2024"
|
|
73
|
+
|
|
74
|
+
Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
|
|
75
|
+
# -> "42,00\xa0€"
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
### Plural support
|
|
@@ -89,4 +93,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
|
89
93
|
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
90
94
|
```
|
|
91
95
|
|
|
96
|
+
### Error handling
|
|
97
|
+
|
|
98
|
+
By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
|
|
99
|
+
missing variable, etc.) and collect the errors, pass an `on_error` callback:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
errors = []
|
|
103
|
+
Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
|
|
104
|
+
errors # -> [UnresolvedVariable('Unresolved variable: name')]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Bidirectional text
|
|
108
|
+
|
|
109
|
+
The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
|
|
110
|
+
text displays correctly) is available with `bidi_isolation=True`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
|
|
114
|
+
# -> "Hello, \u2068Alice\u2069!"
|
|
115
|
+
```
|
|
116
|
+
|
|
92
117
|
Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
|
|
@@ -14,6 +14,8 @@ As a selector, this function selects the exact match among the variant keys
|
|
|
14
14
|
(after NFC normalization of the value).
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
|
+
from messageformat2 import format_message
|
|
18
|
+
|
|
17
19
|
format_message("""\
|
|
18
20
|
.input {$pet :string}
|
|
19
21
|
.match $pet
|
|
@@ -28,8 +30,8 @@ cat {{I have a cat}}
|
|
|
28
30
|
|
|
29
31
|
The operand must be an `int`, `float` or `Decimal` (but not a `bool`), or a string matching the
|
|
30
32
|
`number-literal` production of the spec (e.g. `|-1234.5|`, `|1e3|`; `|01|` or `|.5|` are not numbers).
|
|
31
|
-
The resolved value of another numeric function (`:number`, `:integer`, `:offset`) is also accepted,
|
|
32
|
-
case its options are inherited, with the options of the expression taking priority.
|
|
33
|
+
The resolved value of another numeric function (`:number`, `:integer`, `:offset`, `:percent`) is also accepted,
|
|
34
|
+
in which case its options are inherited, with the options of the expression taking priority.
|
|
33
35
|
|
|
34
36
|
#### Formatter
|
|
35
37
|
|
|
@@ -43,16 +45,34 @@ case its options are inherited, with the options of the expression taking priori
|
|
|
43
45
|
- auto (default)
|
|
44
46
|
- always
|
|
45
47
|
- never
|
|
46
|
-
- min2 (
|
|
48
|
+
- min2 (grouping separators only for numbers with at least 5 integer digits)
|
|
47
49
|
- `minimumIntegerDigits`
|
|
48
50
|
- `minimumFractionDigits`
|
|
49
51
|
- `maximumFractionDigits`
|
|
50
52
|
- `minimumSignificantDigits`
|
|
51
53
|
- `maximumSignificantDigits`
|
|
52
|
-
- `
|
|
54
|
+
- `roundingMode`
|
|
55
|
+
- ceil
|
|
56
|
+
- floor
|
|
57
|
+
- expand
|
|
58
|
+
- trunc
|
|
59
|
+
- halfCeil
|
|
60
|
+
- halfFloor
|
|
61
|
+
- halfExpand (default; ties away from zero, like `Intl.NumberFormat` and unlike Python's `round()`)
|
|
62
|
+
- halfTrunc
|
|
63
|
+
- halfEven
|
|
64
|
+
- `roundingPriority`: which of the fraction and significant digits options wins when both are given
|
|
65
|
+
- auto (default; the significant digits options)
|
|
66
|
+
- morePrecision
|
|
67
|
+
- lessPrecision
|
|
68
|
+
- `roundingIncrement`: 1 (default), 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500 or 5000,
|
|
69
|
+
in units of the last fraction digit; requires `minimumFractionDigits` and `maximumFractionDigits` to be equal
|
|
70
|
+
- `trailingZeroDisplay`
|
|
71
|
+
- auto (default)
|
|
72
|
+
- stripIfInteger (no fraction digits when the rounded value is an integer)
|
|
53
73
|
|
|
54
74
|
The digit options are *digit size options*: small non-negative integers (`0`-`99`).
|
|
55
|
-
|
|
75
|
+
The numbering system is the default one of the locale (e.g. `Locale("ar", "EG")` uses Arabic-Indic digits).
|
|
56
76
|
|
|
57
77
|
#### Selector
|
|
58
78
|
|
|
@@ -69,14 +89,15 @@ numeric keys (e.g. `1`), then (unless `select=exact`) matches the CLDR plural or
|
|
|
69
89
|
|
|
70
90
|
### `:integer`
|
|
71
91
|
|
|
72
|
-
Same as `:number`, but the operand is
|
|
73
|
-
and `minimumSignificantDigits` options of an annotated operand
|
|
92
|
+
Same as `:number`, but the operand is rounded to an integer (`halfExpand`: `1.5` becomes `2`, `-2.5` becomes `-3`).
|
|
93
|
+
The `minimumFractionDigits`, `maximumFractionDigits` and `minimumSignificantDigits` options of an annotated operand
|
|
94
|
+
are discarded.
|
|
74
95
|
|
|
75
96
|
### `:offset`
|
|
76
97
|
|
|
77
98
|
Adds or subtracts a small integer from a numeric operand, e.g. to count "other" users:
|
|
78
99
|
|
|
79
|
-
```
|
|
100
|
+
```
|
|
80
101
|
.input {$likes :integer}
|
|
81
102
|
.local $others = {$likes :offset subtract=1}
|
|
82
103
|
```
|
|
@@ -89,6 +110,37 @@ Exactly one of the options must be given:
|
|
|
89
110
|
The formatting options of an annotated operand (e.g. `signDisplay` of `{$likes :integer signDisplay=always}`)
|
|
90
111
|
are inherited. `:offset` can also be used as a selector, in which case it behaves like `:number`.
|
|
91
112
|
|
|
113
|
+
### `:percent`
|
|
114
|
+
|
|
115
|
+
Formats a numeric operand multiplied by 100 as a percentage, e.g. `{0.25 :percent}` → `25%`.
|
|
116
|
+
It takes the same options as `:number` except `select`, `minimumIntegerDigits` and `roundingIncrement`
|
|
117
|
+
(by default no fraction digits are shown). As a selector it always uses the plural rules, on the value
|
|
118
|
+
multiplied by 100. The resolved value of a `:percent` expression keeps the original (unmultiplied) value,
|
|
119
|
+
so `.local $p = {0.25 :percent} {{{$p :number}}}` gives `0.25`.
|
|
120
|
+
|
|
121
|
+
### `:currency`
|
|
122
|
+
|
|
123
|
+
Formats a currency amount, e.g. `{42 :currency currency=EUR}` → `€42.00`. The operand is either a numeric
|
|
124
|
+
operand together with the `currency` option, or a currency value: a `messageformat2.builtins.Currency(value, currency)`
|
|
125
|
+
(or any mapping with the keys `value` and `currency`) passed as an input. The currency of a currency value cannot
|
|
126
|
+
be overridden by the `currency` option (`BadOption`). `:currency` does not support selection.
|
|
127
|
+
|
|
128
|
+
- `currency`: a three-letter currency code (case-insensitive); required for numeric operands
|
|
129
|
+
- `currencySign`
|
|
130
|
+
- standard (default)
|
|
131
|
+
- accounting (e.g. `($42.50)`)
|
|
132
|
+
- `currencyDisplay`
|
|
133
|
+
- symbol (default)
|
|
134
|
+
- narrowSymbol (treated as symbol)
|
|
135
|
+
- code (`USD42.00`)
|
|
136
|
+
- name (`42.00 US dollars`)
|
|
137
|
+
- never (`42.00`)
|
|
138
|
+
- `useGrouping`, `minimumIntegerDigits`, `minimumSignificantDigits`, `maximumSignificantDigits`, `roundingMode`,
|
|
139
|
+
`roundingPriority`, `roundingIncrement`, `trailingZeroDisplay`: as for `:number`
|
|
140
|
+
- `fractionDigits`
|
|
141
|
+
- auto (default): the number of fraction digits of the currency (2 for USD, 0 for JPY, ...)
|
|
142
|
+
- a *digit size option*: a fixed number of fraction digits
|
|
143
|
+
|
|
92
144
|
### `:datetime`, `:date`, `:time`
|
|
93
145
|
|
|
94
146
|
The operand must be a `datetime` (or a `date`/`time` for `:date`/`:time` respectively) or an ISO 8601 date or
|
|
@@ -140,3 +192,13 @@ The option values below must be set by literals (an option set by a variable is
|
|
|
140
192
|
#### Selector
|
|
141
193
|
|
|
142
194
|
The date/time functions do not support selection, as it is not required by MF2.
|
|
195
|
+
|
|
196
|
+
## The `u:` namespace options
|
|
197
|
+
|
|
198
|
+
These options can be used with any function (including custom ones):
|
|
199
|
+
|
|
200
|
+
- `u:dir`: `ltr`, `rtl`, `auto` or `inherit` (default). Sets the directionality of the placeholder for the
|
|
201
|
+
bidi isolation strategy and forces its isolation (except `inherit`). Not allowed on markup.
|
|
202
|
+
- `u:id`: an identifier for the placeholder; accepted (it must be a string) but ignored when formatting to a string.
|
|
203
|
+
|
|
204
|
+
Both are removed from the options before the function is called.
|
|
@@ -5,18 +5,20 @@ If the same variable is referenced more than once, it will also be evaluated mor
|
|
|
5
5
|
Pay attention when providing custom formatters/selectors with side effects:
|
|
6
6
|
|
|
7
7
|
```python
|
|
8
|
+
from messageformat2 import Message
|
|
9
|
+
|
|
8
10
|
counter = 0
|
|
9
11
|
|
|
10
12
|
def formatter_with_side_effect(value, locale, options):
|
|
11
|
-
|
|
12
|
-
counter +=1
|
|
13
|
+
global counter
|
|
14
|
+
counter += 1
|
|
13
15
|
return str(value)
|
|
14
16
|
|
|
15
|
-
msg = Message("\
|
|
17
|
+
msg = Message("""\
|
|
16
18
|
.local $foo = {|foo| :side_effect}
|
|
17
19
|
{{{$foo} {$foo}}}
|
|
18
|
-
")
|
|
19
|
-
msg.format(formatters={
|
|
20
|
+
""")
|
|
21
|
+
msg.format(formatters={"side_effect": formatter_with_side_effect})
|
|
20
22
|
print(counter) # -> 2
|
|
21
23
|
```
|
|
22
24
|
|
|
@@ -27,6 +29,25 @@ numbers (`int`, `float`, `Decimal`) as if annotated with `:number`, `datetime`,
|
|
|
27
29
|
`:datetime`, `:date` and `:time`. Anything else is formatted with `str()`.
|
|
28
30
|
Literals are always strings, so `{|1234.5|}` is formatted verbatim.
|
|
29
31
|
|
|
32
|
+
## Bidirectional text
|
|
33
|
+
|
|
34
|
+
With `bidi_isolation=True`, placeholders are wrapped in Unicode isolate controls following the spec's
|
|
35
|
+
*Default Bidi Strategy*, so that mixed left-to-right and right-to-left text displays correctly: a value whose
|
|
36
|
+
direction is unknown (strings, literals, the results of custom formatters) is wrapped in U+2068 FIRST STRONG
|
|
37
|
+
ISOLATE … U+2069 POP DIRECTIONAL ISOLATE, a value whose direction differs from the message's (e.g. a number,
|
|
38
|
+
which has the direction of the locale, in a message formatted in an RTL locale) in U+2066 LEFT-TO-RIGHT ISOLATE /
|
|
39
|
+
U+2067 RIGHT-TO-LEFT ISOLATE.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True) # -> "Hello, \u2068Alice\u2069!"
|
|
43
|
+
Message("{$n :number} items").format({"n": 3}, bidi_isolation=True) # -> "3 items" (same direction)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The controls are invisible but do change the string, which is why isolation is off by default.
|
|
47
|
+
The `u:dir` option sets the direction of a placeholder (`ltr`, `rtl` or `auto`) and forces its isolation:
|
|
48
|
+
`{$name :string u:dir=rtl}`. Custom formatters can return a `messageformat2.builtins.FormattedValue(text, direction)`
|
|
49
|
+
to declare the direction of their output.
|
|
50
|
+
|
|
30
51
|
## Function composition
|
|
31
52
|
|
|
32
53
|
A variable which was annotated with a function can be annotated again, for example in a placeholder.
|
|
@@ -36,11 +57,12 @@ and decides what to do with it. The built-in numeric functions accept each other
|
|
|
36
57
|
and inherit their options, with the options of the later expression taking priority:
|
|
37
58
|
|
|
38
59
|
```python
|
|
39
|
-
|
|
40
|
-
{{x = {$x :number minimumFractionDigits=1}}} # -> "x = +42.0"
|
|
60
|
+
from messageformat2 import format_message
|
|
41
61
|
|
|
42
|
-
.local $x = {
|
|
43
|
-
|
|
62
|
+
format_message(".local $x = {42 :number minimumFractionDigits=2 signDisplay=always} {{x = {$x :number minimumFractionDigits=1}}}")
|
|
63
|
+
# -> "x = +42.0"
|
|
64
|
+
format_message(".local $x = {1.25 :integer} {{x = {$x :number}}}")
|
|
65
|
+
# -> "x = 1"
|
|
44
66
|
```
|
|
45
67
|
|
|
46
68
|
whereas e.g. `{$x :date}` would raise `BadOperand`. See [Custom formatters](howto/formatter.md) for how to
|
|
@@ -54,6 +76,8 @@ Whitespace around a complex message (a message containing declarations and/or a
|
|
|
54
76
|
so complex messages can be freely indented, e.g. in multiline strings:
|
|
55
77
|
|
|
56
78
|
```python
|
|
79
|
+
from messageformat2 import Message
|
|
80
|
+
|
|
57
81
|
Message("""
|
|
58
82
|
.input {$count :integer}
|
|
59
83
|
.match $count
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
## Installation
|
|
2
|
+
|
|
3
|
+
This package is available via `pip` for Python `>= 3.12`:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install messageformat2
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Simple messages
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from messageformat2 import format_message
|
|
13
|
+
|
|
14
|
+
format_message("Hello, {$name}!", {"name": "Alice"}) # -> "Hello, Alice!"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Messages can be reused with different inputs.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from messageformat2 import Message
|
|
21
|
+
|
|
22
|
+
message = Message("Hello, {$name}!")
|
|
23
|
+
message.format({"name": "Alice"}) # -> "Hello, Alice!"
|
|
24
|
+
message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Built-in formatters
|
|
28
|
+
|
|
29
|
+
There are several builtin locale-aware [formatters](builtins.md) available.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from datetime import date
|
|
33
|
+
from messageformat2 import Message
|
|
34
|
+
|
|
35
|
+
message = Message("Today's date is {$today :date}")
|
|
36
|
+
today = date(2024, 5, 10)
|
|
37
|
+
message.format({"today": today}, locale="en_US")
|
|
38
|
+
# -> "Today's date is May 10, 2024"
|
|
39
|
+
message.format({"today": today}, locale="en_GB")
|
|
40
|
+
# -> "Today's date is 10 May 2024"
|
|
41
|
+
|
|
42
|
+
Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
|
|
43
|
+
# -> "42,00\xa0€"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Plural support
|
|
47
|
+
|
|
48
|
+
MF2 supports pluralization using built-in or custom selectors.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from messageformat2 import Message
|
|
52
|
+
|
|
53
|
+
msg = Message ("""\
|
|
54
|
+
.input {$count :number}
|
|
55
|
+
.match $count
|
|
56
|
+
one {{You have {$count} notification.}}
|
|
57
|
+
* {{You have {$count} notifications.}}
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
61
|
+
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Error handling
|
|
65
|
+
|
|
66
|
+
By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
|
|
67
|
+
missing variable, etc.) and collect the errors, pass an `on_error` callback,
|
|
68
|
+
see [Error handling](howto/errors.md):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
errors = []
|
|
72
|
+
Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
|
|
73
|
+
errors # -> [UnresolvedVariable('Unresolved variable: name')]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Bidirectional text
|
|
77
|
+
|
|
78
|
+
The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
|
|
79
|
+
text displays correctly) is available with `bidi_isolation=True`,
|
|
80
|
+
see [Bidirectional text](explanation.md#bidirectional-text):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
|
|
84
|
+
# -> "Hello, \u2068Alice\u2069!"
|
|
85
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
The `Message` class has a `datamodel` property which gives you access to the
|
|
2
2
|
message data model which is based on the [MF2
|
|
3
3
|
spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md).
|
|
4
|
-
The data model nodes are documented [here](
|
|
4
|
+
The data model nodes are documented [here](../datamodel.md).
|
|
5
5
|
|
|
6
6
|
There are two classes to work with the datamodel - `DataModelVisitor` &
|
|
7
7
|
`DataModelTransformer`.
|
|
@@ -22,7 +22,7 @@ from messageformat2.errors import ParseError, DataModelError
|
|
|
22
22
|
try:
|
|
23
23
|
Message("}} invalid syntax")
|
|
24
24
|
except ParseError as e:
|
|
25
|
-
print(e) # -> Invalid
|
|
25
|
+
print(e) # -> Invalid character at the start of a simple message: }
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
try:
|
|
@@ -45,7 +45,7 @@ msg = Message("Hello, {$name}!")
|
|
|
45
45
|
try:
|
|
46
46
|
msg.format()
|
|
47
47
|
except FormatError as e:
|
|
48
|
-
print(e) # -> Unresolved variable
|
|
48
|
+
print(e) # -> Unresolved variable: name
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
## Formatting with fallback values
|
|
@@ -2,6 +2,9 @@ It is possible to write a custom formatter to extend the functionality of the
|
|
|
2
2
|
library. Here's an example of a simple formatter:
|
|
3
3
|
|
|
4
4
|
```python
|
|
5
|
+
from typing import Any
|
|
6
|
+
from messageformat2 import format_message
|
|
7
|
+
|
|
5
8
|
def capitalize(value: Any, locale, options) -> str:
|
|
6
9
|
return value.capitalize()
|
|
7
10
|
|
|
@@ -41,13 +44,17 @@ and `messageformat2.errors.MessageFunctionError` will be reraised instead.
|
|
|
41
44
|
```python
|
|
42
45
|
from messageformat2.errors import MessageFunctionError
|
|
43
46
|
|
|
47
|
+
def raise_some_error(value, locale, options) -> str:
|
|
48
|
+
raise ValueError("something went wrong")
|
|
49
|
+
|
|
44
50
|
try:
|
|
45
51
|
format_message(
|
|
46
52
|
"{foo :raise}",
|
|
47
53
|
formatters={"raise": raise_some_error}
|
|
48
54
|
)
|
|
49
55
|
except MessageFunctionError as e:
|
|
50
|
-
print(e)
|
|
56
|
+
print(e) # -> Exception raised while evaluating formatter :raise
|
|
57
|
+
print(repr(e.__cause__)) # -> ValueError('something went wrong')
|
|
51
58
|
```
|
|
52
59
|
|
|
53
60
|
To signal a specific error, a formatter can raise one of the `MessageFunctionError` subclasses
|
|
@@ -94,3 +101,17 @@ format_message(
|
|
|
94
101
|
)
|
|
95
102
|
# -> "ALICE"
|
|
96
103
|
```
|
|
104
|
+
|
|
105
|
+
## Directionality
|
|
106
|
+
|
|
107
|
+
The result of a custom formatter has an unknown direction and is isolated with U+2068/U+2069 when the message
|
|
108
|
+
is formatted with `bidi_isolation=True` (see [Bidirectional text](../explanation.md#bidirectional-text)).
|
|
109
|
+
Return a `FormattedValue` to declare the direction instead, so that a value in the direction of the message
|
|
110
|
+
is not isolated:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from messageformat2.builtins import FormattedValue
|
|
114
|
+
|
|
115
|
+
def shout(value, locale, options) -> FormattedValue:
|
|
116
|
+
return FormattedValue(str(value).upper(), locale.text_direction)
|
|
117
|
+
```
|
|
@@ -4,23 +4,27 @@ statement.
|
|
|
4
4
|
Here's a simple implementation of the `:integer` selector:
|
|
5
5
|
|
|
6
6
|
```python
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from babel import Locale
|
|
10
|
+
|
|
11
|
+
from messageformat2 import format_message
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
def integer_selector(
|
|
8
15
|
value: Any, # The value passed to the selector while formatting the message
|
|
9
|
-
*,
|
|
10
16
|
locale: Locale, # Babel locale
|
|
11
17
|
options: dict[str, Any], # Selector options
|
|
12
|
-
keys: list[str] # Variant keys to be filtered out
|
|
18
|
+
keys: list[str], # Variant keys to be filtered out
|
|
13
19
|
) -> list[str]:
|
|
20
|
+
value = int(value)
|
|
14
21
|
select = options.get("select", "plural")
|
|
15
22
|
|
|
16
|
-
exact_keys = [
|
|
23
|
+
exact_keys = [key for key in keys if key == str(value)]
|
|
17
24
|
if select == "exact":
|
|
18
25
|
return exact_keys
|
|
19
|
-
if select == "
|
|
20
|
-
|
|
21
|
-
if select == "ordinal":
|
|
22
|
-
form = locale.ordinal_form(value)
|
|
23
|
-
return exact_keys + keyword_keys
|
|
26
|
+
form = locale.ordinal_form(value) if select == "ordinal" else locale.plural_form(value)
|
|
27
|
+
return exact_keys + [key for key in keys if key == form]
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
format_message("""\
|
|
@@ -32,19 +36,19 @@ one {{You have {$count} notification.}}
|
|
|
32
36
|
{"count": 1},
|
|
33
37
|
selectors={"integer": integer_selector}
|
|
34
38
|
)
|
|
35
|
-
# -> You have one notification.
|
|
39
|
+
# -> "You have one notification."
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
A selector function must take exactly
|
|
42
|
+
A selector function must take exactly 4 arguments, which are passed by keyword (so they must have these names):
|
|
39
43
|
|
|
40
|
-
- `value`: the value passed to the
|
|
44
|
+
- `value`: the value passed to the selector when formatting
|
|
41
45
|
- `locale`: the current locale as a `babel.Locale` instance
|
|
42
|
-
- `options`: a dictionary containing all the options passed to the
|
|
43
|
-
- `keys`:
|
|
46
|
+
- `options`: a dictionary containing all the options passed to the selector
|
|
47
|
+
- `keys`: the key from each variant of the match statement (the catch-all `*` is not included)
|
|
44
48
|
|
|
45
49
|
For example, for this message:
|
|
46
50
|
|
|
47
|
-
```
|
|
51
|
+
```
|
|
48
52
|
.input {$count :integer select=plural}
|
|
49
53
|
.match $count
|
|
50
54
|
1 {{You have one notification.}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
This site contains documentation for the `messageformat2`, a Python library
|
|
2
|
-
which implements the [Unicode
|
|
3
|
-
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
2
|
+
which implements the [Unicode MessageFormat
|
|
3
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
more expressive and flexible than both
|
|
5
|
+
Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
|
|
6
|
+
more expressive and flexible than both MessageFormat 1.0 and, in the context of
|
|
7
7
|
Python, gettext.
|
|
8
8
|
|
|
9
9
|
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
@@ -13,10 +13,13 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
|
13
13
|
|
|
14
14
|
The `messageformat2` library includes:
|
|
15
15
|
|
|
16
|
-
- A message parser and a formatter
|
|
17
|
-
-
|
|
16
|
+
- A message parser and a formatter, passing the official
|
|
17
|
+
[test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
|
|
18
|
+
- All default functions described by the
|
|
18
19
|
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
20
|
+
(`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
|
|
19
21
|
and the possibility to write custom formatters/selectors
|
|
22
|
+
- Formatting with fallback values and error reporting, optional bidi isolation of placeholders
|
|
20
23
|
- Helper classes to inspect and transform the message [data
|
|
21
24
|
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
22
25
|
|