messageformat2 0.1.1__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-0.1.1 → messageformat2-1.1.0}/.github/workflows/ci.yml +7 -3
- {messageformat2-0.1.1 → messageformat2-1.1.0}/.github/workflows/release.yml +1 -1
- {messageformat2-0.1.1 → messageformat2-1.1.0}/.readthedocs.yaml +9 -7
- {messageformat2-0.1.1 → messageformat2-1.1.0}/PKG-INFO +53 -23
- messageformat2-1.1.0/README.md +117 -0
- messageformat2-1.1.0/docs/builtins.md +204 -0
- messageformat2-1.1.0/docs/explanation.md +95 -0
- messageformat2-1.1.0/docs/getting_started.md +85 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/howto/datamodel.md +2 -2
- messageformat2-1.1.0/docs/howto/errors.md +72 -0
- messageformat2-1.1.0/docs/howto/formatter.md +117 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/howto/selector.md +22 -16
- messageformat2-1.1.0/docs/index.md +30 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/__init__.py +1 -1
- messageformat2-1.1.0/messageformat2/builtins.py +930 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/datamodel.py +70 -105
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/errors.py +51 -22
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/message.py +52 -13
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/parser.py +106 -159
- messageformat2-1.1.0/messageformat2/runtime.py +386 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/pyproject.toml +14 -11
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/complex_message_2.txt +7 -4
- messageformat2-1.1.0/tests/data/complex_message_3.txt +28 -0
- messageformat2-1.1.0/tests/data/simple_message_17.txt +8 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_9.txt +1 -1
- messageformat2-1.1.0/tests/data/simple_message_escapes_3.txt +1 -0
- messageformat2-1.1.0/tests/spec/README.md +17 -0
- messageformat2-1.1.0/tests/spec/bidi.json +147 -0
- messageformat2-1.1.0/tests/spec/data-model-errors.json +189 -0
- messageformat2-1.1.0/tests/spec/fallback.json +62 -0
- messageformat2-1.1.0/tests/spec/functions/currency.json +56 -0
- messageformat2-1.1.0/tests/spec/functions/date.json +45 -0
- messageformat2-1.1.0/tests/spec/functions/datetime.json +64 -0
- messageformat2-1.1.0/tests/spec/functions/integer.json +71 -0
- messageformat2-1.1.0/tests/spec/functions/number.json +334 -0
- messageformat2-1.1.0/tests/spec/functions/offset.json +77 -0
- messageformat2-1.1.0/tests/spec/functions/percent.json +45 -0
- messageformat2-1.1.0/tests/spec/functions/string.json +79 -0
- messageformat2-1.1.0/tests/spec/functions/time.json +42 -0
- messageformat2-1.1.0/tests/spec/pattern-selection.json +120 -0
- messageformat2-1.1.0/tests/spec/syntax-errors.json +246 -0
- messageformat2-1.1.0/tests/spec/syntax.json +763 -0
- messageformat2-1.1.0/tests/spec/u-options.json +104 -0
- messageformat2-1.1.0/tests/test_builtins.py +454 -0
- messageformat2-1.1.0/tests/test_parser.py +412 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/test_regex.py +11 -84
- messageformat2-1.1.0/tests/test_runtime.py +552 -0
- messageformat2-1.1.0/tests/test_spec.py +239 -0
- messageformat2-1.1.0/uv.lock +950 -0
- messageformat2-0.1.1/README.md +0 -89
- messageformat2-0.1.1/docs/builtins.md +0 -172
- messageformat2-0.1.1/docs/explanation.md +0 -71
- messageformat2-0.1.1/docs/getting_started.md +0 -58
- messageformat2-0.1.1/docs/howto/errors.md +0 -48
- messageformat2-0.1.1/docs/howto/formatter.md +0 -51
- messageformat2-0.1.1/docs/index.md +0 -27
- messageformat2-0.1.1/docs/mf2.md +0 -13
- messageformat2-0.1.1/messageformat2/builtins.py +0 -323
- messageformat2-0.1.1/messageformat2/runtime.py +0 -301
- messageformat2-0.1.1/tests/data/complex_message_3.txt +0 -22
- messageformat2-0.1.1/tests/data/unsupported_annotations_1.txt +0 -4
- messageformat2-0.1.1/tests/data/unsupported_annotations_2.txt +0 -4
- messageformat2-0.1.1/tests/data/unsupported_annotations_3.txt +0 -3
- messageformat2-0.1.1/tests/data/unsupported_annotations_4.txt +0 -6
- messageformat2-0.1.1/tests/data/unsupported_annotations_5.txt +0 -8
- messageformat2-0.1.1/tests/data/unsupported_statement_1.txt +0 -15
- messageformat2-0.1.1/tests/data/unsupported_statement_2.txt +0 -21
- messageformat2-0.1.1/tests/test_parser.py +0 -235
- messageformat2-0.1.1/tests/test_runtime.py +0 -242
- messageformat2-0.1.1/uv.lock +0 -776
- {messageformat2-0.1.1 → messageformat2-1.1.0}/.github/dependabot.yml +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/.gitignore +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/CONTRIBUTING.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/LICENSE +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/datamodel.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/errors.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/message.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/cli.py +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/mkdocs.yml +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/__init__.py +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/complex_message_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_10.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_11.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_12.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_13.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_14.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_15.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_16.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_2.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_3.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_4.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_5.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_6.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_7.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_8.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_escapes_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_escapes_2.txt +0 -0
|
@@ -11,10 +11,10 @@ jobs:
|
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
strategy:
|
|
13
13
|
matrix:
|
|
14
|
-
python-version: ["3.12", "3.13", "3.14"]
|
|
14
|
+
python-version: ["3.12", "3.13", "3.14", "3.15"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v7.0.1
|
|
18
18
|
|
|
19
19
|
- name: Install the latest version of uv and set the python version
|
|
20
20
|
uses: astral-sh/setup-uv@v7
|
|
@@ -23,7 +23,11 @@ jobs:
|
|
|
23
23
|
|
|
24
24
|
- name: Lint code 🧹
|
|
25
25
|
run: |
|
|
26
|
-
uv run ruff check tests/ messageformat2/
|
|
26
|
+
uv run ruff check tests/ messageformat2/
|
|
27
|
+
|
|
28
|
+
- name: Format code 📃
|
|
29
|
+
run: |
|
|
30
|
+
uv run ruff format --check --output-format=github messageformat2/ tests/
|
|
27
31
|
|
|
28
32
|
- name: Type check 🏷
|
|
29
33
|
run: |
|
|
@@ -6,14 +6,16 @@ version: 2
|
|
|
6
6
|
|
|
7
7
|
# Set the version of Python and other tools you might need
|
|
8
8
|
build:
|
|
9
|
-
os: ubuntu-
|
|
9
|
+
os: ubuntu-26.04
|
|
10
10
|
tools:
|
|
11
|
-
python: "3.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
python: "3.14"
|
|
12
|
+
|
|
13
|
+
python:
|
|
14
|
+
install:
|
|
15
|
+
- method: uv
|
|
16
|
+
command: sync
|
|
17
|
+
groups:
|
|
18
|
+
- docs
|
|
17
19
|
|
|
18
20
|
mkdocs:
|
|
19
21
|
configuration: mkdocs.yml
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: messageformat2
|
|
3
|
-
Version:
|
|
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
|
|
7
|
+
Project-URL: Documentation, https://messageformat2.readthedocs.io/en/latest/
|
|
7
8
|
Author-email: Tomas Roun <tomas.roun8@gmail.com>
|
|
8
9
|
License: MIT License
|
|
9
10
|
|
|
@@ -28,42 +29,46 @@ License: MIT License
|
|
|
28
29
|
SOFTWARE.
|
|
29
30
|
License-File: LICENSE
|
|
30
31
|
Keywords: cldr,gettext,i18n,internationalization,localization,messageformat,messageformat 2
|
|
31
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
32
32
|
Classifier: Operating System :: OS Independent
|
|
33
33
|
Classifier: Programming Language :: Python :: 3
|
|
34
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
-
Classifier:
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
36
38
|
Classifier: Topic :: Software Development :: Internationalization
|
|
37
39
|
Classifier: Topic :: Software Development :: Localization
|
|
38
40
|
Requires-Python: >=3.12
|
|
39
|
-
Requires-Dist: babel
|
|
40
|
-
Requires-Dist: click
|
|
41
|
+
Requires-Dist: babel>=2
|
|
42
|
+
Requires-Dist: click>=8
|
|
41
43
|
Description-Content-Type: text/markdown
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
46
48
|
|
|
47
|
-
This is a Python implementation of the [Unicode
|
|
48
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
49
|
+
This is a Python implementation of the [Unicode MessageFormat
|
|
50
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
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
|
|
52
54
|
Python, gettext.
|
|
53
55
|
|
|
54
56
|
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
55
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
57
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
56
58
|
|
|
57
59
|
## Features
|
|
58
60
|
|
|
59
61
|
The library includes:
|
|
60
62
|
|
|
61
|
-
- A message parser and a formatter
|
|
62
|
-
-
|
|
63
|
-
|
|
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
|
|
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`)
|
|
64
68
|
and the possibility to write custom formatters/selectors
|
|
69
|
+
- Formatting with fallback values and error reporting, optional bidi isolation of placeholders
|
|
65
70
|
- Helper classes to inspect and transform the message [data
|
|
66
|
-
model](https://
|
|
71
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
67
72
|
|
|
68
73
|
## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
|
|
69
74
|
|
|
@@ -100,15 +105,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
|
100
105
|
There are several builtin locale-aware formatters available.
|
|
101
106
|
|
|
102
107
|
```python
|
|
103
|
-
from datetime import
|
|
108
|
+
from datetime import date
|
|
104
109
|
from messageformat2 import Message
|
|
105
110
|
|
|
106
|
-
message = Message("Today's date is {$
|
|
107
|
-
|
|
108
|
-
message.format({"
|
|
109
|
-
# -> Today's date is
|
|
110
|
-
message.format({"
|
|
111
|
-
# -> 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€"
|
|
112
120
|
```
|
|
113
121
|
|
|
114
122
|
### Plural support
|
|
@@ -119,7 +127,8 @@ MF2 supports pluralization using built-in or custom selectors.
|
|
|
119
127
|
from messageformat2 import Message
|
|
120
128
|
|
|
121
129
|
msg = Message ("""\
|
|
122
|
-
.
|
|
130
|
+
.input {$count :number}
|
|
131
|
+
.match $count
|
|
123
132
|
one {{You have {$count} notification.}}
|
|
124
133
|
* {{You have {$count} notifications.}}
|
|
125
134
|
""")
|
|
@@ -128,4 +137,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
|
128
137
|
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
129
138
|
```
|
|
130
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
|
+
|
|
131
161
|
Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
4
|
+
|
|
5
|
+
This is a Python implementation of the [Unicode MessageFormat
|
|
6
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
|
|
7
|
+
|
|
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
|
+
Python, gettext.
|
|
11
|
+
|
|
12
|
+
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
13
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
The library includes:
|
|
18
|
+
|
|
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
|
|
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`)
|
|
24
|
+
and the possibility to write custom formatters/selectors
|
|
25
|
+
- Formatting with fallback values and error reporting, optional bidi isolation of placeholders
|
|
26
|
+
- Helper classes to inspect and transform the message [data
|
|
27
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
28
|
+
|
|
29
|
+
## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Via pip for Python `>= 3.12`
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install messageformat2
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Simple messages
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from messageformat2 import format_message
|
|
45
|
+
|
|
46
|
+
format_message("Hello, {$name}!", {"name": "Alice"}) # -> "Hello, Alice!"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Messages can be reused with different inputs.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from messageformat2 import Message
|
|
53
|
+
|
|
54
|
+
message = Message("Hello, {$name}!")
|
|
55
|
+
message.format({"name": "Alice"}) # -> "Hello, Alice!"
|
|
56
|
+
message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Built-in formatters
|
|
60
|
+
|
|
61
|
+
There are several builtin locale-aware formatters available.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from datetime import date
|
|
65
|
+
from messageformat2 import Message
|
|
66
|
+
|
|
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€"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Plural support
|
|
79
|
+
|
|
80
|
+
MF2 supports pluralization using built-in or custom selectors.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from messageformat2 import Message
|
|
84
|
+
|
|
85
|
+
msg = Message ("""\
|
|
86
|
+
.input {$count :number}
|
|
87
|
+
.match $count
|
|
88
|
+
one {{You have {$count} notification.}}
|
|
89
|
+
* {{You have {$count} notifications.}}
|
|
90
|
+
""")
|
|
91
|
+
|
|
92
|
+
msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
93
|
+
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
94
|
+
```
|
|
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
|
+
|
|
117
|
+
Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
The default function registry includes these functions
|
|
2
|
+
|
|
3
|
+
in line with the [specification](https://github.com/unicode-org/message-format-wg/tree/main/spec/functions)
|
|
4
|
+
|
|
5
|
+
### `:string`
|
|
6
|
+
|
|
7
|
+
#### Formatter
|
|
8
|
+
|
|
9
|
+
As a formatter, this function simply calls `str(value)`.
|
|
10
|
+
|
|
11
|
+
#### Selector
|
|
12
|
+
|
|
13
|
+
As a selector, this function selects the exact match among the variant keys
|
|
14
|
+
(after NFC normalization of the value).
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from messageformat2 import format_message
|
|
18
|
+
|
|
19
|
+
format_message("""\
|
|
20
|
+
.input {$pet :string}
|
|
21
|
+
.match $pet
|
|
22
|
+
dog {{I have a dog}}
|
|
23
|
+
cat {{I have a cat}}
|
|
24
|
+
* {{I have a {$pet}}}
|
|
25
|
+
""", {"pet": "cat"})
|
|
26
|
+
# -> "I have a cat"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `:number`
|
|
30
|
+
|
|
31
|
+
The operand must be an `int`, `float` or `Decimal` (but not a `bool`), or a string matching the
|
|
32
|
+
`number-literal` production of the spec (e.g. `|-1234.5|`, `|1e3|`; `|01|` or `|.5|` are not numbers).
|
|
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.
|
|
35
|
+
|
|
36
|
+
#### Formatter
|
|
37
|
+
|
|
38
|
+
- `signDisplay`
|
|
39
|
+
- auto (default)
|
|
40
|
+
- always
|
|
41
|
+
- exceptZero
|
|
42
|
+
- negative
|
|
43
|
+
- never
|
|
44
|
+
- `useGrouping`
|
|
45
|
+
- auto (default)
|
|
46
|
+
- always
|
|
47
|
+
- never
|
|
48
|
+
- min2 (grouping separators only for numbers with at least 5 integer digits)
|
|
49
|
+
- `minimumIntegerDigits`
|
|
50
|
+
- `minimumFractionDigits`
|
|
51
|
+
- `maximumFractionDigits`
|
|
52
|
+
- `minimumSignificantDigits`
|
|
53
|
+
- `maximumSignificantDigits`
|
|
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)
|
|
73
|
+
|
|
74
|
+
The digit options are *digit size options*: small non-negative integers (`0`-`99`).
|
|
75
|
+
The numbering system is the default one of the locale (e.g. `Locale("ar", "EG")` uses Arabic-Indic digits).
|
|
76
|
+
|
|
77
|
+
#### Selector
|
|
78
|
+
|
|
79
|
+
The selector takes the same options as the formatter in addition to:
|
|
80
|
+
|
|
81
|
+
- `select`
|
|
82
|
+
- plural (default)
|
|
83
|
+
- ordinal
|
|
84
|
+
- exact
|
|
85
|
+
|
|
86
|
+
`select` must be set by a literal. The selector first tries an exact match between the number and
|
|
87
|
+
numeric keys (e.g. `1`), then (unless `select=exact`) matches the CLDR plural or ordinal category of the number
|
|
88
|
+
(`zero`, `one`, `two`, `few`, `many`, `other`). Any other key is a `BadVariantKey` error.
|
|
89
|
+
|
|
90
|
+
### `:integer`
|
|
91
|
+
|
|
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.
|
|
95
|
+
|
|
96
|
+
### `:offset`
|
|
97
|
+
|
|
98
|
+
Adds or subtracts a small integer from a numeric operand, e.g. to count "other" users:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
.input {$likes :integer}
|
|
102
|
+
.local $others = {$likes :offset subtract=1}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Exactly one of the options must be given:
|
|
106
|
+
|
|
107
|
+
- `add`
|
|
108
|
+
- `subtract`
|
|
109
|
+
|
|
110
|
+
The formatting options of an annotated operand (e.g. `signDisplay` of `{$likes :integer signDisplay=always}`)
|
|
111
|
+
are inherited. `:offset` can also be used as a selector, in which case it behaves like `:number`.
|
|
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
|
+
|
|
144
|
+
### `:datetime`, `:date`, `:time`
|
|
145
|
+
|
|
146
|
+
The operand must be a `datetime` (or a `date`/`time` for `:date`/`:time` respectively) or an ISO 8601 date or
|
|
147
|
+
datetime string that can be parsed with `datetime.fromisoformat`, e.g. `|2006-01-02|` or `|2006-01-02T15:04:06+01:00|`.
|
|
148
|
+
The resolved value of another date/time function is also accepted, in which case its override options
|
|
149
|
+
(`timeZone`, `hour12`) are inherited.
|
|
150
|
+
|
|
151
|
+
The option values below must be set by literals (an option set by a variable is a `BadOption` error).
|
|
152
|
+
|
|
153
|
+
#### `:datetime`
|
|
154
|
+
|
|
155
|
+
- `dateFields`
|
|
156
|
+
- weekday
|
|
157
|
+
- day-weekday
|
|
158
|
+
- month-day
|
|
159
|
+
- month-day-weekday
|
|
160
|
+
- year-month-day (default)
|
|
161
|
+
- year-month-day-weekday
|
|
162
|
+
- `dateLength`
|
|
163
|
+
- long
|
|
164
|
+
- medium (default)
|
|
165
|
+
- short
|
|
166
|
+
- `timePrecision`
|
|
167
|
+
- hour
|
|
168
|
+
- minute (default)
|
|
169
|
+
- second
|
|
170
|
+
- `timeZoneStyle`: no time zone is shown unless set
|
|
171
|
+
- long
|
|
172
|
+
- short
|
|
173
|
+
|
|
174
|
+
#### `:date`
|
|
175
|
+
|
|
176
|
+
- `fields`: same values as `dateFields` above
|
|
177
|
+
- `length`: same values as `dateLength` above
|
|
178
|
+
|
|
179
|
+
#### `:time`
|
|
180
|
+
|
|
181
|
+
- `precision`: same values as `timePrecision` above
|
|
182
|
+
- `timeZoneStyle`: same values as above
|
|
183
|
+
|
|
184
|
+
#### Override options
|
|
185
|
+
|
|
186
|
+
- `timeZone`: `UTC`, `input` (the time zone of the operand, which must then have one), an IANA time zone
|
|
187
|
+
name (`|Europe/Zurich|`) or an offset (`|+01:00|`). A naive `datetime` is interpreted in this time zone,
|
|
188
|
+
an aware one is converted to it.
|
|
189
|
+
- `hour12`: `true` or `false` (`:datetime` and `:time` only); the default is locale-dependent.
|
|
190
|
+
- `calendar`: not implemented (ignored).
|
|
191
|
+
|
|
192
|
+
#### Selector
|
|
193
|
+
|
|
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.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
## Runtime model
|
|
2
|
+
|
|
3
|
+
All expressions are lazily evaluated at the time of use.
|
|
4
|
+
If the same variable is referenced more than once, it will also be evaluated more than once.
|
|
5
|
+
Pay attention when providing custom formatters/selectors with side effects:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from messageformat2 import Message
|
|
9
|
+
|
|
10
|
+
counter = 0
|
|
11
|
+
|
|
12
|
+
def formatter_with_side_effect(value, locale, options):
|
|
13
|
+
global counter
|
|
14
|
+
counter += 1
|
|
15
|
+
return str(value)
|
|
16
|
+
|
|
17
|
+
msg = Message("""\
|
|
18
|
+
.local $foo = {|foo| :side_effect}
|
|
19
|
+
{{{$foo} {$foo}}}
|
|
20
|
+
""")
|
|
21
|
+
msg.format(formatters={"side_effect": formatter_with_side_effect})
|
|
22
|
+
print(counter) # -> 2
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Unannotated variables
|
|
26
|
+
|
|
27
|
+
A placeholder consisting only of a variable (`{$x}`) is formatted according to the type of its value:
|
|
28
|
+
numbers (`int`, `float`, `Decimal`) as if annotated with `:number`, `datetime`, `date` and `time` values with
|
|
29
|
+
`:datetime`, `:date` and `:time`. Anything else is formatted with `str()`.
|
|
30
|
+
Literals are always strings, so `{|1234.5|}` is formatted verbatim.
|
|
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
|
+
|
|
51
|
+
## Function composition
|
|
52
|
+
|
|
53
|
+
A variable which was annotated with a function can be annotated again, for example in a placeholder.
|
|
54
|
+
The function handler then receives the resolved value of the earlier annotation
|
|
55
|
+
(a `messageformat2.builtins.LazyValue` carrying the earlier function's name, operand and options) as its operand,
|
|
56
|
+
and decides what to do with it. The built-in numeric functions accept each other's resolved values
|
|
57
|
+
and inherit their options, with the options of the later expression taking priority:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from messageformat2 import format_message
|
|
61
|
+
|
|
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"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
whereas e.g. `{$x :date}` would raise `BadOperand`. See [Custom formatters](howto/formatter.md) for how to
|
|
69
|
+
handle such operands in your own functions.
|
|
70
|
+
|
|
71
|
+
## Whitespace
|
|
72
|
+
|
|
73
|
+
This library implements the [grammar](https://github.com/unicode-org/message-format-wg/blob/main/spec/message.abnf) exactly as specified which includes whitespace.
|
|
74
|
+
|
|
75
|
+
Whitespace around a complex message (a message containing declarations and/or a match block) is not significant,
|
|
76
|
+
so complex messages can be freely indented, e.g. in multiline strings:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from messageformat2 import Message
|
|
80
|
+
|
|
81
|
+
Message("""
|
|
82
|
+
.input {$count :integer}
|
|
83
|
+
.match $count
|
|
84
|
+
* {{count is {$count}}}
|
|
85
|
+
""")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Whitespace in a simple message (a message consisting of a single pattern) is always significant
|
|
89
|
+
and is part of the message text. Note that the first non-whitespace character of a simple message
|
|
90
|
+
cannot be a `.`, so a misspelled keyword (`.imput`) is a syntax error rather than silently becoming text.
|
|
91
|
+
To get a pattern which starts with a `.`, use a quoted pattern:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
Message("{{.input is not a keyword here}}")
|
|
95
|
+
```
|