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.
Files changed (95) hide show
  1. {messageformat2-0.1.1 → messageformat2-1.0.0}/.github/workflows/ci.yml +7 -3
  2. {messageformat2-0.1.1 → messageformat2-1.0.0}/.github/workflows/release.yml +1 -1
  3. {messageformat2-0.1.1 → messageformat2-1.0.0}/.readthedocs.yaml +9 -7
  4. {messageformat2-0.1.1 → messageformat2-1.0.0}/PKG-INFO +17 -12
  5. {messageformat2-0.1.1 → messageformat2-1.0.0}/README.md +9 -6
  6. messageformat2-1.0.0/docs/builtins.md +142 -0
  7. messageformat2-1.0.0/docs/explanation.md +71 -0
  8. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/getting_started.md +2 -1
  9. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/howto/datamodel.md +1 -1
  10. messageformat2-1.0.0/docs/howto/errors.md +72 -0
  11. messageformat2-1.0.0/docs/howto/formatter.md +96 -0
  12. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/howto/selector.md +4 -2
  13. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/index.md +4 -4
  14. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/__init__.py +1 -1
  15. messageformat2-1.0.0/messageformat2/builtins.py +592 -0
  16. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/datamodel.py +70 -105
  17. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/errors.py +51 -22
  18. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/message.py +34 -11
  19. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/parser.py +106 -159
  20. messageformat2-1.0.0/messageformat2/runtime.py +317 -0
  21. {messageformat2-0.1.1 → messageformat2-1.0.0}/pyproject.toml +14 -11
  22. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/complex_message_2.txt +7 -4
  23. messageformat2-1.0.0/tests/data/complex_message_3.txt +28 -0
  24. messageformat2-1.0.0/tests/data/simple_message_17.txt +8 -0
  25. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_9.txt +1 -1
  26. messageformat2-1.0.0/tests/data/simple_message_escapes_3.txt +1 -0
  27. messageformat2-1.0.0/tests/spec/README.md +18 -0
  28. messageformat2-1.0.0/tests/spec/bidi.json +147 -0
  29. messageformat2-1.0.0/tests/spec/data-model-errors.json +189 -0
  30. messageformat2-1.0.0/tests/spec/fallback.json +62 -0
  31. messageformat2-1.0.0/tests/spec/functions/currency.json +56 -0
  32. messageformat2-1.0.0/tests/spec/functions/date.json +45 -0
  33. messageformat2-1.0.0/tests/spec/functions/datetime.json +64 -0
  34. messageformat2-1.0.0/tests/spec/functions/integer.json +71 -0
  35. messageformat2-1.0.0/tests/spec/functions/number.json +334 -0
  36. messageformat2-1.0.0/tests/spec/functions/offset.json +77 -0
  37. messageformat2-1.0.0/tests/spec/functions/percent.json +45 -0
  38. messageformat2-1.0.0/tests/spec/functions/string.json +79 -0
  39. messageformat2-1.0.0/tests/spec/functions/time.json +42 -0
  40. messageformat2-1.0.0/tests/spec/pattern-selection.json +120 -0
  41. messageformat2-1.0.0/tests/spec/syntax-errors.json +246 -0
  42. messageformat2-1.0.0/tests/spec/syntax.json +763 -0
  43. messageformat2-1.0.0/tests/spec/u-options.json +104 -0
  44. messageformat2-1.0.0/tests/test_builtins.py +276 -0
  45. messageformat2-1.0.0/tests/test_parser.py +412 -0
  46. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/test_regex.py +11 -84
  47. messageformat2-1.0.0/tests/test_runtime.py +458 -0
  48. messageformat2-1.0.0/tests/test_spec.py +238 -0
  49. messageformat2-1.0.0/uv.lock +950 -0
  50. messageformat2-0.1.1/docs/builtins.md +0 -172
  51. messageformat2-0.1.1/docs/explanation.md +0 -71
  52. messageformat2-0.1.1/docs/howto/errors.md +0 -48
  53. messageformat2-0.1.1/docs/howto/formatter.md +0 -51
  54. messageformat2-0.1.1/messageformat2/builtins.py +0 -323
  55. messageformat2-0.1.1/messageformat2/runtime.py +0 -301
  56. messageformat2-0.1.1/tests/data/complex_message_3.txt +0 -22
  57. messageformat2-0.1.1/tests/data/unsupported_annotations_1.txt +0 -4
  58. messageformat2-0.1.1/tests/data/unsupported_annotations_2.txt +0 -4
  59. messageformat2-0.1.1/tests/data/unsupported_annotations_3.txt +0 -3
  60. messageformat2-0.1.1/tests/data/unsupported_annotations_4.txt +0 -6
  61. messageformat2-0.1.1/tests/data/unsupported_annotations_5.txt +0 -8
  62. messageformat2-0.1.1/tests/data/unsupported_statement_1.txt +0 -15
  63. messageformat2-0.1.1/tests/data/unsupported_statement_2.txt +0 -21
  64. messageformat2-0.1.1/tests/test_parser.py +0 -235
  65. messageformat2-0.1.1/tests/test_runtime.py +0 -242
  66. messageformat2-0.1.1/uv.lock +0 -776
  67. {messageformat2-0.1.1 → messageformat2-1.0.0}/.github/dependabot.yml +0 -0
  68. {messageformat2-0.1.1 → messageformat2-1.0.0}/.gitignore +0 -0
  69. {messageformat2-0.1.1 → messageformat2-1.0.0}/CONTRIBUTING.md +0 -0
  70. {messageformat2-0.1.1 → messageformat2-1.0.0}/LICENSE +0 -0
  71. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/datamodel.md +0 -0
  72. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/errors.md +0 -0
  73. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/message.md +0 -0
  74. {messageformat2-0.1.1 → messageformat2-1.0.0}/docs/mf2.md +0 -0
  75. {messageformat2-0.1.1 → messageformat2-1.0.0}/messageformat2/cli.py +0 -0
  76. {messageformat2-0.1.1 → messageformat2-1.0.0}/mkdocs.yml +0 -0
  77. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/__init__.py +0 -0
  78. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/complex_message_1.txt +0 -0
  79. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_1.txt +0 -0
  80. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_10.txt +0 -0
  81. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_11.txt +0 -0
  82. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_12.txt +0 -0
  83. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_13.txt +0 -0
  84. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_14.txt +0 -0
  85. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_15.txt +0 -0
  86. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_16.txt +0 -0
  87. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_2.txt +0 -0
  88. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_3.txt +0 -0
  89. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_4.txt +0 -0
  90. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_5.txt +0 -0
  91. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_6.txt +0 -0
  92. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_7.txt +0 -0
  93. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_8.txt +0 -0
  94. {messageformat2-0.1.1 → messageformat2-1.0.0}/tests/data/simple_message_escapes_1.txt +0 -0
  95. {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@v6
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/ --exclude messageformat2/cli.py
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: |
@@ -17,7 +17,7 @@ jobs:
17
17
  id-token: write
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v6
20
+ - uses: actions/checkout@v7.0.1
21
21
 
22
22
  - name: Install the latest version of uv and set the python version
23
23
  uses: astral-sh/setup-uv@v7
@@ -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-22.04
9
+ os: ubuntu-26.04
10
10
  tools:
11
- python: "3.12"
12
- jobs:
13
- post_create_environment:
14
- - pip install uv
15
- post_install:
16
- - uv sync --group docs
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: messageformat2
3
- Version: 0.1.1
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: Topic :: Scientific/Engineering
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==2.17.0
40
- Requires-Dist: click==8.3.1
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 [![Documentation Status](https://readthedocs.org/projects/messageformat2/badge/?version=latest)](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [![CI](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)](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-72/tr35-messageFormat.html).
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-72/tr35-messageFormat.html).
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#function-registry)
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model)
68
+ model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
67
69
 
68
- ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
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
- .match {$count :number}
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 [![Documentation Status](https://readthedocs.org/projects/messageformat2/badge/?version=latest)](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [![CI](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg)](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-72/tr35-messageFormat.html).
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-72/tr35-messageFormat.html).
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#function-registry)
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model)
24
+ model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
25
25
 
26
- ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
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
- .match {$count :number}
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
- .match {$count :number}
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model).
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
- .match {$count :integer select=plural}
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
- .match {$count :integer select=plural}
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-72/tr35-messageFormat.html).
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-72/tr35-messageFormat.html).
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#function-registry)
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://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model)
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
 
@@ -1,5 +1,5 @@
1
1
  from messageformat2.message import Message, format_message
2
2
 
3
3
 
4
- __version__ = "0.1.1"
4
+ __version__ = "1.0.0"
5
5
  __all__ = ["Message", "format_message"]