messageformat2 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- messageformat2/__init__.py +5 -0
- messageformat2/builtins.py +323 -0
- messageformat2/cli.py +51 -0
- messageformat2/datamodel.py +507 -0
- messageformat2/errors.py +151 -0
- messageformat2/message.py +127 -0
- messageformat2/parser.py +557 -0
- messageformat2/runtime.py +301 -0
- messageformat2-0.1.0.dist-info/LICENSE +21 -0
- messageformat2-0.1.0.dist-info/METADATA +148 -0
- messageformat2-0.1.0.dist-info/RECORD +14 -0
- messageformat2-0.1.0.dist-info/WHEEL +5 -0
- messageformat2-0.1.0.dist-info/entry_points.txt +2 -0
- messageformat2-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from babel import Locale
|
|
5
|
+
|
|
6
|
+
from messageformat2.builtins import Registry
|
|
7
|
+
from messageformat2.errors import (
|
|
8
|
+
InvalidExpression,
|
|
9
|
+
SelectionError,
|
|
10
|
+
UnknownFunction,
|
|
11
|
+
UnresolvedVariable,
|
|
12
|
+
UnsupportedExpression,
|
|
13
|
+
UnsupportedStatement,
|
|
14
|
+
)
|
|
15
|
+
from messageformat2.parser import (
|
|
16
|
+
CatchallKey,
|
|
17
|
+
Expression,
|
|
18
|
+
FunctionAnnotation,
|
|
19
|
+
FunctionExpression,
|
|
20
|
+
InputDeclaration,
|
|
21
|
+
Literal,
|
|
22
|
+
LiteralExpression,
|
|
23
|
+
Markup,
|
|
24
|
+
Message,
|
|
25
|
+
Option,
|
|
26
|
+
Pattern,
|
|
27
|
+
PatternMessage,
|
|
28
|
+
SelectMessage,
|
|
29
|
+
UnsupportedAnnotation,
|
|
30
|
+
VariableExpression,
|
|
31
|
+
VariableRef,
|
|
32
|
+
Variant,
|
|
33
|
+
)
|
|
34
|
+
from messageformat2.parser import (
|
|
35
|
+
UnsupportedExpression as _UnsupportedExpression,
|
|
36
|
+
)
|
|
37
|
+
from messageformat2.parser import UnsupportedStatement as _UnsupportedStatement
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FormattingContext:
|
|
42
|
+
locale: Locale
|
|
43
|
+
inputs: dict[str, Any]
|
|
44
|
+
registry: Registry
|
|
45
|
+
declarations: dict[str, Any]
|
|
46
|
+
strict: bool = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LazyValue:
|
|
50
|
+
def __init__(self, *, fn_name: str, value: Any = None, options: dict[str, Any] | None = None):
|
|
51
|
+
self.fn_name = fn_name
|
|
52
|
+
self.value = value
|
|
53
|
+
self.options = options or {}
|
|
54
|
+
|
|
55
|
+
def format(self, ctx: FormattingContext) -> str:
|
|
56
|
+
if annotation := ctx.registry.formatters.get(self.fn_name):
|
|
57
|
+
try:
|
|
58
|
+
return str(annotation(value=self.value, locale=ctx.locale, options=self.options))
|
|
59
|
+
except InvalidExpression:
|
|
60
|
+
raise
|
|
61
|
+
except Exception as e:
|
|
62
|
+
msg = "Exception raised while evaluating formatter"
|
|
63
|
+
raise InvalidExpression(msg) from e
|
|
64
|
+
msg = f"Unknown function: {self.fn_name}"
|
|
65
|
+
raise UnknownFunction(msg)
|
|
66
|
+
|
|
67
|
+
def select(self, ctx: FormattingContext, *, keys: list[str]) -> list[str]:
|
|
68
|
+
if selector := ctx.registry.selectors.get(self.fn_name):
|
|
69
|
+
try:
|
|
70
|
+
return selector(value=self.value, locale=ctx.locale, options=self.options, keys=keys)
|
|
71
|
+
except SelectionError:
|
|
72
|
+
raise
|
|
73
|
+
except Exception as e:
|
|
74
|
+
msg = "Exception raised while evaluating selector"
|
|
75
|
+
raise SelectionError(msg) from e
|
|
76
|
+
msg = f"Unknown selector: {self.fn_name}"
|
|
77
|
+
raise UnknownFunction(msg)
|
|
78
|
+
|
|
79
|
+
def __repr__(self):
|
|
80
|
+
return f"LazyValue({self.fn_name}({self.value}, {self.options}))"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_message(message: Message, locale: Locale, inputs: dict[str, Any], registry: Registry) -> str:
|
|
84
|
+
ctx = FormattingContext(
|
|
85
|
+
locale=locale,
|
|
86
|
+
inputs=inputs,
|
|
87
|
+
registry=registry,
|
|
88
|
+
declarations={},
|
|
89
|
+
)
|
|
90
|
+
match message:
|
|
91
|
+
case PatternMessage():
|
|
92
|
+
return format_pattern_message(message, ctx)
|
|
93
|
+
case _:
|
|
94
|
+
return format_select_message(message, ctx)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_pattern_message(message: PatternMessage, ctx: FormattingContext) -> str:
|
|
98
|
+
for decl in message.declarations:
|
|
99
|
+
match decl:
|
|
100
|
+
case _UnsupportedStatement():
|
|
101
|
+
msg = "Unsupported statement"
|
|
102
|
+
raise UnsupportedStatement(msg)
|
|
103
|
+
case _:
|
|
104
|
+
ctx.declarations[decl.name] = decl
|
|
105
|
+
return format_pattern(message.pattern, ctx)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def format_pattern(pattern: Pattern, ctx: FormattingContext) -> str:
|
|
109
|
+
output = ""
|
|
110
|
+
for part in pattern:
|
|
111
|
+
match part:
|
|
112
|
+
case str():
|
|
113
|
+
output += part
|
|
114
|
+
case LiteralExpression() | VariableExpression() | FunctionExpression() | _UnsupportedExpression():
|
|
115
|
+
output += format_expression(part, ctx)
|
|
116
|
+
case Markup():
|
|
117
|
+
output += format_markup(part, ctx)
|
|
118
|
+
return output
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_select_message(message: SelectMessage, ctx: FormattingContext) -> str:
|
|
122
|
+
for decl in message.declarations:
|
|
123
|
+
match decl:
|
|
124
|
+
case _UnsupportedStatement():
|
|
125
|
+
msg = "Unsupported statement"
|
|
126
|
+
raise UnsupportedStatement(msg)
|
|
127
|
+
case _:
|
|
128
|
+
ctx.declarations[decl.name] = decl
|
|
129
|
+
|
|
130
|
+
selectors = [resolve_selector(selector, ctx) for selector in message.selectors]
|
|
131
|
+
|
|
132
|
+
pref = []
|
|
133
|
+
for i, selector in enumerate(selectors):
|
|
134
|
+
keys = [key.value for v in message.variants if isinstance(key := v.keys[i], Literal)]
|
|
135
|
+
pref.append(selector.select(ctx, keys=keys))
|
|
136
|
+
|
|
137
|
+
variants = filter_variants(pref, message.variants)
|
|
138
|
+
pattern = sort_variants(pref, variants)
|
|
139
|
+
return format_pattern(pattern, ctx)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def filter_variants(pref: list, variants: list[Variant]) -> list[Variant]:
|
|
143
|
+
filtered = []
|
|
144
|
+
for v in variants:
|
|
145
|
+
include = True
|
|
146
|
+
for i, matches in enumerate(pref):
|
|
147
|
+
key = v.keys[i]
|
|
148
|
+
if isinstance(key, CatchallKey):
|
|
149
|
+
continue
|
|
150
|
+
assert isinstance(key, Literal)
|
|
151
|
+
ks = key.value
|
|
152
|
+
if ks not in matches:
|
|
153
|
+
include = False
|
|
154
|
+
if include:
|
|
155
|
+
filtered.append(v)
|
|
156
|
+
return filtered
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def sort_variants(pref: list, variants: list[Variant]) -> Pattern:
|
|
160
|
+
sortable = [[-1, v] for v in variants]
|
|
161
|
+
|
|
162
|
+
i = len(pref) - 1
|
|
163
|
+
while i >= 0:
|
|
164
|
+
matches = pref[i]
|
|
165
|
+
minpref = len(matches)
|
|
166
|
+
for s in sortable:
|
|
167
|
+
matchpref = minpref
|
|
168
|
+
key = s[1].keys[i]
|
|
169
|
+
if not isinstance(key, CatchallKey):
|
|
170
|
+
assert isinstance(key, Literal)
|
|
171
|
+
ks = key.value
|
|
172
|
+
matchpref = matches.index(ks)
|
|
173
|
+
s[0] = matchpref
|
|
174
|
+
sortable.sort(key=lambda x: x[0])
|
|
175
|
+
i = i - 1
|
|
176
|
+
return sortable[0][1].value
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def resolve_selector(selector: Expression, ctx: FormattingContext) -> Any | LazyValue:
|
|
180
|
+
return resolve_expression(selector, ctx)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def format_expression(expression: Expression, ctx: FormattingContext) -> str:
|
|
184
|
+
resolved = resolve_expression(expression, ctx)
|
|
185
|
+
match resolved:
|
|
186
|
+
case LazyValue() as lazy_value:
|
|
187
|
+
return lazy_value.format(ctx)
|
|
188
|
+
case _:
|
|
189
|
+
return str(resolved)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def format_markup(markup: Markup, ctx: FormattingContext) -> str:
|
|
193
|
+
options = format_options(markup.options, ctx)
|
|
194
|
+
options = f" {options}" if options else ""
|
|
195
|
+
|
|
196
|
+
if markup.kind == "standalone":
|
|
197
|
+
return f"<{markup.name}{options}/>"
|
|
198
|
+
if markup.kind == "open":
|
|
199
|
+
return f"<{markup.name}{options}>"
|
|
200
|
+
return f"</{markup.name}{options}>"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def format_options(options: list[Option], ctx: FormattingContext) -> str:
|
|
204
|
+
return " ".join(format_option(opt, ctx) for opt in options)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def format_option(option: Option, ctx: FormattingContext) -> str:
|
|
208
|
+
value = resolve_option(option, ctx)
|
|
209
|
+
return f"{option.name}={value}"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def resolve_options(options: list[Option], ctx: FormattingContext) -> dict[str, Any]:
|
|
213
|
+
return {opt.name: resolve_option(opt, ctx) for opt in options}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def resolve_option(option: Option, ctx: FormattingContext) -> Any:
|
|
217
|
+
match option.value:
|
|
218
|
+
case Literal(value=value):
|
|
219
|
+
return value
|
|
220
|
+
case VariableRef(name=name):
|
|
221
|
+
resolved = resolve_variable(name, ctx)
|
|
222
|
+
match resolved:
|
|
223
|
+
case LazyValue() as lazy_value:
|
|
224
|
+
return lazy_value.format(ctx)
|
|
225
|
+
case _:
|
|
226
|
+
return resolved
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def resolve_expression(expression: Expression, ctx: FormattingContext) -> Any | LazyValue:
|
|
230
|
+
match expression:
|
|
231
|
+
case LiteralExpression(arg=Literal(value), annotation=annotation):
|
|
232
|
+
if not annotation:
|
|
233
|
+
return value
|
|
234
|
+
match annotation:
|
|
235
|
+
case FunctionAnnotation(name=name, options=options):
|
|
236
|
+
options = resolve_options(options, ctx)
|
|
237
|
+
return LazyValue(fn_name=name, value=value, options=options)
|
|
238
|
+
case UnsupportedAnnotation():
|
|
239
|
+
msg = "Unsupported expression"
|
|
240
|
+
raise UnsupportedExpression(msg)
|
|
241
|
+
case VariableExpression(arg=VariableRef(ref), annotation=annotation):
|
|
242
|
+
if not annotation:
|
|
243
|
+
return resolve_variable(ref, ctx)
|
|
244
|
+
match annotation:
|
|
245
|
+
case FunctionAnnotation(name=name, options=options):
|
|
246
|
+
resolved = resolve_variable(ref, ctx)
|
|
247
|
+
options = resolve_options(options, ctx)
|
|
248
|
+
match resolved:
|
|
249
|
+
case LazyValue(fn_name=lazy_function, value=lazy_value, options=lazy_options):
|
|
250
|
+
if lazy_function and lazy_function != name:
|
|
251
|
+
msg = f"Function mismatch: {lazy_function} != {name}"
|
|
252
|
+
raise ValueError(msg)
|
|
253
|
+
return LazyValue(fn_name=name, value=lazy_value, options=(lazy_options or {}) | options)
|
|
254
|
+
case _:
|
|
255
|
+
return LazyValue(fn_name=name, value=resolved, options=options)
|
|
256
|
+
case UnsupportedAnnotation():
|
|
257
|
+
msg = "Unsupported expression"
|
|
258
|
+
raise UnsupportedExpression(msg)
|
|
259
|
+
case FunctionExpression(annotation=FunctionAnnotation(name=name, options=options)):
|
|
260
|
+
options = resolve_options(options, ctx)
|
|
261
|
+
return LazyValue(fn_name=name, value=None, options=options)
|
|
262
|
+
case _UnsupportedExpression(annotation=annotation):
|
|
263
|
+
msg = f"Unsupported expression: {annotation}"
|
|
264
|
+
raise UnsupportedExpression(msg)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def resolve_variable(name: str, ctx: FormattingContext) -> Any:
|
|
268
|
+
if name in ctx.declarations:
|
|
269
|
+
decl = ctx.declarations[name]
|
|
270
|
+
match decl:
|
|
271
|
+
case InputDeclaration(value=VariableExpression(annotation=annotation)):
|
|
272
|
+
if not annotation:
|
|
273
|
+
return resolve_global(name, ctx)
|
|
274
|
+
match annotation:
|
|
275
|
+
case FunctionAnnotation(name=fn_name, options=options):
|
|
276
|
+
resolved = resolve_global(name, ctx)
|
|
277
|
+
options = resolve_options(options, ctx)
|
|
278
|
+
match resolved:
|
|
279
|
+
case LazyValue(fn_name=lazy_fn_name, value=lazy_value, options=lazy_options):
|
|
280
|
+
if lazy_fn_name and lazy_fn_name != fn_name:
|
|
281
|
+
msg = f"Function mismatch: {lazy_fn_name} != {fn_name}"
|
|
282
|
+
raise ValueError(msg)
|
|
283
|
+
return LazyValue(
|
|
284
|
+
fn_name=fn_name, value=lazy_value, options=(lazy_options or {}) | options
|
|
285
|
+
)
|
|
286
|
+
case _:
|
|
287
|
+
return LazyValue(fn_name=fn_name, value=resolved, options=options)
|
|
288
|
+
case UnsupportedAnnotation(annotation=annotation):
|
|
289
|
+
msg = f"Unsupported expression: {annotation}"
|
|
290
|
+
raise UnsupportedExpression(msg)
|
|
291
|
+
case _:
|
|
292
|
+
expression = decl.value
|
|
293
|
+
return resolve_expression(expression, ctx)
|
|
294
|
+
return resolve_global(name, ctx)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def resolve_global(name: str, ctx: FormattingContext) -> Any:
|
|
298
|
+
if name in ctx.inputs:
|
|
299
|
+
return ctx.inputs[name]
|
|
300
|
+
msg = f"Unresolved variable: {name}"
|
|
301
|
+
raise UnresolvedVariable(msg)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) European Organization for Nuclear Research (CERN)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: messageformat2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python implementation of Unicode MessageFormat 2.0
|
|
5
|
+
Author-email: Tomas Roun <tomas.roun8@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) European Organization for Nuclear Research (CERN)
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/tomasr8/messageformat2
|
|
29
|
+
Project-URL: Github, https://github.com/tomasr8/messageformat2
|
|
30
|
+
Keywords: i18n,internationalization,localization,messageformat,messageformat 2,cldr,gettext
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: Topic :: Scientific/Engineering
|
|
36
|
+
Classifier: Topic :: Software Development :: Internationalization
|
|
37
|
+
Classifier: Topic :: Software Development :: Localization
|
|
38
|
+
Requires-Python: >=3.12
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: Babel
|
|
42
|
+
Requires-Dist: click
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: build ; extra == 'dev'
|
|
45
|
+
Requires-Dist: twine ; extra == 'dev'
|
|
46
|
+
Requires-Dist: uv ; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest-snapshot ; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-cov ; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff ; extra == 'dev'
|
|
51
|
+
Requires-Dist: pyright ; extra == 'dev'
|
|
52
|
+
Requires-Dist: mkdocs ; extra == 'dev'
|
|
53
|
+
Requires-Dist: mkdocstrings[python] ; extra == 'dev'
|
|
54
|
+
Requires-Dist: mkdocs-material ; extra == 'dev'
|
|
55
|
+
Provides-Extra: docs
|
|
56
|
+
Requires-Dist: mkdocs ; extra == 'docs'
|
|
57
|
+
Requires-Dist: mkdocstrings[python] ; extra == 'docs'
|
|
58
|
+
Requires-Dist: mkdocs-material ; extra == 'docs'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# MessageFormat2 [](https://messageformat2.readthedocs.io/en/latest/?badge=latest) [](https://github.com/tomasr8/messageformat2/workflows/CI/badge.svg) [](https://microsoft.github.io/pyright/)
|
|
63
|
+
|
|
64
|
+
This is a Python implementation of the [Unicode Message Format 2.0
|
|
65
|
+
specification](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html).
|
|
66
|
+
|
|
67
|
+
Message Format 2.0 (MF2) is a new iteration of Message Format which aims to be
|
|
68
|
+
more expressive and flexible than both Message Format 1.0 and, in the context of
|
|
69
|
+
Python, gettext.
|
|
70
|
+
|
|
71
|
+
If you would like to learn more about MF2 itself head over to the [Unicode technical
|
|
72
|
+
specification](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html).
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
The library includes:
|
|
77
|
+
|
|
78
|
+
- A message parser and a formatter
|
|
79
|
+
- Several builtin formatters and selectors described by the
|
|
80
|
+
[spec](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#function-registry)
|
|
81
|
+
and the possibility to write custom formatters/selectors
|
|
82
|
+
- Helper classes to inspect and transform the message [data
|
|
83
|
+
model](https://www.unicode.org/reports/tr35/tr35-72/tr35-messageFormat.html#interchange-data-model)
|
|
84
|
+
|
|
85
|
+
## [Documentation](https://messageformat2.readthedocs.io/en/latest/)
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
Via pip for Python `>= 3.12`
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
pip install messageformat2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
### Simple messages
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from messageformat2 import format_message
|
|
101
|
+
|
|
102
|
+
format_message("Hello, {$name}!", {"name": "Alice"}) # -> "Hello, Alice!"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Messages can be reused with different inputs.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from messageformat2 import Message
|
|
109
|
+
|
|
110
|
+
message = Message("Hello, {$name}!")
|
|
111
|
+
message.format({"name": "Alice"}) # -> "Hello, Alice!"
|
|
112
|
+
message.format({"name": "Bob"}) # -> "Hello, Bob!"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Built-in formatters
|
|
116
|
+
|
|
117
|
+
There are several builtin locale-aware formatters available.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from datetime import datetime
|
|
121
|
+
from messageformat2 import Message
|
|
122
|
+
|
|
123
|
+
message = Message("Today's date is {$now :date}")
|
|
124
|
+
now = datetime.now()
|
|
125
|
+
message.format({"now": now}, locale='en_US')
|
|
126
|
+
# -> Today's date is 5/10/2024
|
|
127
|
+
message.format({"now": now}, locale='en_GB')
|
|
128
|
+
# -> Today's date is 10/05/2024
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Plural support
|
|
132
|
+
|
|
133
|
+
MF2 supports pluralization using built-in or custom selectors.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from messageformat2 import Message
|
|
137
|
+
|
|
138
|
+
msg = Message ("""\
|
|
139
|
+
.match {$count :number}
|
|
140
|
+
one {{You have {$count} notification.}}
|
|
141
|
+
* {{You have {$count} notifications.}}
|
|
142
|
+
""")
|
|
143
|
+
|
|
144
|
+
msg.format({"count": 42}, locale='en_US') # -> "You have 42 notifications."
|
|
145
|
+
msg.format({"count": 1}, locale='en_US') # -> "You have 1 notification."
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Complete documentation is available [here](https://messageformat2.readthedocs.io/en/latest/).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
messageformat2/__init__.py,sha256=1A2OfD38uErtOd27xMUDA1BoRAMuvDj95QVPTQuMexI,123
|
|
2
|
+
messageformat2/builtins.py,sha256=lJV-ibYu0kk1TwJxkA4NK_mJjdSV6UnLq6CgldZ0DAg,10612
|
|
3
|
+
messageformat2/cli.py,sha256=n_cR9jOYg9aoC3VZ6_sReZ7UM-i8iTtiMWvI5mqEJyU,1421
|
|
4
|
+
messageformat2/datamodel.py,sha256=3MRPau_An48cxOoPAW2JJCornFbLzj823bXyxLxVUI0,15486
|
|
5
|
+
messageformat2/errors.py,sha256=Tli-83XlHhG1ia5fbLcFLnZUISyWSJ79yHsJNaKlt98,4447
|
|
6
|
+
messageformat2/message.py,sha256=iLwKjzkrfN_8-oVFL8-sPeAoHKh9vwhHZP96ZXhv5YM,4159
|
|
7
|
+
messageformat2/parser.py,sha256=6TxFQma0pODuBCz7ArHpKvh53GfvnK8rLDleqiHupK8,17023
|
|
8
|
+
messageformat2/runtime.py,sha256=Ml9EsmLiAExAN8l8H8EzsMXHHlq7kBWSBYKH-o1uzi0,11123
|
|
9
|
+
messageformat2-0.1.0.dist-info/LICENSE,sha256=yPtRmLQVOw1jZIJ9HeJ6Su2-NA_RAXG53XdfeQ7Vyvk,1101
|
|
10
|
+
messageformat2-0.1.0.dist-info/METADATA,sha256=hzsAE8JBrjPsK3cV0wN4FYRUzS2a3tzd3MlxFzzwE3c,5665
|
|
11
|
+
messageformat2-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
12
|
+
messageformat2-0.1.0.dist-info/entry_points.txt,sha256=yQ3aIPQ4zPI188NgLN6F49a7kspObfZFuDBkNpVT0NU,47
|
|
13
|
+
messageformat2-0.1.0.dist-info/top_level.txt,sha256=XlAc7JtB9R1O9vO7viztX1fWAkiucBMmwNY8oG_7poA,15
|
|
14
|
+
messageformat2-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
messageformat2
|