File size: 15,954 Bytes
5afb7b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 | """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"
|