messageformat2 1.0.0__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 (79) hide show
  1. {messageformat2-1.0.0 → messageformat2-1.1.0}/PKG-INFO +42 -17
  2. {messageformat2-1.0.0 → messageformat2-1.1.0}/README.md +41 -16
  3. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/builtins.md +70 -8
  4. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/explanation.md +33 -9
  5. messageformat2-1.1.0/docs/getting_started.md +85 -0
  6. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/datamodel.md +1 -1
  7. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/errors.md +2 -2
  8. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/formatter.md +22 -1
  9. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/howto/selector.md +18 -14
  10. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/index.md +9 -6
  11. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/__init__.py +1 -1
  12. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/builtins.py +396 -58
  13. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/message.py +20 -4
  14. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/runtime.py +136 -67
  15. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/README.md +1 -2
  16. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_builtins.py +181 -3
  17. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_runtime.py +95 -1
  18. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_spec.py +10 -9
  19. messageformat2-1.0.0/docs/getting_started.md +0 -59
  20. messageformat2-1.0.0/docs/mf2.md +0 -13
  21. {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/dependabot.yml +0 -0
  22. {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/workflows/ci.yml +0 -0
  23. {messageformat2-1.0.0 → messageformat2-1.1.0}/.github/workflows/release.yml +0 -0
  24. {messageformat2-1.0.0 → messageformat2-1.1.0}/.gitignore +0 -0
  25. {messageformat2-1.0.0 → messageformat2-1.1.0}/.readthedocs.yaml +0 -0
  26. {messageformat2-1.0.0 → messageformat2-1.1.0}/CONTRIBUTING.md +0 -0
  27. {messageformat2-1.0.0 → messageformat2-1.1.0}/LICENSE +0 -0
  28. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/datamodel.md +0 -0
  29. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/errors.md +0 -0
  30. {messageformat2-1.0.0 → messageformat2-1.1.0}/docs/message.md +0 -0
  31. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/cli.py +0 -0
  32. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/datamodel.py +0 -0
  33. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/errors.py +0 -0
  34. {messageformat2-1.0.0 → messageformat2-1.1.0}/messageformat2/parser.py +0 -0
  35. {messageformat2-1.0.0 → messageformat2-1.1.0}/mkdocs.yml +0 -0
  36. {messageformat2-1.0.0 → messageformat2-1.1.0}/pyproject.toml +0 -0
  37. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/__init__.py +0 -0
  38. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_1.txt +0 -0
  39. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_2.txt +0 -0
  40. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/complex_message_3.txt +0 -0
  41. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_1.txt +0 -0
  42. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_10.txt +0 -0
  43. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_11.txt +0 -0
  44. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_12.txt +0 -0
  45. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_13.txt +0 -0
  46. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_14.txt +0 -0
  47. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_15.txt +0 -0
  48. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_16.txt +0 -0
  49. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_17.txt +0 -0
  50. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_2.txt +0 -0
  51. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_3.txt +0 -0
  52. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_4.txt +0 -0
  53. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_5.txt +0 -0
  54. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_6.txt +0 -0
  55. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_7.txt +0 -0
  56. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_8.txt +0 -0
  57. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_9.txt +0 -0
  58. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_1.txt +0 -0
  59. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_2.txt +0 -0
  60. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/data/simple_message_escapes_3.txt +0 -0
  61. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/bidi.json +0 -0
  62. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/data-model-errors.json +0 -0
  63. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/fallback.json +0 -0
  64. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/currency.json +0 -0
  65. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/date.json +0 -0
  66. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/datetime.json +0 -0
  67. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/integer.json +0 -0
  68. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/number.json +0 -0
  69. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/offset.json +0 -0
  70. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/percent.json +0 -0
  71. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/string.json +0 -0
  72. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/functions/time.json +0 -0
  73. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/pattern-selection.json +0 -0
  74. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/syntax-errors.json +0 -0
  75. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/syntax.json +0 -0
  76. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/spec/u-options.json +0 -0
  77. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_parser.py +0 -0
  78. {messageformat2-1.0.0 → messageformat2-1.1.0}/tests/test_regex.py +0 -0
  79. {messageformat2-1.0.0 → messageformat2-1.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: messageformat2
3
- Version: 1.0.0
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
@@ -46,11 +46,11 @@ Description-Content-Type: text/markdown
46
46
 
47
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)
48
48
 
49
- This is a Python implementation of the [Unicode Message Format 2.0
50
- specification](https://www.unicode.org/reports/tr35/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).
51
51
 
52
- Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
53
- 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
54
54
  Python, gettext.
55
55
 
56
56
  If you would like to learn more about MF2 itself head over to the [Unicode technical
@@ -60,16 +60,17 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
60
60
 
61
61
  The library includes:
62
62
 
63
- - A message parser and a formatter
64
- - Several builtin formatters and selectors described by the
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
65
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`)
66
68
  and the possibility to write custom formatters/selectors
69
+ - Formatting with fallback values and error reporting, optional bidi isolation of placeholders
67
70
  - Helper classes to inspect and transform the message [data
68
71
  model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
69
72
 
70
- ## Documentation
71
-
72
- Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
73
+ ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
73
74
 
74
75
  ## Installation
75
76
 
@@ -104,15 +105,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
104
105
  There are several builtin locale-aware formatters available.
105
106
 
106
107
  ```python
107
- from datetime import datetime
108
+ from datetime import date
108
109
  from messageformat2 import Message
109
110
 
110
- message = Message("Today's date is {$now :date}")
111
- now = datetime.now()
112
- message.format({"now": now}, locale='en_US')
113
- # -> Today's date is 5/10/2024
114
- message.format({"now": now}, locale='en_GB')
115
- # -> 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€"
116
120
  ```
117
121
 
118
122
  ### Plural support
@@ -133,4 +137,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
133
137
  msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
134
138
  ```
135
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
+
136
161
  Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
@@ -2,11 +2,11 @@
2
2
 
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
- This is a Python implementation of the [Unicode Message Format 2.0
6
- specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
5
+ This is a Python implementation of the [Unicode MessageFormat
6
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
7
7
 
8
- Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
9
- more expressive and flexible than both Message Format 1.0 and, in the context of
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
10
  Python, gettext.
11
11
 
12
12
  If you would like to learn more about MF2 itself head over to the [Unicode technical
@@ -16,16 +16,17 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
16
16
 
17
17
  The library includes:
18
18
 
19
- - A message parser and a formatter
20
- - Several builtin formatters and selectors described by the
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
21
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`)
22
24
  and the possibility to write custom formatters/selectors
25
+ - Formatting with fallback values and error reporting, optional bidi isolation of placeholders
23
26
  - Helper classes to inspect and transform the message [data
24
27
  model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
25
28
 
26
- ## Documentation
27
-
28
- Documentation is available here: [https://messageformat2.readthedocs.io/en/latest/](https://messageformat2.readthedocs.io/en/latest/)
29
+ ## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
29
30
 
30
31
  ## Installation
31
32
 
@@ -60,15 +61,18 @@ message.format({"name": "Bob"}) # -> "Hello, Bob!"
60
61
  There are several builtin locale-aware formatters available.
61
62
 
62
63
  ```python
63
- from datetime import datetime
64
+ from datetime import date
64
65
  from messageformat2 import Message
65
66
 
66
- message = Message("Today's date is {$now :date}")
67
- now = datetime.now()
68
- message.format({"now": now}, locale='en_US')
69
- # -> Today's date is 5/10/2024
70
- message.format({"now": now}, locale='en_GB')
71
- # -> Today's date is 10/05/2024
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€"
72
76
  ```
73
77
 
74
78
  ### Plural support
@@ -89,4 +93,25 @@ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
89
93
  msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
90
94
  ```
91
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
+
92
117
  Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
@@ -14,6 +14,8 @@ As a selector, this function selects the exact match among the variant keys
14
14
  (after NFC normalization of the value).
15
15
 
16
16
  ```python
17
+ from messageformat2 import format_message
18
+
17
19
  format_message("""\
18
20
  .input {$pet :string}
19
21
  .match $pet
@@ -28,8 +30,8 @@ cat {{I have a cat}}
28
30
 
29
31
  The operand must be an `int`, `float` or `Decimal` (but not a `bool`), or a string matching the
30
32
  `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
+ 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.
33
35
 
34
36
  #### Formatter
35
37
 
@@ -43,16 +45,34 @@ case its options are inherited, with the options of the expression taking priori
43
45
  - auto (default)
44
46
  - always
45
47
  - never
46
- - min2 (treated as auto)
48
+ - min2 (grouping separators only for numbers with at least 5 integer digits)
47
49
  - `minimumIntegerDigits`
48
50
  - `minimumFractionDigits`
49
51
  - `maximumFractionDigits`
50
52
  - `minimumSignificantDigits`
51
53
  - `maximumSignificantDigits`
52
- - `numberingSystem` (implementation extension): a Unicode Number System Identifier
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)
53
73
 
54
74
  The digit options are *digit size options*: small non-negative integers (`0`-`99`).
55
- Not implemented: `trailingZeroDisplay`, `roundingPriority`, `roundingIncrement`, `roundingMode` (these are ignored).
75
+ The numbering system is the default one of the locale (e.g. `Locale("ar", "EG")` uses Arabic-Indic digits).
56
76
 
57
77
  #### Selector
58
78
 
@@ -69,14 +89,15 @@ numeric keys (e.g. `1`), then (unless `select=exact`) matches the CLDR plural or
69
89
 
70
90
  ### `:integer`
71
91
 
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.
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.
74
95
 
75
96
  ### `:offset`
76
97
 
77
98
  Adds or subtracts a small integer from a numeric operand, e.g. to count "other" users:
78
99
 
79
- ```python
100
+ ```
80
101
  .input {$likes :integer}
81
102
  .local $others = {$likes :offset subtract=1}
82
103
  ```
@@ -89,6 +110,37 @@ Exactly one of the options must be given:
89
110
  The formatting options of an annotated operand (e.g. `signDisplay` of `{$likes :integer signDisplay=always}`)
90
111
  are inherited. `:offset` can also be used as a selector, in which case it behaves like `:number`.
91
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
+
92
144
  ### `:datetime`, `:date`, `:time`
93
145
 
94
146
  The operand must be a `datetime` (or a `date`/`time` for `:date`/`:time` respectively) or an ISO 8601 date or
@@ -140,3 +192,13 @@ The option values below must be set by literals (an option set by a variable is
140
192
  #### Selector
141
193
 
142
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.
@@ -5,18 +5,20 @@ If the same variable is referenced more than once, it will also be evaluated mor
5
5
  Pay attention when providing custom formatters/selectors with side effects:
6
6
 
7
7
  ```python
8
+ from messageformat2 import Message
9
+
8
10
  counter = 0
9
11
 
10
12
  def formatter_with_side_effect(value, locale, options):
11
- nonlocal counter
12
- counter +=1
13
+ global counter
14
+ counter += 1
13
15
  return str(value)
14
16
 
15
- msg = Message("\
17
+ msg = Message("""\
16
18
  .local $foo = {|foo| :side_effect}
17
19
  {{{$foo} {$foo}}}
18
- ")
19
- msg.format(formatters={'side_effect': formatter_with_side_effect})
20
+ """)
21
+ msg.format(formatters={"side_effect": formatter_with_side_effect})
20
22
  print(counter) # -> 2
21
23
  ```
22
24
 
@@ -27,6 +29,25 @@ numbers (`int`, `float`, `Decimal`) as if annotated with `:number`, `datetime`,
27
29
  `:datetime`, `:date` and `:time`. Anything else is formatted with `str()`.
28
30
  Literals are always strings, so `{|1234.5|}` is formatted verbatim.
29
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
+
30
51
  ## Function composition
31
52
 
32
53
  A variable which was annotated with a function can be annotated again, for example in a placeholder.
@@ -36,11 +57,12 @@ and decides what to do with it. The built-in numeric functions accept each other
36
57
  and inherit their options, with the options of the later expression taking priority:
37
58
 
38
59
  ```python
39
- .local $x = {42 :number minimumFractionDigits=2 signDisplay=always}
40
- {{x = {$x :number minimumFractionDigits=1}}} # -> "x = +42.0"
60
+ from messageformat2 import format_message
41
61
 
42
- .local $x = {1.25 :integer}
43
- {{x = {$x :number}}} # -> "x = 1"
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"
44
66
  ```
45
67
 
46
68
  whereas e.g. `{$x :date}` would raise `BadOperand`. See [Custom formatters](howto/formatter.md) for how to
@@ -54,6 +76,8 @@ Whitespace around a complex message (a message containing declarations and/or a
54
76
  so complex messages can be freely indented, e.g. in multiline strings:
55
77
 
56
78
  ```python
79
+ from messageformat2 import Message
80
+
57
81
  Message("""
58
82
  .input {$count :integer}
59
83
  .match $count
@@ -0,0 +1,85 @@
1
+ ## Installation
2
+
3
+ This package is available via `pip` for Python `>= 3.12`:
4
+
5
+ ```sh
6
+ pip install messageformat2
7
+ ```
8
+
9
+ ## Simple messages
10
+
11
+ ```python
12
+ from messageformat2 import format_message
13
+
14
+ format_message("Hello, {$name}!", {"name": "Alice"}) # -> "Hello, Alice!"
15
+ ```
16
+
17
+ Messages can be reused with different inputs.
18
+
19
+ ```python
20
+ from messageformat2 import Message
21
+
22
+ message = Message("Hello, {$name}!")
23
+ message.format({"name": "Alice"}) # -> "Hello, Alice!"
24
+ message.format({"name": "Bob"}) # -> "Hello, Bob!"
25
+ ```
26
+
27
+ ## Built-in formatters
28
+
29
+ There are several builtin locale-aware [formatters](builtins.md) available.
30
+
31
+ ```python
32
+ from datetime import date
33
+ from messageformat2 import Message
34
+
35
+ message = Message("Today's date is {$today :date}")
36
+ today = date(2024, 5, 10)
37
+ message.format({"today": today}, locale="en_US")
38
+ # -> "Today's date is May 10, 2024"
39
+ message.format({"today": today}, locale="en_GB")
40
+ # -> "Today's date is 10 May 2024"
41
+
42
+ Message("{$price :currency currency=EUR}").format({"price": 42}, locale="de")
43
+ # -> "42,00\xa0€"
44
+ ```
45
+
46
+ ## Plural support
47
+
48
+ MF2 supports pluralization using built-in or custom selectors.
49
+
50
+ ```python
51
+ from messageformat2 import Message
52
+
53
+ msg = Message ("""\
54
+ .input {$count :number}
55
+ .match $count
56
+ one {{You have {$count} notification.}}
57
+ * {{You have {$count} notifications.}}
58
+ """)
59
+
60
+ msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
61
+ msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
62
+ ```
63
+
64
+ ## Error handling
65
+
66
+ By default the first error is raised. To get the spec's fallback behaviour instead (`{$name}` for a
67
+ missing variable, etc.) and collect the errors, pass an `on_error` callback,
68
+ see [Error handling](howto/errors.md):
69
+
70
+ ```python
71
+ errors = []
72
+ Message("Hello, {$name}!").format({}, on_error=errors.append) # -> "Hello, {$name}!"
73
+ errors # -> [UnresolvedVariable('Unresolved variable: name')]
74
+ ```
75
+
76
+ ## Bidirectional text
77
+
78
+ The spec's *Default Bidi Strategy* (wrapping placeholders in Unicode isolate controls so that mixed-direction
79
+ text displays correctly) is available with `bidi_isolation=True`,
80
+ see [Bidirectional text](explanation.md#bidirectional-text):
81
+
82
+ ```python
83
+ Message("Hello, {$name}!").format({"name": "Alice"}, bidi_isolation=True)
84
+ # -> "Hello, \u2068Alice\u2069!"
85
+ ```
@@ -1,7 +1,7 @@
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
3
  spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md).
4
- The data model nodes are documented [here](/datamodel).
4
+ The data model nodes are documented [here](../datamodel.md).
5
5
 
6
6
  There are two classes to work with the datamodel - `DataModelVisitor` &
7
7
  `DataModelTransformer`.
@@ -22,7 +22,7 @@ from messageformat2.errors import ParseError, DataModelError
22
22
  try:
23
23
  Message("}} invalid syntax")
24
24
  except ParseError as e:
25
- print(e) # -> Invalid syntax
25
+ print(e) # -> Invalid character at the start of a simple message: }
26
26
 
27
27
 
28
28
  try:
@@ -45,7 +45,7 @@ msg = Message("Hello, {$name}!")
45
45
  try:
46
46
  msg.format()
47
47
  except FormatError as e:
48
- print(e) # -> Unresolved variable
48
+ print(e) # -> Unresolved variable: name
49
49
  ```
50
50
 
51
51
  ## Formatting with fallback values
@@ -2,6 +2,9 @@ It is possible to write a custom formatter to extend the functionality of the
2
2
  library. Here's an example of a simple formatter:
3
3
 
4
4
  ```python
5
+ from typing import Any
6
+ from messageformat2 import format_message
7
+
5
8
  def capitalize(value: Any, locale, options) -> str:
6
9
  return value.capitalize()
7
10
 
@@ -41,13 +44,17 @@ and `messageformat2.errors.MessageFunctionError` will be reraised instead.
41
44
  ```python
42
45
  from messageformat2.errors import MessageFunctionError
43
46
 
47
+ def raise_some_error(value, locale, options) -> str:
48
+ raise ValueError("something went wrong")
49
+
44
50
  try:
45
51
  format_message(
46
52
  "{foo :raise}",
47
53
  formatters={"raise": raise_some_error}
48
54
  )
49
55
  except MessageFunctionError as e:
50
- print(e)
56
+ print(e) # -> Exception raised while evaluating formatter :raise
57
+ print(repr(e.__cause__)) # -> ValueError('something went wrong')
51
58
  ```
52
59
 
53
60
  To signal a specific error, a formatter can raise one of the `MessageFunctionError` subclasses
@@ -94,3 +101,17 @@ format_message(
94
101
  )
95
102
  # -> "ALICE"
96
103
  ```
104
+
105
+ ## Directionality
106
+
107
+ The result of a custom formatter has an unknown direction and is isolated with U+2068/U+2069 when the message
108
+ is formatted with `bidi_isolation=True` (see [Bidirectional text](../explanation.md#bidirectional-text)).
109
+ Return a `FormattedValue` to declare the direction instead, so that a value in the direction of the message
110
+ is not isolated:
111
+
112
+ ```python
113
+ from messageformat2.builtins import FormattedValue
114
+
115
+ def shout(value, locale, options) -> FormattedValue:
116
+ return FormattedValue(str(value).upper(), locale.text_direction)
117
+ ```
@@ -4,23 +4,27 @@ statement.
4
4
  Here's a simple implementation of the `:integer` selector:
5
5
 
6
6
  ```python
7
+ from typing import Any
8
+
9
+ from babel import Locale
10
+
11
+ from messageformat2 import format_message
12
+
13
+
7
14
  def integer_selector(
8
15
  value: Any, # The value passed to the selector while formatting the message
9
- *,
10
16
  locale: Locale, # Babel locale
11
17
  options: dict[str, Any], # Selector options
12
- keys: list[str] # Variant keys to be filtered out
18
+ keys: list[str], # Variant keys to be filtered out
13
19
  ) -> list[str]:
20
+ value = int(value)
14
21
  select = options.get("select", "plural")
15
22
 
16
- exact_keys = [value] if value in keys else []
23
+ exact_keys = [key for key in keys if key == str(value)]
17
24
  if select == "exact":
18
25
  return exact_keys
19
- if select == "plural":
20
- form = locale.plural_form(value)
21
- if select == "ordinal":
22
- form = locale.ordinal_form(value)
23
- return exact_keys + keyword_keys
26
+ form = locale.ordinal_form(value) if select == "ordinal" else locale.plural_form(value)
27
+ return exact_keys + [key for key in keys if key == form]
24
28
 
25
29
 
26
30
  format_message("""\
@@ -32,19 +36,19 @@ one {{You have {$count} notification.}}
32
36
  {"count": 1},
33
37
  selectors={"integer": integer_selector}
34
38
  )
35
- # -> You have one notification.
39
+ # -> "You have one notification."
36
40
  ```
37
41
 
38
- A selector function must take exactly 3 arguments:
42
+ A selector function must take exactly 4 arguments, which are passed by keyword (so they must have these names):
39
43
 
40
- - `value`: the value passed to the formatter when formatting
44
+ - `value`: the value passed to the selector when formatting
41
45
  - `locale`: the current locale as a `babel.Locale` instance
42
- - `options`: a dictionary containing all the options passed to the formatter.
43
- - `keys`: The key from each variant of the match statement.
46
+ - `options`: a dictionary containing all the options passed to the selector
47
+ - `keys`: the key from each variant of the match statement (the catch-all `*` is not included)
44
48
 
45
49
  For example, for this message:
46
50
 
47
- ```python
51
+ ```
48
52
  .input {$count :integer select=plural}
49
53
  .match $count
50
54
  1 {{You have one notification.}}
@@ -1,9 +1,9 @@
1
1
  This site contains documentation for the `messageformat2`, a Python library
2
- which implements the [Unicode Message Format 2.0
3
- specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
2
+ which implements the [Unicode MessageFormat
3
+ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html) (LDML 49).
4
4
 
5
- Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
6
- more expressive and flexible than both Message Format 1.0 and, in the context of
5
+ Unicode MessageFormat (MessageFormat 2.0, MF2) is a new iteration of MessageFormat which aims to be
6
+ more expressive and flexible than both MessageFormat 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
@@ -13,10 +13,13 @@ specification](https://www.unicode.org/reports/tr35/tr35-messageFormat.html).
13
13
 
14
14
  The `messageformat2` library includes:
15
15
 
16
- - A message parser and a formatter
17
- - Several builtin formatters and selectors described by the
16
+ - A message parser and a formatter, passing the official
17
+ [test suite](https://github.com/unicode-org/message-format-wg/tree/main/test)
18
+ - All default functions described by the
18
19
  [spec](https://github.com/unicode-org/message-format-wg/blob/main/spec/functions/README.md)
20
+ (`:string`, `:number`, `:integer`, `:offset`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`)
19
21
  and the possibility to write custom formatters/selectors
22
+ - Formatting with fallback values and error reporting, optional bidi isolation of placeholders
20
23
  - Helper classes to inspect and transform the message [data
21
24
  model](https://github.com/unicode-org/message-format-wg/blob/main/spec/data-model/README.md)
22
25
 
@@ -1,5 +1,5 @@
1
1
  from messageformat2.message import Message, format_message
2
2
 
3
3
 
4
- __version__ = "1.0.0"
4
+ __version__ = "1.1.0"
5
5
  __all__ = ["Message", "format_message"]