gettext-tstrings 0.1.0a1__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.
- gettext_tstrings/__init__.py +43 -0
- gettext_tstrings/_patterns.py +93 -0
- gettext_tstrings/checkers.py +42 -0
- gettext_tstrings/core.py +593 -0
- gettext_tstrings/errors.py +13 -0
- gettext_tstrings/extract.py +327 -0
- gettext_tstrings/lazy.py +57 -0
- gettext_tstrings/py.typed +1 -0
- gettext_tstrings-0.1.0a1.dist-info/METADATA +346 -0
- gettext_tstrings-0.1.0a1.dist-info/RECORD +13 -0
- gettext_tstrings-0.1.0a1.dist-info/WHEEL +4 -0
- gettext_tstrings-0.1.0a1.dist-info/entry_points.txt +5 -0
- gettext_tstrings-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Gettext and extraction-tool integration for Python t-strings."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
CompiledTemplate,
|
|
5
|
+
Translations,
|
|
6
|
+
Translator,
|
|
7
|
+
compile_template,
|
|
8
|
+
get_translations,
|
|
9
|
+
gettext,
|
|
10
|
+
ngettext,
|
|
11
|
+
npgettext,
|
|
12
|
+
ntr,
|
|
13
|
+
pgettext,
|
|
14
|
+
set_translations,
|
|
15
|
+
tr,
|
|
16
|
+
use_translations,
|
|
17
|
+
)
|
|
18
|
+
from .errors import InvalidTemplateError, InvalidTranslationError, TStringError
|
|
19
|
+
from .lazy import LazyString, lazy_gettext, lazy_pgettext
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CompiledTemplate",
|
|
23
|
+
"InvalidTemplateError",
|
|
24
|
+
"InvalidTranslationError",
|
|
25
|
+
"LazyString",
|
|
26
|
+
"TStringError",
|
|
27
|
+
"Translations",
|
|
28
|
+
"Translator",
|
|
29
|
+
"compile_template",
|
|
30
|
+
"get_translations",
|
|
31
|
+
"gettext",
|
|
32
|
+
"lazy_gettext",
|
|
33
|
+
"lazy_pgettext",
|
|
34
|
+
"ngettext",
|
|
35
|
+
"npgettext",
|
|
36
|
+
"ntr",
|
|
37
|
+
"pgettext",
|
|
38
|
+
"set_translations",
|
|
39
|
+
"tr",
|
|
40
|
+
"use_translations",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0a1"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Shared brace-pattern parsing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import keyword
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from string import Formatter
|
|
9
|
+
|
|
10
|
+
from .errors import InvalidTranslationError
|
|
11
|
+
|
|
12
|
+
MARKER_COMMENT = "gettext-tstrings"
|
|
13
|
+
_FORMATTER = Formatter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class Pattern:
|
|
18
|
+
"""A validated, cached translation pattern."""
|
|
19
|
+
|
|
20
|
+
chunks: tuple[tuple[str, str | None], ...]
|
|
21
|
+
fields: frozenset[str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def escape_literal(text: str) -> str:
|
|
25
|
+
"""Escape braces in literal t-string segments for a brace-format pattern."""
|
|
26
|
+
return text.replace("{", "{{").replace("}", "}}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_name(name: str) -> str:
|
|
30
|
+
"""Return a safe, simple placeholder name or raise."""
|
|
31
|
+
normalized = name.strip()
|
|
32
|
+
if not normalized.isidentifier() or keyword.iskeyword(normalized):
|
|
33
|
+
raise InvalidTranslationError(
|
|
34
|
+
f"placeholder {name!r} is not a simple Python identifier",
|
|
35
|
+
)
|
|
36
|
+
return normalized
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=4096)
|
|
40
|
+
def parse_pattern(pattern: str) -> Pattern:
|
|
41
|
+
"""Parse and cache a restricted brace pattern.
|
|
42
|
+
|
|
43
|
+
Only bare named placeholders are accepted. Formatting and conversion are
|
|
44
|
+
controlled by the source t-string and never by a catalog translation.
|
|
45
|
+
"""
|
|
46
|
+
chunks: list[tuple[str, str | None]] = []
|
|
47
|
+
fields: set[str] = set()
|
|
48
|
+
try:
|
|
49
|
+
for literal, field_name, format_spec, conversion in _FORMATTER.parse(pattern):
|
|
50
|
+
if field_name is None:
|
|
51
|
+
chunks.append((literal, None))
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
name = validate_name(field_name)
|
|
55
|
+
if format_spec or conversion:
|
|
56
|
+
raise InvalidTranslationError(
|
|
57
|
+
f"translation placeholder {{{name}}} must not add "
|
|
58
|
+
"a conversion or format specifier",
|
|
59
|
+
)
|
|
60
|
+
fields.add(name)
|
|
61
|
+
chunks.append((literal, name))
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
raise InvalidTranslationError(f"invalid translation pattern: {exc}") from exc
|
|
64
|
+
return Pattern(tuple(chunks), frozenset(fields))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def require_fields(
|
|
68
|
+
*,
|
|
69
|
+
required: frozenset[str],
|
|
70
|
+
allowed: frozenset[str],
|
|
71
|
+
actual: frozenset[str],
|
|
72
|
+
label: str = "translation",
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Require all necessary fields and reject unknown ones.
|
|
75
|
+
|
|
76
|
+
Placeholder occurrence counts are deliberately unrestricted: repeating a
|
|
77
|
+
known value can be grammatically necessary in a target language.
|
|
78
|
+
"""
|
|
79
|
+
missing = required - actual
|
|
80
|
+
unexpected = actual - allowed
|
|
81
|
+
if not missing and not unexpected:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# 非ASCIIのホモグリフ名はASCII名と見分けがつかないため、
|
|
85
|
+
# 各名前を ascii() で可視化してから並べる。
|
|
86
|
+
details: list[str] = []
|
|
87
|
+
if missing:
|
|
88
|
+
details.append(f"missing [{', '.join(ascii(n) for n in sorted(missing))}]")
|
|
89
|
+
if unexpected:
|
|
90
|
+
details.append(f"unexpected [{', '.join(ascii(n) for n in sorted(unexpected))}]")
|
|
91
|
+
raise InvalidTranslationError(
|
|
92
|
+
f"{label} placeholders do not match source: {', '.join(details)}",
|
|
93
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Catalog checker for messages emitted by the t-string extractor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from babel.messages.catalog import Catalog, Message, TranslationError
|
|
8
|
+
|
|
9
|
+
from ._patterns import MARKER_COMMENT, parse_pattern, require_fields
|
|
10
|
+
from .errors import InvalidTranslationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _strings(value: str | Iterable[str] | None) -> tuple[str, ...]:
|
|
14
|
+
if value is None:
|
|
15
|
+
return ()
|
|
16
|
+
if isinstance(value, str):
|
|
17
|
+
return (value,)
|
|
18
|
+
return tuple(value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_tstring(catalog: Catalog | None, message: Message) -> None:
|
|
22
|
+
"""Validate placeholder integrity for extracted t-string messages."""
|
|
23
|
+
del catalog
|
|
24
|
+
if MARKER_COMMENT not in message.auto_comments:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
source_patterns = _strings(message.id)
|
|
28
|
+
try:
|
|
29
|
+
source_fields = [parse_pattern(pattern).fields for pattern in source_patterns]
|
|
30
|
+
allowed = frozenset().union(*source_fields)
|
|
31
|
+
required = frozenset.intersection(*source_fields)
|
|
32
|
+
|
|
33
|
+
for translation in _strings(message.string):
|
|
34
|
+
if not translation:
|
|
35
|
+
continue
|
|
36
|
+
require_fields(
|
|
37
|
+
required=required,
|
|
38
|
+
allowed=allowed,
|
|
39
|
+
actual=parse_pattern(translation).fields,
|
|
40
|
+
)
|
|
41
|
+
except InvalidTranslationError as exc:
|
|
42
|
+
raise TranslationError(str(exc)) from exc
|