Route the two-row confirmation prompts through Strings()

The Bug-Catching Contest confirmation was drawn as two bare literals, one per row, with no hook a mod could reach. It becomes one catalog key, "Would you like to\nend the Contest?", the way every other confirmation prompt in this port is written (src/ui/StartMenu.lua, src/ui/gen2/SaveMenu.lua): "Would you like to" alone has no stable translation out of context, and the cart writes it as one text (data/text/common_2.asm).

Both prompts in this file now share one helper. It draws with Chrome.printWrapped, which wraps on the translated line break AND on pixel width, and reads the line count back from it to warn -- once per prompt -- when a translation needs a third row the box cannot hold. The title-screen prompt went through a hand-rolled splitter on the first line break, which handed Font.draw an embedded break, rendered as a space: a translation needing three rows printed two of them joined and ran past the box.

The official wordings fit: "Voulez-vous arrê- / ter le concours?" and "Möchtest du den / Wettkampf beenden?".

tests/engine/gen2_contest_coins_translation_test.lua drives StartMenu's confirmContest arm and StartGameCornerGame's no-coins branch with a mod-loaded catalog and checks the translated wording reaches the screen, plus the English case. On dev the three prompt checks fail; the refusal's own check passes there too, because that lookup already ran at runtime -- what dev was missing is the key, not the call.
This commit is contained in:
thibautbus
2026-09-20 14:34:19 +02:00
parent 53c320ed77
commit 06d99eb2aa
3 changed files with 145 additions and 8 deletions
+38 -8
View File
@@ -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)
@@ -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")
@@ -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?",
}