messageformat2 0.1.0__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-1.0.0/.github/dependabot.yml +27 -0
- messageformat2-1.0.0/.github/workflows/ci.yml +38 -0
- messageformat2-1.0.0/.github/workflows/release.yml +31 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/.gitignore +1 -3
- {messageformat2-0.1.0 → messageformat2-1.0.0}/.readthedocs.yaml +9 -7
- {messageformat2-0.1.0 → messageformat2-1.0.0}/CONTRIBUTING.md +1 -1
- {messageformat2-0.1.0 → messageformat2-1.0.0}/PKG-INFO +25 -37
- {messageformat2-0.1.0 → messageformat2-1.0.0}/README.md +10 -7
- messageformat2-1.0.0/docs/builtins.md +142 -0
- messageformat2-1.0.0/docs/explanation.md +71 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/getting_started.md +2 -1
- {messageformat2-0.1.0 → 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.0 → messageformat2-1.0.0}/docs/howto/selector.md +4 -2
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/index.md +4 -14
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/__init__.py +1 -1
- messageformat2-1.0.0/messageformat2/builtins.py +592 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/datamodel.py +70 -105
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/errors.py +51 -22
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/message.py +34 -11
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/parser.py +106 -159
- messageformat2-1.0.0/messageformat2/runtime.py +317 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/pyproject.toml +23 -25
- {messageformat2-0.1.0 → 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.0 → 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.0 → 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.0/.github/workflows/ci.yml +0 -34
- messageformat2-0.1.0/docs/builtins.md +0 -172
- messageformat2-0.1.0/docs/explanation.md +0 -86
- messageformat2-0.1.0/docs/howto/errors.md +0 -48
- messageformat2-0.1.0/docs/howto/formatter.md +0 -51
- messageformat2-0.1.0/docs/reference.md +0 -12
- messageformat2-0.1.0/messageformat2/builtins.py +0 -323
- messageformat2-0.1.0/messageformat2/runtime.py +0 -301
- messageformat2-0.1.0/messageformat2.egg-info/PKG-INFO +0 -148
- messageformat2-0.1.0/messageformat2.egg-info/SOURCES.txt +0 -70
- messageformat2-0.1.0/messageformat2.egg-info/dependency_links.txt +0 -1
- messageformat2-0.1.0/messageformat2.egg-info/entry_points.txt +0 -2
- messageformat2-0.1.0/messageformat2.egg-info/requires.txt +0 -20
- messageformat2-0.1.0/messageformat2.egg-info/top_level.txt +0 -1
- messageformat2-0.1.0/requirements.dev.txt +0 -176
- messageformat2-0.1.0/requirements.docs.txt +0 -96
- messageformat2-0.1.0/requirements.txt +0 -4
- messageformat2-0.1.0/setup.cfg +0 -4
- messageformat2-0.1.0/tests/data/complex_message_3.txt +0 -22
- messageformat2-0.1.0/tests/data/unsupported_annotations_1.txt +0 -4
- messageformat2-0.1.0/tests/data/unsupported_annotations_2.txt +0 -4
- messageformat2-0.1.0/tests/data/unsupported_annotations_3.txt +0 -3
- messageformat2-0.1.0/tests/data/unsupported_annotations_4.txt +0 -6
- messageformat2-0.1.0/tests/data/unsupported_annotations_5.txt +0 -8
- messageformat2-0.1.0/tests/data/unsupported_statement_1.txt +0 -15
- messageformat2-0.1.0/tests/data/unsupported_statement_2.txt +0 -21
- messageformat2-0.1.0/tests/test_parser.py +0 -235
- messageformat2-0.1.0/tests/test_runtime.py +0 -242
- {messageformat2-0.1.0 → messageformat2-1.0.0}/LICENSE +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/datamodel.md +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/errors.md +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/message.md +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/docs/mf2.md +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/messageformat2/cli.py +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/mkdocs.yml +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/__init__.py +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/complex_message_1.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_1.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_10.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_11.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_12.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_13.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_14.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_15.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_16.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_2.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_3.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_4.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_5.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_6.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_7.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_8.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_escapes_1.txt +0 -0
- {messageformat2-0.1.0 → messageformat2-1.0.0}/tests/data/simple_message_escapes_2.txt +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
day: "monday"
|
|
8
|
+
time: "09:00"
|
|
9
|
+
timezone: "Europe/Zurich"
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
groups:
|
|
13
|
+
github-actions:
|
|
14
|
+
patterns: ["*"]
|
|
15
|
+
|
|
16
|
+
- package-ecosystem: "pip"
|
|
17
|
+
directory: "/"
|
|
18
|
+
schedule:
|
|
19
|
+
interval: "weekly"
|
|
20
|
+
day: "monday"
|
|
21
|
+
time: "09:00"
|
|
22
|
+
timezone: "Europe/Zurich"
|
|
23
|
+
cooldown:
|
|
24
|
+
default-days: 7
|
|
25
|
+
groups:
|
|
26
|
+
python-dependencies:
|
|
27
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13", "3.14", "3.15"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7.0.1
|
|
18
|
+
|
|
19
|
+
- name: Install the latest version of uv and set the python version
|
|
20
|
+
uses: astral-sh/setup-uv@v7
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Lint code 🧹
|
|
25
|
+
run: |
|
|
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/
|
|
31
|
+
|
|
32
|
+
- name: Type check 🏷
|
|
33
|
+
run: |
|
|
34
|
+
uv run ty check messageformat2/ tests/
|
|
35
|
+
|
|
36
|
+
- name: Run tests 🧪
|
|
37
|
+
run: |
|
|
38
|
+
uv run pytest --no-cov --doctest-modules
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Upload To PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
pypi-publish:
|
|
13
|
+
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: release
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v7.0.1
|
|
21
|
+
|
|
22
|
+
- name: Install the latest version of uv and set the python version
|
|
23
|
+
uses: astral-sh/setup-uv@v7
|
|
24
|
+
with:
|
|
25
|
+
python-version: '3.14'
|
|
26
|
+
|
|
27
|
+
- name: Build package 📦
|
|
28
|
+
run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Publish package distributions to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -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.
|
|
11
|
+
python: "3.14"
|
|
12
12
|
|
|
13
|
-
mkdocs:
|
|
14
|
-
configuration: mkdocs.yml
|
|
15
|
-
|
|
16
|
-
# Optionally declare the Python requirements required to build your docs
|
|
17
13
|
python:
|
|
18
14
|
install:
|
|
19
|
-
-
|
|
15
|
+
- method: uv
|
|
16
|
+
command: sync
|
|
17
|
+
groups:
|
|
18
|
+
- docs
|
|
19
|
+
|
|
20
|
+
mkdocs:
|
|
21
|
+
configuration: mkdocs.yml
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: messageformat2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python implementation of Unicode MessageFormat 2.0
|
|
5
|
+
Project-URL: Homepage, https://github.com/tomasr8/messageformat2
|
|
6
|
+
Project-URL: Github, https://github.com/tomasr8/messageformat2
|
|
7
|
+
Project-URL: Documentation, https://messageformat2.readthedocs.io/en/latest/
|
|
5
8
|
Author-email: Tomas Roun <tomas.roun8@gmail.com>
|
|
6
9
|
License: MIT License
|
|
7
10
|
|
|
@@ -24,52 +27,34 @@ License: MIT License
|
|
|
24
27
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
28
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
29
|
SOFTWARE.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Project-URL: Github, https://github.com/tomasr8/messageformat2
|
|
30
|
-
Keywords: i18n,internationalization,localization,messageformat,messageformat 2,cldr,gettext
|
|
31
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cldr,gettext,i18n,internationalization,localization,messageformat,messageformat 2
|
|
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
|
|
41
|
+
Requires-Dist: babel>=2
|
|
42
|
+
Requires-Dist: click>=8
|
|
39
43
|
Description-Content-Type: text/markdown
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Requires-Dist: build; extra == "dev"
|
|
45
|
-
Requires-Dist: twine; extra == "dev"
|
|
46
|
-
Requires-Dist: uv; extra == "dev"
|
|
47
|
-
Requires-Dist: pytest; extra == "dev"
|
|
48
|
-
Requires-Dist: pytest-snapshot; extra == "dev"
|
|
49
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
50
|
-
Requires-Dist: ruff; extra == "dev"
|
|
51
|
-
Requires-Dist: pyright; extra == "dev"
|
|
52
|
-
Requires-Dist: mkdocs; extra == "dev"
|
|
53
|
-
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
54
|
-
Requires-Dist: mkdocs-material; extra == "dev"
|
|
55
|
-
Provides-Extra: docs
|
|
56
|
-
Requires-Dist: mkdocs; extra == "docs"
|
|
57
|
-
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
58
|
-
Requires-Dist: mkdocs-material; extra == "docs"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg) [](https://microsoft.github.io/pyright/)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
63
48
|
|
|
64
49
|
This is a Python implementation of the [Unicode Message Format 2.0
|
|
65
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
50
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
66
51
|
|
|
67
52
|
Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
|
|
68
53
|
more expressive and flexible than both Message Format 1.0 and, in the context of
|
|
69
54
|
Python, gettext.
|
|
70
55
|
|
|
71
56
|
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
72
|
-
specification](https://www.unicode.org/reports/tr35/tr35-
|
|
57
|
+
specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
|
|
73
58
|
|
|
74
59
|
## Features
|
|
75
60
|
|
|
@@ -77,12 +62,14 @@ The library includes:
|
|
|
77
62
|
|
|
78
63
|
- A message parser and a formatter
|
|
79
64
|
- Several builtin formatters and selectors described by the
|
|
80
|
-
[spec](https://
|
|
65
|
+
[spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
|
|
81
66
|
and the possibility to write custom formatters/selectors
|
|
82
67
|
- Helper classes to inspect and transform the message [data
|
|
83
|
-
model](https://
|
|
68
|
+
model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
84
71
|
|
|
85
|
-
|
|
72
|
+
Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
|
|
86
73
|
|
|
87
74
|
## Installation
|
|
88
75
|
|
|
@@ -136,7 +123,8 @@ MF2 supports pluralization using built-in or custom selectors.
|
|
|
136
123
|
from messageformat2 import Message
|
|
137
124
|
|
|
138
125
|
msg = Message ("""\
|
|
139
|
-
.
|
|
126
|
+
.input {$count :number}
|
|
127
|
+
.match $count
|
|
140
128
|
one {{You have {$count} notification.}}
|
|
141
129
|
* {{You have {$count} notifications.}}
|
|
142
130
|
""")
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)
|
|
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()`.
|