From 0d1498020568d11331fdd89961fe11bc9728fd60 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:55:08 +0200 Subject: [PATCH] Translate the PC's CHANGE BOX save flow (PcMenu.lua) PcMenu.lua's own CHANGE BOX save confirmation stayed in English on every non-English build: its overwrite/saving/done prompts and its YES/NO choice were bare literals, invisible to a translation mod's `strings` registry, even though the overwrite/saving prompts are the exact same two cart messages Gold's SAVE screen (SaveMenu.lua) already routes through Strings(). SaveMenu.lua now exports OVERWRITE_PROMPT_SOURCE, SAVING_PROMPT_SOURCE and twoLines() so PcMenu:savePrompt() can call through them instead of duplicating the two shared prompts. The now-unused plain OVERWRITE_PROMPT/SAVING_PROMPT tables (PcMenu's old direct-read contract) are gone along with them, removing a second copy of the same literal text that had to be kept in sync by hand. The CHANGE BOX confirm prompt itself ("#MON BOX, data\nwill be saved. OK?") gets its own new Strings.source() key -- it is not shared with SaveMenu, and the port's own English text is already a same-shape truncation of the cart's three-line _ChangeBoxSaveText, confirmed against poke-corpus GoldSilver en_msg.txt:4897. The done message and YES/NO now go through the exact same Strings() keys SaveMenu.lua already uses ("%s saved\nthe game.", "Could not save.", "YES", "NO"), so a translation covering SaveMenu's screen needs no PcMenu-specific fork for any of those. Added tests/engine/gen2_pcmenu_changebox_save_translation_test.lua, mirroring gen2_save_menu_translation_test.lua's technique for PcMenu's own CHANGE BOX flow, and updated the stale cross-file-contract regression case in gen2_save_menu_translation_test.lua to match the new shared-export shape instead of the old plain-table one. --- src/ui/gen2/PcMenu.lua | 27 +-- src/ui/gen2/SaveMenu.lua | 39 ++--- ...pcmenu_changebox_save_translation_test.lua | 159 ++++++++++++++++++ .../gen2_save_menu_translation_test.lua | 33 ++-- 4 files changed, 212 insertions(+), 46 deletions(-) create mode 100644 tests/engine/gen2_pcmenu_changebox_save_translation_test.lua diff --git a/src/ui/gen2/PcMenu.lua b/src/ui/gen2/PcMenu.lua index cf365eca..a7fdd744 100644 --- a/src/ui/gen2/PcMenu.lua +++ b/src/ui/gen2/PcMenu.lua @@ -38,9 +38,12 @@ local MON_HOLDING_MAIL = { Strings.source("Please remove the\nMAIL."), } --- _ChangeBoxSaveText (data/text/common_2.asm:1306) is three lines whose `cont` --- has already scrolled by the time YesNoBox goes up over its last two. -local CHANGE_BOX_SAVE = { "#MON BOX, data", "will be saved. OK?" } +-- _ChangeBoxSaveText (data/text/common_2.asm:1306) is three lines whose first +-- `cont` ("When you change a") has already scrolled by the time YesNoBox goes +-- up over its last two -- confirmed against poke-corpus GoldSilver +-- en_msg.txt:4897. One \n-joined translatable key, same pattern as +-- SaveMenu.lua's OVERWRITE_PROMPT_SOURCE/SAVING_PROMPT_SOURCE. +local CHANGE_BOX_SAVE_SOURCE = Strings.source("#MON BOX, data\nwill be saved. OK?") -- YesNoBox's own `lb bc, SCREEN_WIDTH - 6, 7` (home/menu.asm:382-383). local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5 @@ -212,16 +215,20 @@ function PcMenu:writeChangeBox() end function PcMenu:savePrompt() - if self.savePhase == "overwrite" then return SaveMenu.OVERWRITE_PROMPT end - if self.savePhase == "saving" then return SaveMenu.SAVING_PROMPT end + if self.savePhase == "overwrite" then + return SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE)) + end + if self.savePhase == "saving" then + return SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE)) + end if self.savePhase == "done" then if self.saved then local name = (self.save.player and self.save.player.name) or "GOLD" - return { name .. " saved", "the game." } + return SaveMenu.twoLines(Strings("%s saved\nthe game.", name)) end - return { "Could not save.", "" } + return SaveMenu.twoLines(Strings("Could not save.")) end - return CHANGE_BOX_SAVE + return SaveMenu.twoLines(Strings(CHANGE_BOX_SAVE_SOURCE)) end function PcMenu:updateChangeBox() @@ -404,8 +411,8 @@ function PcMenu:drawPanel() Chrome.print(lines[2] or "", 1, 16) if self.savePhase == "confirm" or self.savePhase == "overwrite" then Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H) - Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1) - Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3) + Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1) + Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3) Chrome.cursor(YESNO_X + 1, YESNO_Y + (self.saveChoice == 1 and 1 or 3)) end diff --git a/src/ui/gen2/SaveMenu.lua b/src/ui/gen2/SaveMenu.lua index d3e0eacc..bbcfd70e 100644 --- a/src/ui/gen2/SaveMenu.lua +++ b/src/ui/gen2/SaveMenu.lua @@ -57,27 +57,21 @@ local TIME_X, TIME_Y = 13, 8 local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5 -- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and --- SavingDontTurnOffThePower's own line, shared with the PC's CHANGE BOX save --- (src/ui/gen2/PcMenu.lua:savePrompt() reads these two tables' lines[1]/ --- lines[2] directly, so their shape is a cross-file contract: keep them --- plain, untranslated tables). -local OVERWRITE_PROMPT = { "There is already a", "save file. Is it" } -local SAVING_PROMPT = { "SAVING… DON'T TURN", "OFF THE POWER." } - --- Translatable copies of the two prompts above, one \n-joined key each, used --- only by this screen's own prompt() below. One key per prompt lets a --- translation write one whole, freely reordered sentence instead of two --- fragments translated in isolation, and lets a cart whose own text is a --- single line (German's SAVING prompt) say so directly by simply omitting --- the "\n" -- the per-line override style used elsewhere requires a --- non-empty value for every line, so it can't express "this line is blank". +-- SavingDontTurnOffThePower's own line -- one \n-joined translatable key +-- each, used both by this screen's own prompt() below and, through the +-- SOURCE/twoLines() exports at the bottom of this file, by the PC's CHANGE +-- BOX save (src/ui/gen2/PcMenu.lua:savePrompt()), which shares these exact +-- same two cart messages. One key per prompt lets a translation write one +-- whole, freely reordered sentence instead of two fragments translated in +-- isolation, and lets a cart whose own text is a single line (German's +-- SAVING prompt) say so directly by simply omitting the "\n" -- the +-- per-line override style used elsewhere requires a non-empty value for +-- every line, so it can't express "this line is blank". -- --- Written as literals, not `table.concat(OVERWRITE_PROMPT, "\n")`: the --- translation tooling's string harvester only recognizes a literal inside --- Strings.source(...), not a computed expression, so a concat call here --- would quietly never reach a translator. Keep byte-for-byte in sync with --- OVERWRITE_PROMPT/SAVING_PROMPT above (checked by --- tests/engine/gen2_save_menu_translation_test.lua). +-- Written as a literal, not built from a table: the translation tooling's +-- string harvester only recognizes a literal inside Strings.source(...), +-- not a computed expression, so a concat call here would quietly never +-- reach a translator. local OVERWRITE_PROMPT_SOURCE = Strings.source("There is already a\nsave file. Is it") local SAVING_PROMPT_SOURCE = Strings.source("SAVING… DON'T TURN\nOFF THE POWER.") @@ -279,7 +273,8 @@ end SaveMenu.SFX_SAVE = SFX_SAVE SaveMenu.SAVING_FRAMES = SAVING_FRAMES SaveMenu.SAVED_FRAMES = SAVED_FRAMES -SaveMenu.OVERWRITE_PROMPT = OVERWRITE_PROMPT -SaveMenu.SAVING_PROMPT = SAVING_PROMPT +SaveMenu.OVERWRITE_PROMPT_SOURCE = OVERWRITE_PROMPT_SOURCE +SaveMenu.SAVING_PROMPT_SOURCE = SAVING_PROMPT_SOURCE +SaveMenu.twoLines = twoLines return SaveMenu diff --git a/tests/engine/gen2_pcmenu_changebox_save_translation_test.lua b/tests/engine/gen2_pcmenu_changebox_save_translation_test.lua new file mode 100644 index 00000000..ef58dba7 --- /dev/null +++ b/tests/engine/gen2_pcmenu_changebox_save_translation_test.lua @@ -0,0 +1,159 @@ +-- The PC's CHANGE BOX save flow (src/ui/gen2/PcMenu.lua:savePrompt()) used to +-- draw its overwrite/saving/done prompts and its YES/NO choice as bare +-- literals, invisible to a translation mod's `strings` registry, even though +-- the overwrite/saving prompts are the exact same two cart messages Gold's +-- SAVE screen (src/ui/gen2/SaveMenu.lua) already routes through Strings(). +-- Same technique as tests/engine/gen2_save_menu_translation_test.lua: drives +-- PcMenu:drawPanel() directly at each save phase with a mod-loaded Strings +-- catalog and checks the translated text reaches Font.draw. +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") + +love = require("tests.love_stub") + +require("src.core.Logger").warn = function() end + +local drawn +package.loaded["src.render.Font"] = { + draw = function(text, x, y) + drawn[#drawn + 1] = { text = text, x = x, y = y } + end, + drawCode = function() end, + drawBox = function() end, + width = function() return 0 end, +} + +local PcMenu = require("src.ui.gen2.PcMenu") +local Strings = require("src.core.Strings") + +local function drawnAt(x, y) + for _, d in ipairs(drawn) do + if d.x == x and d.y == y then return d.text end + end + return nil +end + +-- Chrome.print multiplies tile coordinates by 8 (src/ui/gen2/Chrome.lua). +-- The save-prompt box sits at the same (0,12) origin SaveMenu.lua's does, so +-- its two lines print at the same (1,14)/(1,16); PcMenu's own YESNO_X/Y +-- (14,7) differ from SaveMenu's (0,7), so YES/NO print at (16,8)/(16,10). +local PROMPT1_X, PROMPT1_Y = 1 * 8, 14 * 8 +local PROMPT2_X, PROMPT2_Y = 1 * 8, 16 * 8 +local YES_X, YES_Y = 16 * 8, 8 * 8 +local NO_X, NO_Y = 16 * 8, 10 * 8 + +-- One party mon so Boxes.canUsePc doesn't refuse to open the PC at all. +local SAVE = { player = { name = "GOLD" }, party = { {} } } + +local function newMenu() + return PcMenu.new({}, { + save = SAVE, + saveExists = false, + writer = function() return true end, + }) +end + +-- ---------------------------------------------- vanilla: no mod catalog +do + local menu = newMenu() + menu.picking = true + menu.pickIndex = 1 + menu.savePhase = "confirm" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "#MON BOX, data", "the confirm prompt draws in English with no mod loaded") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "will be saved. OK?", "and its second line") + T.eq(drawnAt(YES_X, YES_Y), "YES", "and YES") + T.eq(drawnAt(NO_X, NO_Y), "NO", "and NO") + + menu.savePhase = "overwrite" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "There is already a", + "the overwrite prompt, the same cart message SaveMenu.lua's SAVE screen shares") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save file. Is it", "its second line") + + menu.savePhase = "saving" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "SAVING… DON'T TURN", "the saving message") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "OFF THE POWER.", "its second line") + + menu.savePhase, menu.saved = "done", true + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "GOLD saved", "the saved message") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "the game.", "its second line") + + menu.savePhase, menu.saved = "done", false + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Could not save.", "the failed-save message") +end + +-- ------------------------------------------------- a translation mod's turn +-- +-- Same catalog values as gen2_save_menu_translation_test.lua's own +-- translated block: the overwrite/saving prompts, the saved/failed messages, +-- and YES/NO are the exact same keys both screens read, so one translation +-- covers both without a PcMenu-specific fork. Only the CHANGE BOX confirm +-- prompt's key is new here. +do + Strings.load({ + strings = { + ["YES"] = "OUI", + ["NO"] = "NON", + ["#MON BOX, data\nwill be saved. OK?"] = "Les donnees de la\nBOITE seront sauv.", + ["There is already a\nsave file. Is it"] = "Un fichier existe\ndeja. Est-ce", + ["SAVING… DON'T TURN\nOFF THE POWER."] = "SAUVEGARDE...\nN'ETEIGNEZ PAS.", + ["%s saved\nthe game."] = "%s a sauvegarde\nla partie.", + ["Could not save."] = "Echec de sauvegarde.", + }, + }) + + local menu = newMenu() + menu.picking = true + menu.pickIndex = 1 + menu.savePhase = "confirm" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Les donnees de la", "the confirm prompt") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "BOITE seront sauv.", "its second line") + T.eq(drawnAt(YES_X, YES_Y), "OUI", "and YES") + T.eq(drawnAt(NO_X, NO_Y), "NON", "and NO") + + menu.savePhase = "overwrite" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Un fichier existe", + "the overwrite prompt, translated with no PcMenu-specific key") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "deja. Est-ce", "its second line") + + menu.savePhase = "saving" + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "SAUVEGARDE...", "the saving message") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "N'ETEIGNEZ PAS.", "its second line") + + menu.savePhase, menu.saved = "done", true + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "GOLD a sauvegarde", + "the saved message folds the player name into the mod's own word order") + T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "la partie.", "its second line") + + menu.savePhase, menu.saved = "done", false + drawn = {} + menu:drawPanel() + T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Echec de sauvegarde.", "the failed-save message") + + -- Module state is process-global (see tests/gen2_clock_test.lua's own + -- note); this suite gets its own process from tests/tier_runner.lua, but + -- leaving the catalog loaded past this point would still mistranslate + -- every check below it in this file. + Strings.load({}) + T.check(not Strings.active(), "the catalog is unloaded for the checks after this one") +end + +T.finish("gen2_pcmenu_changebox_save_translation_test") diff --git a/tests/engine/gen2_save_menu_translation_test.lua b/tests/engine/gen2_save_menu_translation_test.lua index c560d0bf..91dad6af 100644 --- a/tests/engine/gen2_save_menu_translation_test.lua +++ b/tests/engine/gen2_save_menu_translation_test.lua @@ -147,21 +147,26 @@ do T.check(not Strings.active(), "the catalog is unloaded for the checks after this one") end --- src/ui/gen2/PcMenu.lua:savePrompt() returns SaveMenu.OVERWRITE_PROMPT/ --- SAVING_PROMPT straight through to its own `lines[1]`/`lines[2]` --- Chrome.print calls (the PC's CHANGE BOX save uses the same two prompts). --- Indexing a plain string with [1]/[2] returns nil, not characters, so this --- shape is a cross-file contract: it caught a real regression during review, --- where routing these through a single Strings.source()-wrapped string (to --- translate SaveMenu's own screen) silently turned them into non-table --- values and left PcMenu's overwrite/saving prompt blank. +-- src/ui/gen2/PcMenu.lua:savePrompt() shares SaveMenu's overwrite/saving +-- prompts through SaveMenu.OVERWRITE_PROMPT_SOURCE/SAVING_PROMPT_SOURCE and +-- SaveMenu.twoLines(), rather than duplicating them -- both exported below, +-- both used by PcMenu's own translation test +-- (tests/engine/gen2_pcmenu_changebox_save_translation_test.lua). Checked +-- here that they stay callable the shape twoLines() expects: a table in, +-- one \n-joined string out with the split back on load. do - T.eq(type(SaveMenu.OVERWRITE_PROMPT), "table", "OVERWRITE_PROMPT stays a table for PcMenu.lua") - T.eq(SaveMenu.OVERWRITE_PROMPT[1], "There is already a", "and its first line stays indexable") - T.eq(SaveMenu.OVERWRITE_PROMPT[2], "save file. Is it", "and its second line") - T.eq(type(SaveMenu.SAVING_PROMPT), "table", "SAVING_PROMPT stays a table for PcMenu.lua") - T.eq(SaveMenu.SAVING_PROMPT[1], "SAVING… DON'T TURN", "and its first line stays indexable") - T.eq(SaveMenu.SAVING_PROMPT[2], "OFF THE POWER.", "and its second line") + T.eq(SaveMenu.OVERWRITE_PROMPT_SOURCE, "There is already a\nsave file. Is it", + "OVERWRITE_PROMPT_SOURCE stays the cart's own \\n-joined text") + T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[1], "There is already a", + "and twoLines() splits its untranslated fallback back to the first line") + T.eq(SaveMenu.twoLines(Strings(SaveMenu.OVERWRITE_PROMPT_SOURCE))[2], "save file. Is it", + "and its second line") + T.eq(SaveMenu.SAVING_PROMPT_SOURCE, "SAVING… DON'T TURN\nOFF THE POWER.", + "SAVING_PROMPT_SOURCE stays the cart's own \\n-joined text") + T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[1], "SAVING… DON'T TURN", + "and twoLines() splits its untranslated fallback back to the first line") + T.eq(SaveMenu.twoLines(Strings(SaveMenu.SAVING_PROMPT_SOURCE))[2], "OFF THE POWER.", + "and its second line") end -- A translation with a THIRD line (a second embedded "\n") has nowhere on