messageformat2 0.1.1__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {messageformat2-0.1.1 → messageformat2-1.1.0}/.github/workflows/ci.yml +7 -3
  2. {messageformat2-0.1.1 → messageformat2-1.1.0}/.github/workflows/release.yml +1 -1
  3. {messageformat2-0.1.1 → messageformat2-1.1.0}/.readthedocs.yaml +9 -7
  4. {messageformat2-0.1.1 → messageformat2-1.1.0}/PKG-INFO +53 -23
  5. messageformat2-1.1.0/README.md +117 -0
  6. messageformat2-1.1.0/docs/builtins.md +204 -0
  7. messageformat2-1.1.0/docs/explanation.md +95 -0
  8. messageformat2-1.1.0/docs/getting_started.md +85 -0
  9. {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/howto/datamodel.md +2 -2
  10. messageformat2-1.1.0/docs/howto/errors.md +72 -0
  11. messageformat2-1.1.0/docs/howto/formatter.md +117 -0
  12. {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/howto/selector.md +22 -16
  13. messageformat2-1.1.0/docs/index.md +30 -0
  14. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/__init__.py +1 -1
  15. messageformat2-1.1.0/messageformat2/builtins.py +930 -0
  16. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/datamodel.py +70 -105
  17. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/errors.py +51 -22
  18. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/message.py +52 -13
  19. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/parser.py +106 -159
  20. messageformat2-1.1.0/messageformat2/runtime.py +386 -0
  21. {messageformat2-0.1.1 → messageformat2-1.1.0}/pyproject.toml +14 -11
  22. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/complex_message_2.txt +7 -4
  23. messageformat2-1.1.0/tests/data/complex_message_3.txt +28 -0
  24. messageformat2-1.1.0/tests/data/simple_message_17.txt +8 -0
  25. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_9.txt +1 -1
  26. messageformat2-1.1.0/tests/data/simple_message_escapes_3.txt +1 -0
  27. messageformat2-1.1.0/tests/spec/README.md +17 -0
  28. messageformat2-1.1.0/tests/spec/bidi.json +147 -0
  29. messageformat2-1.1.0/tests/spec/data-model-errors.json +189 -0
  30. messageformat2-1.1.0/tests/spec/fallback.json +62 -0
  31. messageformat2-1.1.0/tests/spec/functions/currency.json +56 -0
  32. messageformat2-1.1.0/tests/spec/functions/date.json +45 -0
  33. messageformat2-1.1.0/tests/spec/functions/datetime.json +64 -0
  34. messageformat2-1.1.0/tests/spec/functions/integer.json +71 -0
  35. messageformat2-1.1.0/tests/spec/functions/number.json +334 -0
  36. messageformat2-1.1.0/tests/spec/functions/offset.json +77 -0
  37. messageformat2-1.1.0/tests/spec/functions/percent.json +45 -0
  38. messageformat2-1.1.0/tests/spec/functions/string.json +79 -0
  39. messageformat2-1.1.0/tests/spec/functions/time.json +42 -0
  40. messageformat2-1.1.0/tests/spec/pattern-selection.json +120 -0
  41. messageformat2-1.1.0/tests/spec/syntax-errors.json +246 -0
  42. messageformat2-1.1.0/tests/spec/syntax.json +763 -0
  43. messageformat2-1.1.0/tests/spec/u-options.json +104 -0
  44. messageformat2-1.1.0/tests/test_builtins.py +454 -0
  45. messageformat2-1.1.0/tests/test_parser.py +412 -0
  46. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/test_regex.py +11 -84
  47. messageformat2-1.1.0/tests/test_runtime.py +552 -0
  48. messageformat2-1.1.0/tests/test_spec.py +239 -0
  49. messageformat2-1.1.0/uv.lock +950 -0
  50. messageformat2-0.1.1/README.md +0 -89
  51. messageformat2-0.1.1/docs/builtins.md +0 -172
  52. messageformat2-0.1.1/docs/explanation.md +0 -71
  53. messageformat2-0.1.1/docs/getting_started.md +0 -58
  54. messageformat2-0.1.1/docs/howto/errors.md +0 -48
  55. messageformat2-0.1.1/docs/howto/formatter.md +0 -51
  56. messageformat2-0.1.1/docs/index.md +0 -27
  57. messageformat2-0.1.1/docs/mf2.md +0 -13
  58. messageformat2-0.1.1/messageformat2/builtins.py +0 -323
  59. messageformat2-0.1.1/messageformat2/runtime.py +0 -301
  60. messageformat2-0.1.1/tests/data/complex_message_3.txt +0 -22
  61. messageformat2-0.1.1/tests/data/unsupported_annotations_1.txt +0 -4
  62. messageformat2-0.1.1/tests/data/unsupported_annotations_2.txt +0 -4
  63. messageformat2-0.1.1/tests/data/unsupported_annotations_3.txt +0 -3
  64. messageformat2-0.1.1/tests/data/unsupported_annotations_4.txt +0 -6
  65. messageformat2-0.1.1/tests/data/unsupported_annotations_5.txt +0 -8
  66. messageformat2-0.1.1/tests/data/unsupported_statement_1.txt +0 -15
  67. messageformat2-0.1.1/tests/data/unsupported_statement_2.txt +0 -21
  68. messageformat2-0.1.1/tests/test_parser.py +0 -235
  69. messageformat2-0.1.1/tests/test_runtime.py +0 -242
  70. messageformat2-0.1.1/uv.lock +0 -776
  71. {messageformat2-0.1.1 → messageformat2-1.1.0}/.github/dependabot.yml +0 -0
  72. {messageformat2-0.1.1 → messageformat2-1.1.0}/.gitignore +0 -0
  73. {messageformat2-0.1.1 → messageformat2-1.1.0}/CONTRIBUTING.md +0 -0
  74. {messageformat2-0.1.1 → messageformat2-1.1.0}/LICENSE +0 -0
  75. {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/datamodel.md +0 -0
  76. {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/errors.md +0 -0
  77. {messageformat2-0.1.1 → messageformat2-1.1.0}/docs/message.md +0 -0
  78. {messageformat2-0.1.1 → messageformat2-1.1.0}/messageformat2/cli.py +0 -0
  79. {messageformat2-0.1.1 → messageformat2-1.1.0}/mkdocs.yml +0 -0
  80. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/__init__.py +0 -0
  81. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/complex_message_1.txt +0 -0
  82. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_1.txt +0 -0
  83. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_10.txt +0 -0
  84. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_11.txt +0 -0
  85. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_12.txt +0 -0
  86. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_13.txt +0 -0
  87. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_14.txt +0 -0
  88. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_15.txt +0 -0
  89. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_16.txt +0 -0
  90. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_2.txt +0 -0
  91. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_3.txt +0 -0
  92. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_4.txt +0 -0
  93. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_5.txt +0 -0
  94. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_6.txt +0 -0
  95. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_7.txt +0 -0
  96. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_8.txt +0 -0
  97. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_escapes_1.txt +0 -0
  98. {messageformat2-0.1.1 → messageformat2-1.1.0}/tests/data/simple_message_escapes_2.txt +0 -0
@@ -11,10 +11,10 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- python-version: ["3.12", "3.13", "3.14"]
14
+ python-version: ["3.12", "3.13", "3.14", "3.15"]
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@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.1.0
4
4
  Summary: Python implementation of Unicode MessageFormat 2.0
5
5
  Project-URL: Homepage, https://github.com/tomasr8/messageformat2
6
6
  Project-URL: Github, https://github.com/tomasr8/messageformat2
7
+ Project-URL: Documentation, https://messageformat2.readthedocs.io/en/latest/
7
8
  Author-email: Tomas Roun <tomas.roun8@gmail.com>
8
9
  License: MIT License
9
10
 
@@ -28,42 +29,46 @@ License: MIT License
28
29
  SOFTWARE.
29
30
  License-File: LICENSE
30
31
  Keywords: cldr,gettext,i18n,internationalization,localization,messageformat,messageformat 2
31
- Classifier: License :: OSI Approved :: MIT License
32
32
  Classifier: Operating System :: OS Independent
33
33
  Classifier: Programming Language :: Python :: 3
34
34
  Classifier: Programming Language :: Python :: 3.12
35
- Classifier: 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
 
44
46
 
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
- 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).
49
+ This is a Python implementation of the [Unicode MessageFormat
50
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
49
51
 
50
- Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
51
- more expressive and flexible than both Message Format 1.0 and, in the context of
52
+ Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
53
+ more expressive and flexible than both MessageFormat 1.0 and, in the context of
52
54
  Python, gettext.
53
55
 
54
56
  If you would like to learn more about MF2 itself head over to the [Unicode technical
55
- specification](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html).
57
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
56
58
 
57
59
  ## Features
58
60
 
59
61
  The library includes:
60
62
 
61
- - A message parser and a formatter
62
- - Several builtin formatters and selectors described by the
63
- [spec](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#function-registry)
63
+ - A message parser and a formatter, passing the official
64
+ [test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
65
+ - All default functions described by the
66
+ [spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
67
+ (`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
64
68
  and the possibility to write custom formatters/selectors
69
+ - Formatting with fallback values and error reporting, optional bidi isolation of placeholders
65
70
  - Helper classes to inspect and transform the message [data
66
- model](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model)
71
+ model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
67
72
 
68
73
  ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
69
74
 
@@ -100,15 +105,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
100
105
  There are several builtin locale-aware formatters available.
101
106
 
102
107
  ```python
103
- from datetime import datetime
108
+ from datetime import date
104
109
  from messageformat2 import Message
105
110
 
106
- message = Message("Today's date is {$now :date}")
107
- now = datetime.now()
108
- message.format({"now": now}, locale='en_US')
109
- # -> Today's date is 5/10/2024
110
- message.format({"now": now}, locale='en_GB')
111
- # -> Today's date is 10/05/2024
111
+ message = Message("Today's date is {$today :date}")
112
+ today = date(2024, 5, 10)
113
+ message.format({"today": today}, locale="en_US")
114
+ # -> "Today's date is May 10, 2024"
115
+ message.format({"today": today}, locale="en_GB")
116
+ # -> "Today's date is 10 May 2024"
117
+
118
+ Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
119
+ # -> "42,00\xa0€"
112
120
  ```
113
121
 
114
122
  ### Plural support
@@ -119,7 +127,8 @@ MF2 supports pluralization using built-in or custom selectors.
119
127
  from messageformat2 import Message
120
128
 
121
129
  msg = Message ("""\
122
- .match {$count :number}
130
+ .input {$count :number}
131
+ .match $count
123
132
  one {{You have {$count} notification.}}
124
133
  * {{You have {$count} notifications.}}
125
134
  """)
@@ -128,4 +137,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
128
137
  msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
129
138
  ```
130
139
 
140
+ ### Error handling
141
+
142
+ By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
143
+ missing variable, etc.) and collect the errors, pass an `on_error` callback:
144
+
145
+ ```python
146
+ errors = []
147
+ Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
148
+ errors # -> [UnresolvedVariable('Unresolved variable: name')]
149
+ ```
150
+
151
+ ### Bidirectional text
152
+
153
+ The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
154
+ text displays correctly) is available with `bidi_isolation=True`:
155
+
156
+ ```python
157
+ Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
158
+ # -> "Hello, \u2068Alice\u2069!"
159
+ ```
160
+
131
161
  Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
@@ -0,0 +1,117 @@
1
+
2
+
3
+ # MessageFormat2 [![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
+
5
+ This is a Python implementation of the [Unicode MessageFormat
6
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
7
+
8
+ Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
9
+ more expressive and flexible than both MessageFormat 1.0 and, in the context of
10
+ Python, gettext.
11
+
12
+ If you would like to learn more about MF2 itself head over to the [Unicode technical
13
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
14
+
15
+ ## Features
16
+
17
+ The library includes:
18
+
19
+ - A message parser and a formatter, passing the official
20
+ [test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
21
+ - All default functions described by the
22
+ [spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
23
+ (`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
24
+ and the possibility to write custom formatters/selectors
25
+ - Formatting with fallback values and error reporting, optional bidi isolation of placeholders
26
+ - Helper classes to inspect and transform the message [data
27
+ model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
28
+
29
+ ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
30
+
31
+ ## Installation
32
+
33
+ Via pip for Python `>= 3.12`
34
+
35
+ ```sh
36
+ pip install messageformat2
37
+ ```
38
+
39
+ ## Examples
40
+
41
+ ### Simple messages
42
+
43
+ ```python
44
+ from messageformat2 import format_message
45
+
46
+ format_message("Hello, {$name}!", {"name": "Alice"}) # -> "Hello, Alice!"
47
+ ```
48
+
49
+ Messages can be reused with different inputs.
50
+
51
+ ```python
52
+ from messageformat2 import Message
53
+
54
+ message = Message("Hello, {$name}!")
55
+ message.format({"name": "Alice"}) # -> "Hello, Alice!"
56
+ message.format({"name": "Bob"}) # -> "Hello, Bob!"
57
+ ```
58
+
59
+ ### Built-in formatters
60
+
61
+ There are several builtin locale-aware formatters available.
62
+
63
+ ```python
64
+ from datetime import date
65
+ from messageformat2 import Message
66
+
67
+ message = Message("Today's date is {$today :date}")
68
+ today = date(2024, 5, 10)
69
+ message.format({"today": today}, locale="en_US")
70
+ # -> "Today's date is May 10, 2024"
71
+ message.format({"today": today}, locale="en_GB")
72
+ # -> "Today's date is 10 May 2024"
73
+
74
+ Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
75
+ # -> "42,00\xa0€"
76
+ ```
77
+
78
+ ### Plural support
79
+
80
+ MF2 supports pluralization using built-in or custom selectors.
81
+
82
+ ```python
83
+ from messageformat2 import Message
84
+
85
+ msg = Message ("""\
86
+ .input {$count :number}
87
+ .match $count
88
+ one {{You have {$count} notification.}}
89
+ * {{You have {$count} notifications.}}
90
+ """)
91
+
92
+ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
93
+ msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
94
+ ```
95
+
96
+ ### Error handling
97
+
98
+ By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
99
+ missing variable, etc.) and collect the errors, pass an `on_error` callback:
100
+
101
+ ```python
102
+ errors = []
103
+ Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
104
+ errors # -> [UnresolvedVariable('Unresolved variable: name')]
105
+ ```
106
+
107
+ ### Bidirectional text
108
+
109
+ The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
110
+ text displays correctly) is available with `bidi_isolation=True`:
111
+
112
+ ```python
113
+ Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
114
+ # -> "Hello, \u2068Alice\u2069!"
115
+ ```
116
+
117
+ Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
@@ -0,0 +1,204 @@
1
+ The default function registry includes these functions
2
+
3
+ in line with the [specification](https://github.com/unicode-org/message-format-wg/tree/main/spec/functions)
4
+
5
+ ### `:string`
6
+
7
+ #### Formatter
8
+
9
+ As a formatter, this function simply calls `str(value)`.
10
+
11
+ #### Selector
12
+
13
+ As a selector, this function selects the exact match among the variant keys
14
+ (after NFC normalization of the value).
15
+
16
+ ```python
17
+ from messageformat2 import format_message
18
+
19
+ format_message("""\
20
+ .input {$pet :string}
21
+ .match $pet
22
+ dog {{I have a dog}}
23
+ cat {{I have a cat}}
24
+ * {{I have a {$pet}}}
25
+ """, {"pet": "cat"})
26
+ # -> "I have a cat"
27
+ ```
28
+
29
+ ### `:number`
30
+
31
+ The operand must be an `int`, `float` or `Decimal` (but not a `bool`), or a string matching the
32
+ `number-literal` production of the spec (e.g. `|-1234.5|`, `|1e3|`; `|01|` or `|.5|` are not numbers).
33
+ The resolved value of another numeric function (`:number`, `:integer`, `:offset`, `:percent`) is also accepted,
34
+ in which case its options are inherited, with the options of the expression taking priority.
35
+
36
+ #### Formatter
37
+
38
+ - `signDisplay`
39
+ - auto (default)
40
+ - always
41
+ - exceptZero
42
+ - negative
43
+ - never
44
+ - `useGrouping`
45
+ - auto (default)
46
+ - always
47
+ - never
48
+ - min2 (grouping separators only for numbers with at least 5 integer digits)
49
+ - `minimumIntegerDigits`
50
+ - `minimumFractionDigits`
51
+ - `maximumFractionDigits`
52
+ - `minimumSignificantDigits`
53
+ - `maximumSignificantDigits`
54
+ - `roundingMode`
55
+ - ceil
56
+ - floor
57
+ - expand
58
+ - trunc
59
+ - halfCeil
60
+ - halfFloor
61
+ - halfExpand (default; ties away from zero, like `Intl.NumberFormat` and unlike Python's `round()`)
62
+ - halfTrunc
63
+ - halfEven
64
+ - `roundingPriority`: which of the fraction and significant digits options wins when both are given
65
+ - auto (default; the significant digits options)
66
+ - morePrecision
67
+ - lessPrecision
68
+ - `roundingIncrement`: 1 (default), 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500 or 5000,
69
+ in units of the last fraction digit; requires `minimumFractionDigits` and `maximumFractionDigits` to be equal
70
+ - `trailingZeroDisplay`
71
+ - auto (default)
72
+ - stripIfInteger (no fraction digits when the rounded value is an integer)
73
+
74
+ The digit options are *digit size options*: small non-negative integers (`0`-`99`).
75
+ The numbering system is the default one of the locale (e.g. `Locale("ar", "EG")` uses Arabic-Indic digits).
76
+
77
+ #### Selector
78
+
79
+ The selector takes the same options as the formatter in addition to:
80
+
81
+ - `select`
82
+ - plural (default)
83
+ - ordinal
84
+ - exact
85
+
86
+ `select` must be set by a literal. The selector first tries an exact match between the number and
87
+ numeric keys (e.g. `1`), then (unless `select=exact`) matches the CLDR plural or ordinal category of the number
88
+ (`zero`, `one`, `two`, `few`, `many`, `other`). Any other key is a `BadVariantKey` error.
89
+
90
+ ### `:integer`
91
+
92
+ Same as `:number`, but the operand is rounded to an integer (`halfExpand`: `1.5` becomes `2`, `-2.5` becomes `-3`).
93
+ The `minimumFractionDigits`, `maximumFractionDigits` and `minimumSignificantDigits` options of an annotated operand
94
+ are discarded.
95
+
96
+ ### `:offset`
97
+
98
+ Adds or subtracts a small integer from a numeric operand, e.g. to count "other" users:
99
+
100
+ ```
101
+ .input {$likes :integer}
102
+ .local $others = {$likes :offset subtract=1}
103
+ ```
104
+
105
+ Exactly one of the options must be given:
106
+
107
+ - `add`
108
+ - `subtract`
109
+
110
+ The formatting options of an annotated operand (e.g. `signDisplay` of `{$likes :integer signDisplay=always}`)
111
+ are inherited. `:offset` can also be used as a selector, in which case it behaves like `:number`.
112
+
113
+ ### `:percent`
114
+
115
+ Formats a numeric operand multiplied by 100 as a percentage, e.g. `{0.25 :percent}` → `25%`.
116
+ It takes the same options as `:number` except `select`, `minimumIntegerDigits` and `roundingIncrement`
117
+ (by default no fraction digits are shown). As a selector it always uses the plural rules, on the value
118
+ multiplied by 100. The resolved value of a `:percent` expression keeps the original (unmultiplied) value,
119
+ so `.local $p = {0.25 :percent} {{{$p :number}}}` gives `0.25`.
120
+
121
+ ### `:currency`
122
+
123
+ Formats a currency amount, e.g. `{42 :currency currency=EUR}` → `€42.00`. The operand is either a numeric
124
+ operand together with the `currency` option, or a currency value: a `messageformat2.builtins.Currency(value, currency)`
125
+ (or any mapping with the keys `value` and `currency`) passed as an input. The currency of a currency value cannot
126
+ be overridden by the `currency` option (`BadOption`). `:currency` does not support selection.
127
+
128
+ - `currency`: a three-letter currency code (case-insensitive); required for numeric operands
129
+ - `currencySign`
130
+ - standard (default)
131
+ - accounting (e.g. `($42.50)`)
132
+ - `currencyDisplay`
133
+ - symbol (default)
134
+ - narrowSymbol (treated as symbol)
135
+ - code (`USD42.00`)
136
+ - name (`42.00 US dollars`)
137
+ - never (`42.00`)
138
+ - `useGrouping`, `minimumIntegerDigits`, `minimumSignificantDigits`, `maximumSignificantDigits`, `roundingMode`,
139
+ `roundingPriority`, `roundingIncrement`, `trailingZeroDisplay`: as for `:number`
140
+ - `fractionDigits`
141
+ - auto (default): the number of fraction digits of the currency (2 for USD, 0 for JPY, ...)
142
+ - a *digit size option*: a fixed number of fraction digits
143
+
144
+ ### `:datetime`, `:date`, `:time`
145
+
146
+ The operand must be a `datetime` (or a `date`/`time` for `:date`/`:time` respectively) or an ISO 8601 date or
147
+ datetime string that can be parsed with `datetime.fromisoformat`, e.g. `|2006-01-02|` or `|2006-01-02T15:04:06+01:00|`.
148
+ The resolved value of another date/time function is also accepted, in which case its override options
149
+ (`timeZone`, `hour12`) are inherited.
150
+
151
+ The option values below must be set by literals (an option set by a variable is a `BadOption` error).
152
+
153
+ #### `:datetime`
154
+
155
+ - `dateFields`
156
+ - weekday
157
+ - day-weekday
158
+ - month-day
159
+ - month-day-weekday
160
+ - year-month-day (default)
161
+ - year-month-day-weekday
162
+ - `dateLength`
163
+ - long
164
+ - medium (default)
165
+ - short
166
+ - `timePrecision`
167
+ - hour
168
+ - minute (default)
169
+ - second
170
+ - `timeZoneStyle`: no time zone is shown unless set
171
+ - long
172
+ - short
173
+
174
+ #### `:date`
175
+
176
+ - `fields`: same values as `dateFields` above
177
+ - `length`: same values as `dateLength` above
178
+
179
+ #### `:time`
180
+
181
+ - `precision`: same values as `timePrecision` above
182
+ - `timeZoneStyle`: same values as above
183
+
184
+ #### Override options
185
+
186
+ - `timeZone`: `UTC`, `input` (the time zone of the operand, which must then have one), an IANA time zone
187
+ name (`|Europe/Zurich|`) or an offset (`|+01:00|`). A naive `datetime` is interpreted in this time zone,
188
+ an aware one is converted to it.
189
+ - `hour12`: `true` or `false` (`:datetime` and `:time` only); the default is locale-dependent.
190
+ - `calendar`: not implemented (ignored).
191
+
192
+ #### Selector
193
+
194
+ The date/time functions do not support selection, as it is not required by MF2.
195
+
196
+ ## The `u:` namespace options
197
+
198
+ These options can be used with any function (including custom ones):
199
+
200
+ - `u:dir`: `ltr`, `rtl`, `auto` or `inherit` (default). Sets the directionality of the placeholder for the
201
+ bidi isolation strategy and forces its isolation (except `inherit`). Not allowed on markup.
202
+ - `u:id`: an identifier for the placeholder; accepted (it must be a string) but ignored when formatting to a string.
203
+
204
+ Both are removed from the options before the function is called.
@@ -0,0 +1,95 @@
1
+ ## Runtime model
2
+
3
+ All expressions are lazily evaluated at the time of use.
4
+ If the same variable is referenced more than once, it will also be evaluated more than once.
5
+ Pay attention when providing custom formatters/selectors with side effects:
6
+
7
+ ```python
8
+ from messageformat2 import Message
9
+
10
+ counter = 0
11
+
12
+ def formatter_with_side_effect(value, locale, options):
13
+ global counter
14
+ counter += 1
15
+ return str(value)
16
+
17
+ msg = Message("""\
18
+ .local $foo = {|foo| :side_effect}
19
+ {{{$foo} {$foo}}}
20
+ """)
21
+ msg.format(formatters={"side_effect": formatter_with_side_effect})
22
+ print(counter) # -> 2
23
+ ```
24
+
25
+ ## Unannotated variables
26
+
27
+ A placeholder consisting only of a variable (`{$x}`) is formatted according to the type of its value:
28
+ numbers (`int`, `float`, `Decimal`) as if annotated with `:number`, `datetime`, `date` and `time` values with
29
+ `:datetime`, `:date` and `:time`. Anything else is formatted with `str()`.
30
+ Literals are always strings, so `{|1234.5|}` is formatted verbatim.
31
+
32
+ ## Bidirectional text
33
+
34
+ With `bidi_isolation=True`, placeholders are wrapped in Unicode isolate controls following the spec's
35
+ *Default Bidi Strategy*, so that mixed left-to-right and right-to-left text displays correctly: a value whose
36
+ direction is unknown (strings, literals, the results of custom formatters) is wrapped in U+2068 FIRST STRONG
37
+ ISOLATE … U+2069 POP DIRECTIONAL ISOLATE, a value whose direction differs from the message's (e.g. a number,
38
+ which has the direction of the locale, in a message formatted in an RTL locale) in U+2066 LEFT-TO-RIGHT ISOLATE /
39
+ U+2067 RIGHT-TO-LEFT ISOLATE.
40
+
41
+ ```python
42
+ Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True) # -> "Hello, \u2068Alice\u2069!"
43
+ Message("{$n :number} items").format({"n": 3}, bidi_isolation=True) # -> "3 items" (same direction)
44
+ ```
45
+
46
+ The controls are invisible but do change the string, which is why isolation is off by default.
47
+ The `u:dir` option sets the direction of a placeholder (`ltr`, `rtl` or `auto`) and forces its isolation:
48
+ `{$name :string u:dir=rtl}`. Custom formatters can return a `messageformat2.builtins.FormattedValue(text, direction)`
49
+ to declare the direction of their output.
50
+
51
+ ## Function composition
52
+
53
+ A variable which was annotated with a function can be annotated again, for example in a placeholder.
54
+ The function handler then receives the resolved value of the earlier annotation
55
+ (a `messageformat2.builtins.LazyValue` carrying the earlier function's name, operand and options) as its operand,
56
+ and decides what to do with it. The built-in numeric functions accept each other's resolved values
57
+ and inherit their options, with the options of the later expression taking priority:
58
+
59
+ ```python
60
+ from messageformat2 import format_message
61
+
62
+ format_message(".local $x = {42 :number minimumFractionDigits=2 signDisplay=always} {{x = {$x :number minimumFractionDigits=1}}}")
63
+ # -> "x = +42.0"
64
+ format_message(".local $x = {1.25 :integer} {{x = {$x :number}}}")
65
+ # -> "x = 1"
66
+ ```
67
+
68
+ whereas e.g. `{$x :date}` would raise `BadOperand`. See [Custom formatters](howto/formatter.md) for how to
69
+ handle such operands in your own functions.
70
+
71
+ ## Whitespace
72
+
73
+ This library implements the [grammar](https://github.com/unicode-org/message-format-wg/blob/main/spec/message.abnf) exactly as specified which includes whitespace.
74
+
75
+ Whitespace around a complex message (a message containing declarations and/or a match block) is not significant,
76
+ so complex messages can be freely indented, e.g. in multiline strings:
77
+
78
+ ```python
79
+ from messageformat2 import Message
80
+
81
+ Message("""
82
+ .input {$count :integer}
83
+ .match $count
84
+ * {{count is {$count}}}
85
+ """)
86
+ ```
87
+
88
+ Whitespace in a simple message (a message consisting of a single pattern) is always significant
89
+ and is part of the message text. Note that the first non-whitespace character of a simple message
90
+ cannot be a `.`, so a misspelled keyword (`.imput`) is a syntax error rather than silently becoming text.
91
+ To get a pattern which starts with a `.`, use a quoted pattern:
92
+
93
+ ```python
94
+ Message("{{.input is not a keyword here}}")
95
+ ```