Skip to content

Commit 7a7ce0d

Browse files
authored
refactor(gt-i18n): registration interface (#18)
1 parent af544ff commit 7a7ce0d

13 files changed

Lines changed: 140 additions & 61 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/gt-i18n: patch
3+
---
4+
5+
refactor: string registration interface

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ lint-fix:
1010
uv run ruff check --fix .
1111

1212
format:
13+
uv run ruff check --fix .
1314
uv run ruff format .
1415

1516
format-check:
Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
"""Filter $-prefixed GT keys from options, returning user variables."""
1+
"""Filter GT-reserved keys from options, returning user variables."""
22

33
from __future__ import annotations
44

5+
_GT_RESERVED_KEYS = frozenset(
6+
{
7+
"_context",
8+
"_id",
9+
"_max_chars",
10+
"__hash",
11+
"__source",
12+
"__fallback",
13+
}
14+
)
15+
516

617
def extract_variables(options: dict[str, object]) -> dict[str, object]:
7-
"""Return only user interpolation variables (non-$ keys).
18+
"""Return only user interpolation variables.
819
920
Args:
10-
options: The full kwargs dict which may contain ``$context``,
11-
``$id``, ``$max_chars``, etc.
21+
options: The full kwargs dict which may contain GT-reserved keys
22+
like ``_context``, ``_id``, ``_max_chars``, etc.
1223
1324
Returns:
14-
A new dict with only the user-provided variable keys.
25+
A new dict with GT-reserved keys removed.
1526
"""
16-
return {k: v for k, v in options.items() if not k.startswith("$")}
27+
return {k: v for k, v in options.items() if k not in _GT_RESERVED_KEYS}

packages/gt-i18n/src/gt_i18n/translation_functions/_interpolate.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ def interpolate_message(
2525
2626
Mirrors JS ``interpolateMessage()`` behavior:
2727
28-
1. Extract user variables (filter out ``$``-prefixed keys).
28+
1. Extract user variables (filter out GT-reserved keys).
2929
2. Extract ``_gt_`` declared variables from the source/fallback.
3030
3. Condense ``_gt_`` selects to simple refs (only if declared vars exist).
3131
4. Format with ICU MessageFormat.
32-
5. Apply ``$max_chars`` cutoff if specified.
32+
5. Apply ``_max_chars`` cutoff if specified.
3333
3434
On error:
35-
- If ``$_fallback`` (source) is available, recursively retry with
36-
the source message (clearing ``$_fallback`` to prevent infinite loop).
35+
- If ``__fallback`` (source) is available, recursively retry with
36+
the source message (clearing ``__fallback`` to prevent infinite loop).
3737
- Otherwise, return the raw message with cutoff applied.
3838
3939
Args:
@@ -44,8 +44,8 @@ def interpolate_message(
4444
Returns:
4545
The interpolated string.
4646
"""
47-
source = options.get("$_fallback")
48-
max_chars = options.get("$max_chars")
47+
source = options.get("__fallback")
48+
max_chars = options.get("_max_chars")
4949

5050
# Remove GT-related options, keep user variables
5151
variables = extract_variables(options)
@@ -83,7 +83,7 @@ def interpolate_message(
8383
if source is not None and isinstance(source, str):
8484
return interpolate_message(
8585
source,
86-
{**options, "$_fallback": None},
86+
{**options, "__fallback": None},
8787
locale,
8888
)
8989

packages/gt-i18n/src/gt_i18n/translation_functions/_is_encoded.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55

66
def is_encoded_translation_options(decoded_options: dict[str, object]) -> bool:
7-
"""Return True if the dict contains both ``$_hash`` and ``$_source``."""
8-
return bool(decoded_options.get("$_hash") and decoded_options.get("$_source"))
7+
"""Return True if the dict contains both ``__hash`` and ``__source``."""
8+
return bool(decoded_options.get("__hash") and decoded_options.get("__source"))

packages/gt-i18n/src/gt_i18n/translation_functions/_msg.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ def msg(message: str, **kwargs: object) -> str:
2929
except Exception:
3030
return message
3131

32-
# Build encoded options (preserve all kwargs including $-prefixed)
33-
h = kwargs.get("$_hash") or hash_message(
32+
# Build encoded options (preserve all kwargs including _-prefixed)
33+
h = kwargs.get("__hash") or hash_message(
3434
message,
35-
context=kwargs.get("$context"), # type: ignore[arg-type]
36-
id=kwargs.get("$id"), # type: ignore[arg-type]
37-
max_chars=kwargs.get("$max_chars"), # type: ignore[arg-type]
35+
context=kwargs.get("_context"), # type: ignore[arg-type]
36+
id=kwargs.get("_id"), # type: ignore[arg-type]
37+
max_chars=kwargs.get("_max_chars"), # type: ignore[arg-type]
3838
)
3939

40-
encoded_options = {**kwargs, "$_source": message, "$_hash": h}
40+
encoded_options = {**kwargs, "__source": message, "__hash": h}
4141
options_encoding = base64.b64encode(json.dumps(encoded_options, separators=(",", ":")).encode()).decode()
4242

4343
return f"{interpolated}:{options_encoding}"

packages/gt-i18n/src/gt_i18n/translation_functions/_t.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def t(message: str, **kwargs: object) -> str:
1717
Args:
1818
message: The ICU MessageFormat source string.
1919
**kwargs: Interpolation variables and GT options
20-
(``$context``, ``$id``, ``$max_chars``).
20+
(``_context``, ``_id``, ``_max_chars``).
2121
2222
Returns:
2323
The translated and interpolated string.
@@ -31,13 +31,13 @@ def t(message: str, **kwargs: object) -> str:
3131
translations = manager.get_translations_sync(locale)
3232
h = hash_message(
3333
message,
34-
context=kwargs.get("$context"), # type: ignore[arg-type]
35-
id=kwargs.get("$id"), # type: ignore[arg-type]
36-
max_chars=kwargs.get("$max_chars"), # type: ignore[arg-type]
34+
context=kwargs.get("_context"), # type: ignore[arg-type]
35+
id=kwargs.get("_id"), # type: ignore[arg-type]
36+
max_chars=kwargs.get("_max_chars"), # type: ignore[arg-type]
3737
)
3838
translated = translations.get(h)
3939
if translated:
40-
return interpolate_message(translated, {**kwargs, "$_fallback": message}, locale)
40+
return interpolate_message(translated, {**kwargs, "__fallback": message}, locale)
4141

4242
# No translation found — use source
4343
return interpolate_message(message, kwargs, locale)

packages/gt-i18n/tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Make the tests directory importable so helpers.py can be used."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
sys.path.insert(0, str(Path(__file__).parent))

packages/gt-i18n/tests/helpers.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Shared test helpers for converting JS-style fixture keys to Python-style."""
2+
3+
from __future__ import annotations
4+
5+
import base64
6+
import json
7+
from typing import Any
8+
9+
# Mapping from JS $-prefixed keys to Python _-prefixed keys
10+
JS_TO_PY_KEY_MAP: dict[str, str] = {
11+
"$context": "_context",
12+
"$id": "_id",
13+
"$max_chars": "_max_chars",
14+
"$_source": "__source",
15+
"$_hash": "__hash",
16+
}
17+
18+
19+
def convert_keys(d: dict[str, Any]) -> dict[str, Any]:
20+
"""Convert JS-style $-prefixed keys to Python-style _-prefixed keys."""
21+
return {JS_TO_PY_KEY_MAP.get(k, k): v for k, v in d.items()}
22+
23+
24+
def convert_encoded_string(encoded: str) -> str:
25+
"""Re-encode a JS-style encoded message with Python-style keys.
26+
27+
Takes 'message:base64payload', decodes the payload, converts
28+
$-prefixed keys to _-prefixed, and re-encodes.
29+
"""
30+
idx = encoded.rfind(":")
31+
if idx == -1:
32+
return encoded
33+
msg_part = encoded[:idx]
34+
payload = json.loads(base64.b64decode(encoded[idx + 1 :]).decode())
35+
converted = convert_keys(payload)
36+
new_b64 = base64.b64encode(json.dumps(converted, separators=(",", ":")).encode()).decode()
37+
return f"{msg_part}:{new_b64}"
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
"""Tests for extract_variables ($-key filtering)."""
1+
"""Tests for extract_variables (_-key filtering)."""
22

33
from gt_i18n.translation_functions._extract_variables import extract_variables
44

55

6-
def test_filters_dollar_keys() -> None:
7-
opts: dict[str, object] = dict({"name": "Alice", "$context": "greeting", "$id": "hello"})
6+
def test_filters_underscore_keys() -> None:
7+
opts: dict[str, object] = dict({"name": "Alice", "_context": "greeting", "_id": "hello"})
88
result = extract_variables(opts)
99
assert result == {"name": "Alice"}
1010

1111

12-
def test_keeps_all_non_dollar() -> None:
12+
def test_keeps_all_non_underscore() -> None:
1313
opts = {"name": "Alice", "count": 5, "item": "apples"}
1414
result = extract_variables(opts)
1515
assert result == opts
@@ -19,6 +19,12 @@ def test_empty_dict() -> None:
1919
assert extract_variables({}) == {}
2020

2121

22-
def test_all_dollar_keys() -> None:
23-
opts = {"$context": "x", "$id": "y", "$max_chars": 10}
22+
def test_all_underscore_keys() -> None:
23+
opts = {"_context": "x", "_id": "y", "_max_chars": 10}
2424
assert extract_variables(opts) == {}
25+
26+
27+
def test_user_underscore_prefixed_keys_preserved() -> None:
28+
opts: dict[str, object] = {"_name": "Alice", "_context": "greeting"}
29+
result = extract_variables(opts)
30+
assert result == {"_name": "Alice"}

0 commit comments

Comments
 (0)