"""Bilingual UI strings table for The Mentor's Oracles. Centralizes every player-facing UI label, button, info text, badge, heading, error message and template paragraph so the UI can flip between English and Simplified Chinese based on `GameState.lang`. Dynamic narrative output from the LLM (obstacle setup, narration, tactic, interlude, epilogue) is NOT routed through this table — those flow through the `{language}` placeholder in the prompt templates instead. Same with the mentor's grimoire soliloquy, which is loaded per-language from ``oracles/mentor_text.py``. Public API: * ``UI_STRINGS``: dict[key -> dict[lang -> str]] * ``t(key, lang="en")``: localized lookup, English fallback * ``tlang(state)``: read ``state.lang`` safely, default "en" Keys use ``msg_…`` for transient processing strings, ``btn_…`` for button labels, ``label_…`` for component labels, ``info_…`` for component help text, ``ph_…`` for placeholders, ``badge_…`` for status chips, ``heading_…`` for headings, ``err_…`` for validation errors, ``tmpl_…`` for template paragraphs. Placeholders inside the strings (e.g. ``{hero}``, ``{step}``, ``{GRIMOIRE_NUM_STEPS}``) are filled by the caller via ``str.format`` — do NOT pre-format here. """ from __future__ import annotations from typing import Any UI_STRINGS: dict[str, dict[str, str]] = { # ---- Grimoire / inscribe phase ---------------------------------------- "grimoire_step_marker": { "en": "{step:02d} / {GRIMOIRE_NUM_STEPS:02d}", "zh": "第 {step:02d} / {GRIMOIRE_NUM_STEPS:02d} 页", }, "blank_oracle": { "en": "[blank]", "zh": "[空白]", }, "alt_sealed_grimoire": { "en": "the sealed grimoire", "zh": "已封缄的卷书", }, "summary_five_sealed": { "en": "The five sealed oracles:", "zh": "已封缄的五道神谕:", }, # ---- Prologue ---------------------------------------------------------- "prologue_title": { "en": "~~ THE PROLOGUE ~~", "zh": "~~ 序章 ~~", }, # ---- Pipeline-demo card ----------------------------------------------- "demo_mode": { "en": "Mode {mode}", "zh": "第 {mode} 式", }, "demo_tag_inscribes": { "en": "\U0001f4dc He inscribes", "zh": "\U0001f4dc 他写下", }, "demo_tag_meets": { "en": "⚔️ The apprentice meets", "zh": "⚔️ 弟子遭遇", }, "demo_tag_somehow": { "en": "✨ Somehow", "zh": "✨ 不知怎的", }, "demo_header": { "en": "WHAT THE MENTOR IMAGINES MIGHT HAPPEN…", "zh": "导师设想可能会发生的事……", }, "demo_mode_a_label": { "en": "Wild Imagination", "zh": "天马行空", }, "demo_mode_b_label": { "en": "Accidental Trip", "zh": "阴差阳错", }, "demo_mode_c_label": { "en": "Last-Minute Revelation", "zh": "灵光乍现", }, "demo_footer": { "en": ( "Now YOU are the mentor. Inscribe any 5 words " "you wish. The story-engine will pretend they save the apprentice." ), "zh": ( "如今 就是那位" "导师。随意写下五个字" "句。故事引擎会让它们" "成为弟子的救命之言。" ), }, # ---- Inscribe decoration (the YOU / YOUR APPRENTICE figs) ---------------- "figure_you": { "en": "YOU", "zh": "你", }, "figure_apprentice": { "en": "YOUR APPRENTICE", "zh": "你的弟子", }, # ---- Banner fallback subtitle ---------------------------------------- "banner_default_subtitle": { "en": "You inscribe the words. The apprentice must make them save his life.", "zh": "字句由你写下。弟子须" "以这些字句保住性命。", }, # ---- Badge / status chip ---------------------------------------------- "badge_inscribing": { "en": "INSCRIBING", "zh": "书写中", }, "badge_sealed": { "en": "SEALED", "zh": "已封缄", }, "badge_trial": { "en": "TRIAL {current} / {total}", "zh": "第 {current} / {total} 难", }, "badge_return": { "en": "RETURN", "zh": "归途", }, "badge_done": { "en": "DONE", "zh": "已毕", }, # ---- Send-off / sealed-list panel ------------------------------------- "blank_silence": { "en": "[silence]", "zh": "[沉默]", }, "sealed_list_header": { "en": "The five oracles, sealed:", "zh": "五道神谕,已封缄:", }, "tmpl_send_off_narration": { "en": ( "{hero} kneels by the well at dawn. You — the anonymous " "mentor — place the five sealed oracles in his pack and " "step back into the mist. He does not see your face. He rises, " "takes the road south, and the village of {village} falls " "behind him." ), "zh": ( "{hero} 在黎明时分跪于井" "边。你——那位无名的" "导师——将五道封缄的" "神谕放入他的行囊,退" "入薄雾之中。他未曾见" "过你的面容。他起身南" "行,{village} 在他身后渐行" "渐远。" ), }, # ---- Trial heading + await -------------------------------------------- "trial_dragon_title": { "en": "Trial {n}: The Dragon", "zh": "第 {n} 难:巨龙", }, "trial_normal_title": { "en": "Trial {n} of {total}", "zh": "第 {n} 难,共 {total} 难", }, "trial_remaining_one": { "en": ( "He has {remaining} oracle left. " "The path forward is barred." ), "zh": ( "他还剩 {remaining} 道" "神谕。前路已被阻断。" ), }, "trial_remaining_many": { "en": ( "He has {remaining} oracles left. " "The path forward is barred." ), "zh": ( "他还剩 {remaining} 道" "神谕。前路已被阻断。" ), }, # ---- Trial reveal ------------------------------------------------------ "oracle_label_sealed_since": { "en": "Oracle {roman} (sealed since the village):", "zh": "第 {roman} 道神谕(自村庄" "起便已封缄):", }, "parchment_blank_quote": { "en": "[the parchment was blank]", "zh": "[这张羊皮卷是空白的]", }, "tactic_prefix": { "en": "Tactic: {tactic}", "zh": "策略:{tactic}", }, # ---- Chronicle --------------------------------------------------------- "chronicle_empty": { "en": "_The chronicle is empty._", "zh": "_史册尚未落墨。_", }, "chronicle_trial_heading": { "en": "### Trial {n}", "zh": "### 第 {n} 难", }, "chronicle_obstacle": { "en": "**Obstacle:** {ob_setup}", "zh": "**险境:** {ob_setup}", }, "chronicle_oracle": { "en": "**Oracle {roman}:** _{oracle_text}_", "zh": "**第 {roman} 道神谕:** _{oracle_text}_", }, "chronicle_tactic": { "en": "**Tactic:** {tactic}", "zh": "**策略:** {tactic}", }, # ---- Epilogue ---------------------------------------------------------- "epilogue_heading_return": { "en": "Return", "zh": "归途", }, # ---- Validation errors ------------------------------------------------- "err_hero_blank": { "en": "The parchment is blank. The apprentice needs a name.", "zh": "羊皮卷尚是一片空白。" "这位弟子需要一个名字。", }, "err_village_blank": { "en": "Every apprentice comes from somewhere. Name the place.", "zh": "每位弟子皆有所来之处。" "为那地方起个名吧。", }, "err_parchment_blank": { "en": "The {ord_word} parchment cannot be blank. Inscribe at least one mark.", "zh": "第{ord_word}卷羊皮不可空白。" "至少落下一笔。", }, # English ordinal words used inside err_parchment_blank's English form. # The Chinese version uses the numeric character so we map each to its # localized equivalent here. "ord_first": {"en": "first", "zh": "一"}, "ord_second": {"en": "second", "zh": "二"}, "ord_third": {"en": "third", "zh": "三"}, "ord_fourth": {"en": "fourth", "zh": "四"}, "ord_fifth": {"en": "fifth", "zh": "五"}, "ord_next": {"en": "next", "zh": "下一"}, # ---- Processing-overlay captions --------------------------------------- "msg_walking_east": { "en": "The apprentice walks east along the dawn road…", "zh": "弟子沿着拂晓之路向" "东而行……", }, "msg_reading_oracle": { "en": "The apprentice reads the oracle aloud…", "zh": "弟子朗读神谕……", }, "msg_oracle_quivers": { "en": ( "The oracle quivers and goes still. The mentor's voice does " "not arrive: {e}" ), "zh": ( "神谕颤动,随即归于死" "寂。导师的话音未至:{e}" ), }, "msg_no_tactic": { "en": "(no tactic — the LLM could not be reached)", "zh": "(无策略——无法连" "接到大模型)", }, "msg_trial_caption_fallback": { "en": "Trial {n}: {tactic}", "zh": "第 {n} 难:{tactic}", }, "msg_walking_between_trials": { "en": "The apprentice walks the road between trials…", "zh": "弟子走在两难之间的路" "上……", }, "msg_climbing_final": { "en": "The apprentice climbs toward the final reckoning…", "zh": "弟子向那最后的清算攀" "登……", }, "msg_road_silent": { "en": "[the road between trials is silent — {e}]", "zh": "[两难之间的路途寂然无" "声——{e}]", }, "msg_mentor_silent": { "en": "[the mentor is silent — {e}]", "zh": "[导师无言——{e}]", }, # ---- Grimoire spread-0 dropdowns / info text -------------------------- # NOTE: the picker LABELS for language and theme stay bilingual # ("Tongue / 语言", "World / 世界") because they are seen BEFORE # the language has been chosen. Only the INFO body is localized here. "info_language_dropdown": { "en": "The mentor will write in this tongue for the rest of the tale.", "zh": "此后整个故事,导师都" "将以此语言书写。", }, "info_theme_dropdown": { "en": ( "The shape of the world the apprentice walks. Each setting has its " "own mentor, obstacles, and final reckoning." ), "zh": ( "弟子所行世界之形貌。" "每一设定皆有其专属的" "导师、险境与最终之劫" "。" ), }, "label_narration_length": { "en": "Narration length per trial", "zh": "每一难的叙述长度", }, "info_narration_length": { "en": ( "How many words you want the mentor to spin per trial. " "Shorter = snappier, longer = more vivid. You can rerun the " "journey with a different choice." ), "zh": ( "你希望导师在每一难中" "编织多少字句。越短越" "紧凑,越长越生动。你" "可以选择不同长度再走" "一遍旅程。" ), }, # ---- Grimoire button labels ------------------------------------------- "btn_inscribe_choice": { "en": "Inscribe my choice →", "zh": "印下我的选择 →", }, "label_hero_name": { "en": "The apprentice's name", "zh": "弟子之名", }, "ph_hero_name": { "en": "A name a dragon would forget…", "zh": "一个连巨龙也会忘记的" "名字……", }, "btn_name_him": { "en": "Name him →", "zh": "为他起名 →", }, "label_village": { "en": "His village", "zh": "他的村庄", }, "ph_village": { "en": "A place with a well and a bell…", "zh": "一个有水井与钟声的地" "方……", }, "btn_place_him": { "en": "Place him →", "zh": "为他定居 →", }, "label_oracle_1": { "en": "The first parchment", "zh": "第一卷羊皮", }, "ph_oracle_1": { "en": "Anything you write may save him.", "zh": "你写下的任何字句皆可" "救他一命。", }, "btn_seal_first": { "en": "Seal the first →", "zh": "封缄第一卷 →", }, "label_oracle_2": { "en": "The second parchment", "zh": "第二卷羊皮", }, "btn_seal_second": { "en": "Seal the second →", "zh": "封缄第二卷 →", }, "label_oracle_3": { "en": "The third parchment", "zh": "第三卷羊皮", }, "btn_seal_third": { "en": "Seal the third →", "zh": "封缄第三卷 →", }, "label_oracle_4": { "en": "The fourth parchment", "zh": "第四卷羊皮", }, "btn_seal_fourth": { "en": "Seal the fourth →", "zh": "封缄第四卷 →", }, "label_oracle_5": { "en": "The fifth (and final) parchment", "zh": "第五卷(也是最后一卷)羊皮", }, "ph_oracle_5": { "en": "He will open this at the dragon.", "zh": "他将在巨龙面前开启此卷。", }, "btn_seal_last": { "en": "Seal the last →", "zh": "封缄末卷 →", }, "btn_let_journey_begin": { "en": "Let the journey begin →", "zh": "旅程启航 →", }, # ---- Trial action buttons --------------------------------------------- "btn_open_oracle": { "en": "He opens one of the mentor's oracles.", "zh": "他启封导师的一道神谕。", }, "btn_continue": { "en": "Continue.", "zh": "继续。", }, # ---- Epilogue chrome -------------------------------------------------- "label_chronicle_accordion": { "en": "Chronicle", "zh": "史册", }, "btn_new_tale": { "en": "Begin a new tale", "zh": "开启新的传说", }, "btn_to_chronicle": { "en": "Continue to the chronicle", "zh": "翻阅史册", }, } def t(key: str, lang: str = "en") -> str: """Return the localized string for `key` in `lang`. Falls back to the English string when `lang` has no entry. If the key itself is unknown, returns the key (so callers see a clear miss instead of an empty string). """ entry = UI_STRINGS.get(key) if entry is None: return key val = entry.get(lang) if val is not None: return val return entry.get("en", key) def tlang(state: Any) -> str: """Read the language code from a GameState (or "en" if no state). Defensive: handles ``None`` state and a state object missing ``lang`` so call sites in render helpers don't have to guard. """ if state is None: return "en" lang = getattr(state, "lang", None) or "en" return lang if lang in ("en", "zh") else "en"