messageformat2 0.1.1__tar.gz → 1.0.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.0.0}/.github/workflows/ci.yml +7 -3
- {messageformat2-0.1.1 → messageformat2-1.0.0}/.github/workflows/release.yml +1 -1
- {messageformat2-0.1.1 → messageformat2-1.0.0}/.readthedocs.yaml +9 -7
- {messageformat2-0.1.1 → messageformat2-1.0.0}/PKG-INFO +17 -12
- {messageformat2-0.1.1 → messageformat2-1.0.0}/README.md +9 -6
- messageformat2-1.0.0/docs/builtins.md +142 -0
- messageformat2-1.0.0/docs/explanation.md +71 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/getting_started.md +2 -1
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/howto/datamodel.md +1 -1
- messageformat2-1.0.0/docs/howto/errors.md +72 -0
- messageformat2-1.0.0/docs/howto/formatter.md +96 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/howto/selector.md +4 -2
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/index.md +4 -4
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/__init__.py +1 -1
- messageformat2-1.0.0/messageformat2/builtins.py +592 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/datamodel.py +70 -105
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/errors.py +51 -22
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/message.py +34 -11
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/parser.py +106 -159
- messageformat2-1.0.0/messageformat2/runtime.py +317 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/pyproject.toml +14 -11
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/complex_message_2.txt +7 -4
- messageformat2-1.0.0/tests/data/complex_message_3.txt +28 -0
- messageformat2-1.0.0/tests/data/simple_message_17.txt +8 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_9.txt +1 -1
- messageformat2-1.0.0/tests/data/simple_message_escapes_3.txt +1 -0
- messageformat2-1.0.0/tests/spec/README.md +18 -0
- messageformat2-1.0.0/tests/spec/bidi.json +147 -0
- messageformat2-1.0.0/tests/spec/data-model-errors.json +189 -0
- messageformat2-1.0.0/tests/spec/fallback.json +62 -0
- messageformat2-1.0.0/tests/spec/functions/currency.json +56 -0
- messageformat2-1.0.0/tests/spec/functions/date.json +45 -0
- messageformat2-1.0.0/tests/spec/functions/datetime.json +64 -0
- messageformat2-1.0.0/tests/spec/functions/integer.json +71 -0
- messageformat2-1.0.0/tests/spec/functions/number.json +334 -0
- messageformat2-1.0.0/tests/spec/functions/offset.json +77 -0
- messageformat2-1.0.0/tests/spec/functions/percent.json +45 -0
- messageformat2-1.0.0/tests/spec/functions/string.json +79 -0
- messageformat2-1.0.0/tests/spec/functions/time.json +42 -0
- messageformat2-1.0.0/tests/spec/pattern-selection.json +120 -0
- messageformat2-1.0.0/tests/spec/syntax-errors.json +246 -0
- messageformat2-1.0.0/tests/spec/syntax.json +763 -0
- messageformat2-1.0.0/tests/spec/u-options.json +104 -0
- messageformat2-1.0.0/tests/test_builtins.py +276 -0
- messageformat2-1.0.0/tests/test_parser.py +412 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/test_regex.py +11 -84
- messageformat2-1.0.0/tests/test_runtime.py +458 -0
- messageformat2-1.0.0/tests/test_spec.py +238 -0
- messageformat2-1.0.0/uv.lock +950 -0
- messageformat2-0.1.1/docs/builtins.md +0 -172
- messageformat2-0.1.1/docs/explanation.md +0 -71
- messageformat2-0.1.1/docs/howto/errors.md +0 -48
- messageformat2-0.1.1/docs/howto/formatter.md +0 -51
- 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.0.0}/.github/dependabot.yml +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/.gitignore +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/CONTRIBUTING.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/LICENSE +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/datamodel.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/errors.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/message.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/mf2.md +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/cli.py +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/mkdocs.yml +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/__init__.py +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/complex_message_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_10.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_11.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_12.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_13.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_14.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_15.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_16.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_2.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_3.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_4.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_5.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_6.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_7.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_8.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_escapes_1.txt +0 -0
- {messageformat2-0.1.1 → messageformat2-1.0.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: 0.
|
|
3
|
+
Version: 1.0.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,16 +29,17 @@ 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
|
|
|
@@ -45,14 +47,14 @@ Description-Content-Type: text/markdown
|
|
|
45
47
|
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
46
48
|
|
|
47
49
|
This is a Python implementation of the [Unicode Message Format 2.0
|
|
48
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
50
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
49
51
|
|
|
50
52
|
Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
|
|
51
53
|
more expressive and flexible than both Message Format 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
|
|
|
@@ -60,12 +62,14 @@ The library includes:
|
|
|
60
62
|
|
|
61
63
|
- A message parser and a formatter
|
|
62
64
|
- Several builtin formatters and selectors described by the
|
|
63
|
-
[spec](https://
|
|
65
|
+
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
64
66
|
and the possibility to write custom formatters/selectors
|
|
65
67
|
- Helper classes to inspect and transform the message [data
|
|
66
|
-
model](https://
|
|
68
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
67
69
|
|
|
68
|
-
##
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
|
|
69
73
|
|
|
70
74
|
## Installation
|
|
71
75
|
|
|
@@ -119,7 +123,8 @@ MF2 supports pluralization using built-in or custom selectors.
|
|
|
119
123
|
from messageformat2 import Message
|
|
120
124
|
|
|
121
125
|
msg = Message ("""\
|
|
122
|
-
.
|
|
126
|
+
.input {$count :number}
|
|
127
|
+
.match $count
|
|
123
128
|
one {{You have {$count} notification.}}
|
|
124
129
|
* {{You have {$count} notifications.}}
|
|
125
130
|
""")
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
4
4
|
|
|
5
5
|
This is a Python implementation of the [Unicode Message Format 2.0
|
|
6
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
6
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
7
7
|
|
|
8
8
|
Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
|
|
9
9
|
more expressive and flexible than both Message Format 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
|
|
13
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
13
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
@@ -18,12 +18,14 @@ The library includes:
|
|
|
18
18
|
|
|
19
19
|
- A message parser and a formatter
|
|
20
20
|
- Several builtin formatters and selectors described by the
|
|
21
|
-
[spec](https://
|
|
21
|
+
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
22
22
|
and the possibility to write custom formatters/selectors
|
|
23
23
|
- Helper classes to inspect and transform the message [data
|
|
24
|
-
model](https://
|
|
24
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Documentation
|
|
27
|
+
|
|
28
|
+
Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
|
|
27
29
|
|
|
28
30
|
## Installation
|
|
29
31
|
|
|
@@ -77,7 +79,8 @@ MF2 supports pluralization using built-in or custom selectors.
|
|
|
77
79
|
from messageformat2 import Message
|
|
78
80
|
|
|
79
81
|
msg = Message ("""\
|
|
80
|
-
.
|
|
82
|
+
.input {$count :number}
|
|
83
|
+
.match $count
|
|
81
84
|
one {{You have {$count} notification.}}
|
|
82
85
|
* {{You have {$count} notifications.}}
|
|
83
86
|
""")
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
format_message("""\
|
|
18
|
+
.input {$pet :string}
|
|
19
|
+
.match $pet
|
|
20
|
+
dog {{I have a dog}}
|
|
21
|
+
cat {{I have a cat}}
|
|
22
|
+
* {{I have a {$pet}}}
|
|
23
|
+
""", {"pet": "cat"})
|
|
24
|
+
# -> "I have a cat"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `:number`
|
|
28
|
+
|
|
29
|
+
The operand must be an `int`, `float` or `Decimal` (but not a `bool`), or a string matching the
|
|
30
|
+
`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, in which
|
|
32
|
+
case its options are inherited, with the options of the expression taking priority.
|
|
33
|
+
|
|
34
|
+
#### Formatter
|
|
35
|
+
|
|
36
|
+
- `signDisplay`
|
|
37
|
+
- auto (default)
|
|
38
|
+
- always
|
|
39
|
+
- exceptZero
|
|
40
|
+
- negative
|
|
41
|
+
- never
|
|
42
|
+
- `useGrouping`
|
|
43
|
+
- auto (default)
|
|
44
|
+
- always
|
|
45
|
+
- never
|
|
46
|
+
- min2 (treated as auto)
|
|
47
|
+
- `minimumIntegerDigits`
|
|
48
|
+
- `minimumFractionDigits`
|
|
49
|
+
- `maximumFractionDigits`
|
|
50
|
+
- `minimumSignificantDigits`
|
|
51
|
+
- `maximumSignificantDigits`
|
|
52
|
+
- `numberingSystem` (implementation extension): a Unicode Number System Identifier
|
|
53
|
+
|
|
54
|
+
The digit options are *digit size options*: small non-negative integers (`0`-`99`).
|
|
55
|
+
Not implemented: `trailingZeroDisplay`, `roundingPriority`, `roundingIncrement`, `roundingMode` (these are ignored).
|
|
56
|
+
|
|
57
|
+
#### Selector
|
|
58
|
+
|
|
59
|
+
The selector takes the same options as the formatter in addition to:
|
|
60
|
+
|
|
61
|
+
- `select`
|
|
62
|
+
- plural (default)
|
|
63
|
+
- ordinal
|
|
64
|
+
- exact
|
|
65
|
+
|
|
66
|
+
`select` must be set by a literal. The selector first tries an exact match between the number and
|
|
67
|
+
numeric keys (e.g. `1`), then (unless `select=exact`) matches the CLDR plural or ordinal category of the number
|
|
68
|
+
(`zero`, `one`, `two`, `few`, `many`, `other`). Any other key is a `BadVariantKey` error.
|
|
69
|
+
|
|
70
|
+
### `:integer`
|
|
71
|
+
|
|
72
|
+
Same as `:number`, but the operand is truncated to an integer. The `minimumFractionDigits`, `maximumFractionDigits`
|
|
73
|
+
and `minimumSignificantDigits` options of an annotated operand are discarded.
|
|
74
|
+
|
|
75
|
+
### `:offset`
|
|
76
|
+
|
|
77
|
+
Adds or subtracts a small integer from a numeric operand, e.g. to count "other" users:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
.input {$likes :integer}
|
|
81
|
+
.local $others = {$likes :offset subtract=1}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Exactly one of the options must be given:
|
|
85
|
+
|
|
86
|
+
- `add`
|
|
87
|
+
- `subtract`
|
|
88
|
+
|
|
89
|
+
The formatting options of an annotated operand (e.g. `signDisplay` of `{$likes :integer signDisplay=always}`)
|
|
90
|
+
are inherited. `:offset` can also be used as a selector, in which case it behaves like `:number`.
|
|
91
|
+
|
|
92
|
+
### `:datetime`, `:date`, `:time`
|
|
93
|
+
|
|
94
|
+
The operand must be a `datetime` (or a `date`/`time` for `:date`/`:time` respectively) or an ISO 8601 date or
|
|
95
|
+
datetime string that can be parsed with `datetime.fromisoformat`, e.g. `|2006-01-02|` or `|2006-01-02T15:04:06+01:00|`.
|
|
96
|
+
The resolved value of another date/time function is also accepted, in which case its override options
|
|
97
|
+
(`timeZone`, `hour12`) are inherited.
|
|
98
|
+
|
|
99
|
+
The option values below must be set by literals (an option set by a variable is a `BadOption` error).
|
|
100
|
+
|
|
101
|
+
#### `:datetime`
|
|
102
|
+
|
|
103
|
+
- `dateFields`
|
|
104
|
+
- weekday
|
|
105
|
+
- day-weekday
|
|
106
|
+
- month-day
|
|
107
|
+
- month-day-weekday
|
|
108
|
+
- year-month-day (default)
|
|
109
|
+
- year-month-day-weekday
|
|
110
|
+
- `dateLength`
|
|
111
|
+
- long
|
|
112
|
+
- medium (default)
|
|
113
|
+
- short
|
|
114
|
+
- `timePrecision`
|
|
115
|
+
- hour
|
|
116
|
+
- minute (default)
|
|
117
|
+
- second
|
|
118
|
+
- `timeZoneStyle`: no time zone is shown unless set
|
|
119
|
+
- long
|
|
120
|
+
- short
|
|
121
|
+
|
|
122
|
+
#### `:date`
|
|
123
|
+
|
|
124
|
+
- `fields`: same values as `dateFields` above
|
|
125
|
+
- `length`: same values as `dateLength` above
|
|
126
|
+
|
|
127
|
+
#### `:time`
|
|
128
|
+
|
|
129
|
+
- `precision`: same values as `timePrecision` above
|
|
130
|
+
- `timeZoneStyle`: same values as above
|
|
131
|
+
|
|
132
|
+
#### Override options
|
|
133
|
+
|
|
134
|
+
- `timeZone`: `UTC`, `input` (the time zone of the operand, which must then have one), an IANA time zone
|
|
135
|
+
name (`|Europe/Zurich|`) or an offset (`|+01:00|`). A naive `datetime` is interpreted in this time zone,
|
|
136
|
+
an aware one is converted to it.
|
|
137
|
+
- `hour12`: `true` or `false` (`:datetime` and `:time` only); the default is locale-dependent.
|
|
138
|
+
- `calendar`: not implemented (ignored).
|
|
139
|
+
|
|
140
|
+
#### Selector
|
|
141
|
+
|
|
142
|
+
The date/time functions do not support selection, as it is not required by MF2.
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
counter = 0
|
|
9
|
+
|
|
10
|
+
def formatter_with_side_effect(value, locale, options):
|
|
11
|
+
nonlocal counter
|
|
12
|
+
counter +=1
|
|
13
|
+
return str(value)
|
|
14
|
+
|
|
15
|
+
msg = Message("\
|
|
16
|
+
.local $foo = {|foo| :side_effect}
|
|
17
|
+
{{{$foo} {$foo}}}
|
|
18
|
+
")
|
|
19
|
+
msg.format(formatters={'side_effect': formatter_with_side_effect})
|
|
20
|
+
print(counter) # -> 2
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Unannotated variables
|
|
24
|
+
|
|
25
|
+
A placeholder consisting only of a variable (`{$x}`) is formatted according to the type of its value:
|
|
26
|
+
numbers (`int`, `float`, `Decimal`) as if annotated with `:number`, `datetime`, `date` and `time` values with
|
|
27
|
+
`:datetime`, `:date` and `:time`. Anything else is formatted with `str()`.
|
|
28
|
+
Literals are always strings, so `{|1234.5|}` is formatted verbatim.
|
|
29
|
+
|
|
30
|
+
## Function composition
|
|
31
|
+
|
|
32
|
+
A variable which was annotated with a function can be annotated again, for example in a placeholder.
|
|
33
|
+
The function handler then receives the resolved value of the earlier annotation
|
|
34
|
+
(a `messageformat2.builtins.LazyValue` carrying the earlier function's name, operand and options) as its operand,
|
|
35
|
+
and decides what to do with it. The built-in numeric functions accept each other's resolved values
|
|
36
|
+
and inherit their options, with the options of the later expression taking priority:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
.local $x = {42 :number minimumFractionDigits=2 signDisplay=always}
|
|
40
|
+
{{x = {$x :number minimumFractionDigits=1}}} # -> "x = +42.0"
|
|
41
|
+
|
|
42
|
+
.local $x = {1.25 :integer}
|
|
43
|
+
{{x = {$x :number}}} # -> "x = 1"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
whereas e.g. `{$x :date}` would raise `BadOperand`. See [Custom formatters](howto/formatter.md) for how to
|
|
47
|
+
handle such operands in your own functions.
|
|
48
|
+
|
|
49
|
+
## Whitespace
|
|
50
|
+
|
|
51
|
+
This library implements the [grammar](https://github.com/unicode-org/message-format-wg/blob/main/spec/message.abnf) exactly as specified which includes whitespace.
|
|
52
|
+
|
|
53
|
+
Whitespace around a complex message (a message containing declarations and/or a match block) is not significant,
|
|
54
|
+
so complex messages can be freely indented, e.g. in multiline strings:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
Message("""
|
|
58
|
+
.input {$count :integer}
|
|
59
|
+
.match $count
|
|
60
|
+
* {{count is {$count}}}
|
|
61
|
+
""")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Whitespace in a simple message (a message consisting of a single pattern) is always significant
|
|
65
|
+
and is part of the message text. Note that the first non-whitespace character of a simple message
|
|
66
|
+
cannot be a `.`, so a misspelled keyword (`.imput`) is a syntax error rather than silently becoming text.
|
|
67
|
+
To get a pattern which starts with a `.`, use a quoted pattern:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
Message("{{.input is not a keyword here}}")
|
|
71
|
+
```
|
|
@@ -48,7 +48,8 @@ MF2 supports pluralization using built-in or custom selectors.
|
|
|
48
48
|
from messageformat2 import Message
|
|
49
49
|
|
|
50
50
|
msg = Message ("""\
|
|
51
|
-
.
|
|
51
|
+
.input {$count :number}
|
|
52
|
+
.match $count
|
|
52
53
|
one {{You have {$count} notification.}}
|
|
53
54
|
* {{You have {$count} notifications.}}
|
|
54
55
|
""")
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
spec](https://
|
|
3
|
+
spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md).
|
|
4
4
|
The data model nodes are documented [here](/datamodel).
|
|
5
5
|
|
|
6
6
|
There are two classes to work with the datamodel - `DataModelVisitor` &
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Error handling
|
|
2
|
+
|
|
3
|
+
Errors might be raised during parsing or during formatting. You can learn more
|
|
4
|
+
about MF2 errors
|
|
5
|
+
[here](https://github.com/unicode-org/message-format-wg/blob/main/spec/errors.md).
|
|
6
|
+
|
|
7
|
+
During parsing the following exceptions (or their subclasses) might be raised:
|
|
8
|
+
|
|
9
|
+
- `messageformat2.errors.ParseError` - for syntax errors
|
|
10
|
+
- `messageformat2.errors.DataModelError` - for semantic errors
|
|
11
|
+
|
|
12
|
+
During formatting the following exception (or its subclasses) might be raised:
|
|
13
|
+
|
|
14
|
+
- `messageformat2.errors.FormatError`
|
|
15
|
+
|
|
16
|
+
## Handling parsing errors
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from messageformat2.message import Message
|
|
20
|
+
from messageformat2.errors import ParseError, DataModelError
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
Message("}} invalid syntax")
|
|
24
|
+
except ParseError as e:
|
|
25
|
+
print(e) # -> Invalid syntax
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
Message("""\
|
|
30
|
+
.input {$count :integer}
|
|
31
|
+
.match $count
|
|
32
|
+
one {{The count is {$count}}}
|
|
33
|
+
""")
|
|
34
|
+
except DataModelError as e:
|
|
35
|
+
print(e) # -> Missing fallback variant
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Handling formatting errors
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from messageformat2.message import Message
|
|
42
|
+
from messageformat2.errors import FormatError
|
|
43
|
+
|
|
44
|
+
msg = Message("Hello, {$name}!")
|
|
45
|
+
try:
|
|
46
|
+
msg.format()
|
|
47
|
+
except FormatError as e:
|
|
48
|
+
print(e) # -> Unresolved variable
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Formatting with fallback values
|
|
52
|
+
|
|
53
|
+
By default the first formatting error is raised and no output is produced. The spec requires
|
|
54
|
+
that a valid message can always be formatted, with
|
|
55
|
+
[fallback values](https://github.com/unicode-org/message-format-wg/blob/main/spec/formatting.md#fallback-resolution)
|
|
56
|
+
standing in for the parts that failed. Pass an `on_error` callback to get this behaviour: every error is
|
|
57
|
+
passed to the callback instead of being raised, and formatting continues.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from messageformat2.message import Message
|
|
61
|
+
|
|
62
|
+
errors = []
|
|
63
|
+
msg = Message("Hello, {$name}! You have {$count :number} messages.")
|
|
64
|
+
msg.format({"count": "many"}, on_error=errors.append)
|
|
65
|
+
# -> "Hello, {$name}! You have {$count} messages."
|
|
66
|
+
errors
|
|
67
|
+
# -> [UnresolvedVariable('Unresolved variable: name'), BadOperand("Bad operand: 'many' is not a number")]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The fallback value is `{$name}` for a variable, `{|literal|}` for a literal operand and `{:function}` for a
|
|
71
|
+
function without an operand. An option whose value cannot be resolved is omitted, and a selector which fails
|
|
72
|
+
only matches the catch-all key `*`. Syntax and data model errors are still raised by `Message()`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
It is possible to write a custom formatter to extend the functionality of the
|
|
2
|
+
library. Here's an example of a simple formatter:
|
|
3
|
+
|
|
4
|
+
```python
|
|
5
|
+
def capitalize(value: Any, locale, options) -> str:
|
|
6
|
+
return value.capitalize()
|
|
7
|
+
|
|
8
|
+
format_message(
|
|
9
|
+
"{|foo bar| :capitalize}",
|
|
10
|
+
formatters={"capitalize": capitalize}
|
|
11
|
+
)
|
|
12
|
+
# -> "Foo bar"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A formatter function must take exactly 3 arguments:
|
|
16
|
+
|
|
17
|
+
- `value`: the value passed to the formatter when formatting
|
|
18
|
+
- `locale`: the current locale as a `babel.Locale` instance
|
|
19
|
+
- `options`: a dictionary containing all the options passed to the formatter.
|
|
20
|
+
|
|
21
|
+
The return value of this function is converted to string and displayed.
|
|
22
|
+
|
|
23
|
+
Here's an example using `options`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
def capitalize(value: str, locale, options) -> str:
|
|
27
|
+
if options.get('eachWord') == "yes":
|
|
28
|
+
return value.title()
|
|
29
|
+
return value.capitalize()
|
|
30
|
+
|
|
31
|
+
format_message(
|
|
32
|
+
"{|foo bar| :capitalize eachWord=yes}",
|
|
33
|
+
formatters={"capitalize": capitalize}
|
|
34
|
+
)
|
|
35
|
+
# -> "Foo Bar"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Any errors during the evaluation of the formatter will be automatically caught
|
|
39
|
+
and `messageformat2.errors.MessageFunctionError` will be reraised instead.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from messageformat2.errors import MessageFunctionError
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
format_message(
|
|
46
|
+
"{foo :raise}",
|
|
47
|
+
formatters={"raise": raise_some_error}
|
|
48
|
+
)
|
|
49
|
+
except MessageFunctionError as e:
|
|
50
|
+
print(e)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To signal a specific error, a formatter can raise one of the `MessageFunctionError` subclasses
|
|
54
|
+
defined by the spec: `BadOperand` (wrong operand type or format), `BadOption` (invalid option or
|
|
55
|
+
option value), `BadVariantKey` (a variant key the selector cannot interpret) or `UnsupportedOperation`.
|
|
56
|
+
These are reraised as-is.
|
|
57
|
+
|
|
58
|
+
If the function can produce a result despite the error (the spec often asks to "emit a Bad Option error and
|
|
59
|
+
ignore the option"), pass it as `result`: when the message is formatted with an `on_error` callback
|
|
60
|
+
(see [Error handling](errors.md)), the error is reported and the result is used; otherwise the error is raised.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from messageformat2.errors import BadOption
|
|
64
|
+
|
|
65
|
+
def shout(value, locale, options) -> str:
|
|
66
|
+
result = str(value).upper()
|
|
67
|
+
if options.get("volume", "loud") != "loud":
|
|
68
|
+
raise BadOption("volume must be loud", result=result)
|
|
69
|
+
return result
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Annotated operands
|
|
73
|
+
|
|
74
|
+
If the operand of your formatter is a variable that was already annotated with a function,
|
|
75
|
+
`value` is a `messageformat2.builtins.LazyValue` carrying the earlier function's name (`fn_name`),
|
|
76
|
+
operand (`value`) and resolved options (`options`). `str(value)` gives the formatted value of the earlier annotation.
|
|
77
|
+
It is up to your function to decide which operands it accepts and whether to inherit the earlier options;
|
|
78
|
+
raise `BadOperand` if your function cannot use the operand.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from messageformat2.builtins import LazyValue
|
|
82
|
+
from messageformat2.errors import BadOperand
|
|
83
|
+
|
|
84
|
+
def shout(value, locale, options) -> str:
|
|
85
|
+
if isinstance(value, LazyValue):
|
|
86
|
+
if value.fn_name != "string":
|
|
87
|
+
raise BadOperand(f"Cannot shout a :{value.fn_name}")
|
|
88
|
+
value = str(value)
|
|
89
|
+
return str(value).upper()
|
|
90
|
+
|
|
91
|
+
format_message(
|
|
92
|
+
".local $name = {alice :string} {{{$name :shout}}}",
|
|
93
|
+
formatters={"shout": shout}
|
|
94
|
+
)
|
|
95
|
+
# -> "ALICE"
|
|
96
|
+
```
|
|
@@ -24,7 +24,8 @@ def integer_selector(
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
format_message("""\
|
|
27
|
-
.
|
|
27
|
+
.input {$count :integer select=plural}
|
|
28
|
+
.match $count
|
|
28
29
|
1 {{You have one notification.}}
|
|
29
30
|
one {{You have {$count} notification.}}
|
|
30
31
|
* {{You have {$count} notifications.}}""",
|
|
@@ -44,7 +45,8 @@ A selector function must take exactly 3 arguments:
|
|
|
44
45
|
For example, for this message:
|
|
45
46
|
|
|
46
47
|
```python
|
|
47
|
-
.
|
|
48
|
+
.input {$count :integer select=plural}
|
|
49
|
+
.match $count
|
|
48
50
|
1 {{You have one notification.}}
|
|
49
51
|
one {{You have {$count} notification.}}
|
|
50
52
|
* {{You have ???}}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
This site contains documentation for the `messageformat2`, a Python library
|
|
2
2
|
which implements the [Unicode Message Format 2.0
|
|
3
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
3
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
4
4
|
|
|
5
5
|
Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
|
|
6
6
|
more expressive and flexible than both Message Format 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
|
|
10
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
10
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
@@ -15,10 +15,10 @@ The `messageformat2` library includes:
|
|
|
15
15
|
|
|
16
16
|
- A message parser and a formatter
|
|
17
17
|
- Several builtin formatters and selectors described by the
|
|
18
|
-
[spec](https://
|
|
18
|
+
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
19
19
|
and the possibility to write custom formatters/selectors
|
|
20
20
|
- Helper classes to inspect and transform the message [data
|
|
21
|
-
model](https://
|
|
21
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
22
22
|
|
|
23
23
|
### Work in progress
|
|
24
24
|
|