feat: _() strings and screen texts become translatable strings; search looks in all states; stutter hint
CI / frontend (push) Failing after 14s
CI / backend (push) Failing after 44s

show text _("...") and literal screen texts are listed in Names and menu; typing in search switches the filter from To do to All; the translator is told how stutters are spelled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 21:09:55 +03:00
1 parent 262db2687e
commit cc71050362
5 files changed
+73 -1

No files matched your search

+37
View File
@@ -62,6 +62,39 @@ class TextIndex:
modules: list[tuple[str, int]] = field(default_factory=list)
_WORD = re.compile(r"[^\W\d_]")
_SCREEN_TEXT = {"text", "textbutton", "label"}
def marked_strings(value: Any) -> list[str]:
"""Texts that Ren'Py translates by their source: ``_("...")`` and the literal text of screen widgets.
``show text _("...")``, ``$ x = _("...")`` and screen ``text "..."`` carry no dialogue id, so they
are keyed by the text itself, like menu items. Strings without a letter (numbers, markup) are skipped.
"""
out: list[str] = []
stack = [value]
while stack:
item = stack.pop()
if isinstance(item, list):
stack.extend(reversed(item))
continue
if not isinstance(item, dict):
continue
func = item.get("f")
if (item.get("k") == "call" and isinstance(func, dict) and func.get("k") == "name"
and func.get("id") in ("_", "__") and len(item.get("a") or []) == 1):
arg = item["a"][0]
if isinstance(arg, dict) and arg.get("k") == "const" and isinstance(arg.get("v"), str):
out.append(arg["v"])
if item.get("w") in _SCREEN_TEXT and item.get("args"):
arg = item["args"][0]
if isinstance(arg, dict) and arg.get("k") == "const" and isinstance(arg.get("v"), str):
out.append(arg["v"])
stack.extend(reversed(list(item.values())))
return [text for text in out if text.strip() and _WORD.search(text)]
def text_index(manifest: dict[str, Any]) -> TextIndex:
game_hash = manifest["files"]["game.json"]["hash"]
game = blob_json(game_hash)
@@ -109,7 +142,11 @@ def _build_index(game_hash: str, scripts: tuple[tuple[str, str], ...]) -> TextIn
idx.lines.append(info)
idx.by_tid[info.tid] = info
count += 1
for text in marked_strings(module["labels"]):
idx.strings.setdefault(text, "text")
idx.modules.append((path, count))
for text in marked_strings(game.get("screens") or {}):
idx.strings.setdefault(text, "text")
return idx
+23
View File
@@ -96,3 +96,26 @@ def test_entries_page_locates_a_line_in_the_filtered_list(manifest):
assert tr.entries_page(m, "ru", state="todo", locate=first.tid)["located"] == 0
assert tr.entries_page(m, "ru", locate="")["located"] is None
assert tr.entries_page(m, "ru", kind="strings", locate="Эйлин")["located"] is not None
def _mark(text):
return {"k": "call", "f": {"k": "name", "id": "_"}, "a": [{"k": "const", "v": text}]}
def test_marked_strings_and_screen_texts_become_translatable_strings():
body = [
{"op": "show", "img": {"textExpr": _mark("Do you want a short tutorial?")}},
{"op": "python", "code": [{"k": "assign", "value": _mark("Hi, [name]!")}]},
{"op": "show", "img": {"textExpr": _mark("")}}, # empty: nothing to translate
{"op": "show", "img": {"textExpr": _mark("42")}}, # no letters: not text
{"op": "show", "img": {"textExpr": {"k": "call", "f": {"k": "name", "id": "other"},
"a": [{"k": "const", "v": "Not marked"}]}}},
]
screens = {"menu": {"body": [
{"w": "text", "args": [{"k": "const", "v": "Preferences"}], "props": {}, "body": []},
{"w": "textbutton", "args": [{"k": "const", "v": "Quit"}], "props": {}, "body": []},
{"w": "text", "args": [{"k": "name", "id": "title"}], "props": {}, "body": []}, # variable
{"w": "add", "args": [{"k": "const", "v": "bg.png"}], "props": {}, "body": []}, # not text
]}}
assert tr.marked_strings([{"name": "start", "body": body}]) == ["Do you want a short tutorial?", "Hi, [name]!"]
assert tr.marked_strings(screens) == ["Preferences", "Quit"]