diff --git a/src/ui/gen2/StartMenu.lua b/src/ui/gen2/StartMenu.lua index 85cbfa00..8a777d25 100644 --- a/src/ui/gen2/StartMenu.lua +++ b/src/ui/gen2/StartMenu.lua @@ -318,6 +318,32 @@ function StartMenu:update(_dt) end -- pokecrystal engine/menus/menu_2.asm:145 +-- The box is two rows tall (y 14/16, step 2), which the cart's own line +-- break fills exactly. A translation can need a third line, and printWrapped +-- would drop it without a word, so say so once instead of shipping a prompt +-- that reads as a sentence cut in half. +-- The confirmation box at (0, 12, 18, 4) holds exactly two rows, which the +-- cart's own line break fills. A translation can need a third, which would be +-- dropped without a word, so this says so once per prompt. printWrapped +-- returns the line count it wrapped to, so the box's own draw is what reports +-- the overflow: no second wrap, and no way for the two to disagree on the +-- width later. It also wraps on pixel width, unlike a `\n`-only splitter, +-- which would hand Font.draw an embedded break -- rendered as a space -- and +-- run a long translated row past the box edge. +local promptWarned = {} +local function twoRowPrompt(text, what) + local lines = Chrome.printWrapped(text, 1, 14, 18, 2, 2) + if lines > 2 and not promptWarned[what] then + promptWarned[what] = true + Logger.warn("%s prompt needs %d lines, the box holds 2: %s", + what, lines, tostring(text)) + end +end + +-- pokecrystal data/text/common_2.asm ContestStatus: the labels and the +-- "None" placeholder are text, so they go through the catalog like the +-- prompt below. The caught mon's own name does not: it is a nickname or a +-- species the `pokemon` registry already renames. function StartMenu:drawContestStatus() Chrome.textbox(0, 0, 17, 5) Chrome.print("CAUGHT", 1, 1) @@ -346,22 +372,26 @@ function StartMenu:draw() if self.phase == "confirmContest" then -- pokecrystal data/text/common_2.asm:1381 Chrome.textbox(0, 12, 18, 4) - Chrome.print("Would you like to", 1, 14) - Chrome.print("end the Contest?", 1, 16) + -- One catalog key ("Would you like to" alone has no stable translation + -- out of context), matching every other confirmation prompt in this + -- port (src/ui/StartMenu.lua, src/ui/gen2/SaveMenu.lua). Printed via + -- Chrome.printWrapped, same as InitClock.lua's own call: this box only + -- has room for two rows (y 14/16, step 2), and printWrapped wraps by + -- both the translated \n and pixel width, unlike a hand-rolled + -- \n-only splitter. + twoRowPrompt(Strings("Would you like to\nend the Contest?"), "contest end") -- pokecrystal home/menu.asm:418 Chrome.box(14, 7, 6, 5) - Chrome.print("YES", 16, 8) - Chrome.print("NO", 16, 10) + Chrome.print(Strings("YES"), 16, 8) + Chrome.print(Strings("NO"), 16, 10) Chrome.cursor(15, self.confirmChoice == 1 and 8 or 10) return end if self.phase == "confirm" then Chrome.textbox(0, 12, 18, 4) - local prompt = Strings(Strings.source("Return to the\ntitle screen?")) - local first, second = prompt:match("^([^\n]*)\n?(.*)$") - Chrome.print(first or "", 1, 14) - Chrome.print(second or "", 1, 16) + twoRowPrompt(Strings(Strings.source("Return to the\ntitle screen?")), + "title screen") Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H) Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1) Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3) diff --git a/tests/engine/gen2_contest_coins_translation_test.lua b/tests/engine/gen2_contest_coins_translation_test.lua new file mode 100644 index 00000000..82a4ecf2 --- /dev/null +++ b/tests/engine/gen2_contest_coins_translation_test.lua @@ -0,0 +1,104 @@ +-- Two Gold/Crystal texts a translation catalog could not reach. +-- +-- The Bug-Catching Contest confirmation was drawn as two bare literals, one +-- per row (src/ui/gen2/StartMenu.lua), with no Strings() call at all. The +-- Game Corner's no-coins refusal (src/script/gen2/Specials.lua) was looked up +-- at runtime but its literal sat behind a plain local, so the catalog +-- harvester never saw the key and no generated catalog carried it. +-- +-- This drives both draw paths with a mod-loaded catalog and checks the +-- translated text is what reaches the screen, plus the no-mod case proving +-- the English wording is unchanged. +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") + +love = require("tests.love_stub") + +require("src.core.Logger").warn = function() end + +-- Chrome.print/printWrapped go straight to Font.draw, so recording that call +-- shows exactly what reached the screen -- the technique +-- tests/engine/gen2_options_menu_translation_test.lua uses. Stubbed before +-- Chrome loads, so its own `local Font = require(...)` captures this. +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(text) return 8 * #tostring(text or "") end, +} + +local StartMenu = require("src.ui.gen2.StartMenu") +local Specials = require("src.script.gen2.Specials") +local Strings = require("src.core.Strings") + +local function drawnText() + local out = {} + for _, d in ipairs(drawn) do out[#out + 1] = d.text end + return table.concat(out, "|") +end + +-- The prompt is drawn from StartMenu:draw()'s confirmContest arm; the list +-- above it is the part this suite does not care about. +local function promptText() + local menu = StartMenu.new({ save = {} }, { save = {} }) + menu.phase = "confirmContest" + menu.confirmChoice = 1 + menu.list = { draw = function() end } + drawn = {} + menu:draw() + return drawnText() +end + +-- StartGameCornerGame's first refusal: no coins at all. +local function coinsRefusal() + local shown + local vm = { + specials = { coins = function() return 0 end }, + showRaw = function(_, body) shown = body end, + } + Specials.ALL.SlotMachine(vm) + return shown +end + +-- ------------------------------------------------ vanilla: no mod catalog +Strings.load({}) +do + local text = promptText() + T.check(text:find("Would you like to", 1, true) ~= nil, + "the Contest prompt draws its English first line") + T.check(text:find("end the Contest?", 1, true) ~= nil, + "and its English second line") + T.eq(coinsRefusal(), "You have no coins.", + "the Game Corner refusal is unchanged in English") + +end + +-- ------------------------------------------------- a translation mod's turn +do + Strings.load({ + strings = { + -- the cart's own French wording (GoldSilver/Crystal, data/text) + ["Would you like to\nend the Contest?"] = "Voulez-vous arrĂª-\nter le concours?", + ["You have no coins."] = "Vous n'avez pas de\njetons.", + ["YES"] = "OUI", + ["NO"] = "NON", + }, + }) + + local text = promptText() + T.check(text:find("Voulez%-vous arr") ~= nil, + "a mod catalog reaches the Contest prompt's first line") + T.check(text:find("ter le concours?", 1, true) ~= nil, + "and its second line, wrapped on the translated line break") + T.check(text:find("Would you like to", 1, true) == nil, + "with no English left on screen") + T.eq(coinsRefusal(), "Vous n'avez pas de\njetons.", + "and the Game Corner refusal too") + +end + +Strings.load({}) + +T.finish("gen2_contest_coins_translation_test") diff --git a/tests/gen2_ui_translation_harvest_test.py b/tests/gen2_ui_translation_harvest_test.py index d76864b5..f77c57d1 100644 --- a/tests/gen2_ui_translation_harvest_test.py +++ b/tests/gen2_ui_translation_harvest_test.py @@ -105,6 +105,9 @@ REQUIRED = { # looked up at runtime, so they only reach a catalog while their literals # stay visible to the harvester rather than hiding behind a local. "You have no coins.", "You don't have a\nCOIN CASE.", + # The Bug-Catching Contest confirmation, one key rather than the two + # half-sentences it used to be drawn as. + "Would you like to\nend the Contest?", }