feat: _() strings and screen texts become translatable strings; search looks in all states; stutter hint
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:
1 parent
262db2687e
commit
cc71050362
5 files changed
+73
-1
No files matched your search
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -239,6 +239,11 @@ translate russian strings:
|
||||
переводит одну строку с тем же контекстом и глоссарием и помечает её
|
||||
«Машинный». Редактор ищет переводчик сам, только если он уже работал в этом
|
||||
браузере, чтобы Chrome не спрашивал про локальную сеть без причины;
|
||||
* вкладка «Имена и меню» собирает не только имена, пункты меню и `show text "…"`,
|
||||
но и строки `_("…")` из скриптов и постоянный текст экранов (`text`, `textbutton`,
|
||||
`label`): у них нет идентификатора реплики, поэтому они переводятся по тексту
|
||||
оригинала, как в Ren'Py. Список строится из уже конвертированной версии, повторный
|
||||
импорт игры не нужен;
|
||||
* декоративные теги Ren'Py (`{size=…}`, `{i}`, `{b}`, `{color=…}` и т. п.) внутри
|
||||
слов (`d{size=38}e{/size}ep`) после перевода на место не вернуть, поэтому такие
|
||||
строки уходят модели без этих тегов, а результат помечается «Неточно». Строки с
|
||||
|
||||
@@ -309,6 +309,8 @@ function EntriesView({ versionId, lang, tab, ov, onStats, session, fills, fillTi
|
||||
const t = setTimeout(() => {
|
||||
setQ(query);
|
||||
setPage(0);
|
||||
// looking for a phrase means looking everywhere, not only among what still needs work
|
||||
if (query.trim()) setState((current) => (current === "todo" ? "all" : current));
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
|
||||
@@ -44,6 +44,11 @@ function loadPrefs(): Prefs {
|
||||
}
|
||||
}
|
||||
|
||||
/** Visual novels spell stutters with hyphens and repeated letters; models misread the broken word. */
|
||||
const STUTTER_HINT = "The source may contain stuttering written with hyphens and repeated letters, "
|
||||
+ "like \"c-cer-cervix\" or \"m-my\". First work out the intended word, translate that word correctly "
|
||||
+ "and keep the stutter in the translation (for example \"ш-шейка матки\"). Never translate the broken spelling literally.";
|
||||
|
||||
export type Connection = Link | "idle" | "checking";
|
||||
|
||||
export interface Session {
|
||||
@@ -151,7 +156,7 @@ export function useTranslatorSession(versionId: string | undefined, version: Ver
|
||||
context: [`Game: ${title}.`, prefs.about && g?.description,
|
||||
prefs.about && g?.tags?.length ? `Genres and tags: ${g.tags.join(", ")}.` : ""]
|
||||
.filter(Boolean).join(" ").slice(0, 4000),
|
||||
instructions: instructions.trim(),
|
||||
instructions: [instructions.trim(), STUTTER_HINT].filter(Boolean).join(" "),
|
||||
glossary: Object.fromEntries((meta.glossary ?? []).filter((t) => t.src.trim() && t.t.trim())
|
||||
.map((t) => [t.src.trim(), t.t.trim()])),
|
||||
provider: prefs.provider || "lmstudio",
|
||||
|
||||
Reference in new issue
Block a user