| """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_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_title": { |
| "en": "~~ THE PROLOGUE ~~", |
| "zh": "~~ 序章 ~~", |
| }, |
|
|
| |
| "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 <strong>YOU</strong> are the mentor. Inscribe any 5 words " |
| "you wish. The story-engine will pretend they save the apprentice." |
| ), |
| "zh": ( |
| "如今 <strong>你</strong> 就是那位" |
| "导师。随意写下五个字" |
| "句。故事引擎会让它们" |
| "成为弟子的救命之言。" |
| ), |
| }, |
|
|
| |
| "figure_you": { |
| "en": "YOU", |
| "zh": "你", |
| }, |
| "figure_apprentice": { |
| "en": "YOUR APPRENTICE", |
| "zh": "你的弟子", |
| }, |
|
|
| |
| "banner_default_subtitle": { |
| "en": "You inscribe the words. The apprentice must make them save his life.", |
| "zh": "字句由你写下。弟子须" |
| "以这些字句保住性命。", |
| }, |
|
|
| |
| "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": "已毕", |
| }, |
|
|
| |
| "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_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 <strong>{remaining}</strong> oracle left. " |
| "The path forward is barred." |
| ), |
| "zh": ( |
| "他还剩 <strong>{remaining}</strong> 道" |
| "神谕。前路已被阻断。" |
| ), |
| }, |
| "trial_remaining_many": { |
| "en": ( |
| "He has <strong>{remaining}</strong> oracles left. " |
| "The path forward is barred." |
| ), |
| "zh": ( |
| "他还剩 <strong>{remaining}</strong> 道" |
| "神谕。前路已被阻断。" |
| ), |
| }, |
|
|
| |
| "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_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_heading_return": { |
| "en": "Return", |
| "zh": "归途", |
| }, |
|
|
| |
| "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}卷羊皮不可空白。" |
| "至少落下一笔。", |
| }, |
|
|
| |
| |
| |
| "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": "下一"}, |
|
|
| |
| "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}]", |
| }, |
|
|
| |
| |
| |
| |
| "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": ( |
| "你希望导师在每一难中" |
| "编织多少字句。越短越" |
| "紧凑,越长越生动。你" |
| "可以选择不同长度再走" |
| "一遍旅程。" |
| ), |
| }, |
|
|
| |
| "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": "旅程启航 →", |
| }, |
|
|
| |
| "btn_open_oracle": { |
| "en": "He opens one of the mentor's oracles.", |
| "zh": "他启封导师的一道神谕。", |
| }, |
| "btn_continue": { |
| "en": "Continue.", |
| "zh": "继续。", |
| }, |
|
|
| |
| "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" |
|
|