the-apprentice / oracles /ui_strings.py
AndrewRqy
Initial commit — The Apprentice for Build Small
5afb7b3
Raw
History Blame Contribute Delete
16 kB
"""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 <strong>YOU</strong> are the mentor. Inscribe any 5 words "
"you wish. The story-engine will pretend they save the apprentice."
),
"zh": (
"如今 <strong>你</strong> 就是那位"
"导师。随意写下五个字"
"句。故事引擎会让它们"
"成为弟子的救命之言。"
),
},
# ---- 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 <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> 道"
"神谕。前路已被阻断。"
),
},
# ---- 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"