Merge pull request #1759 from thibautbus/fix/translate-gold-title-and-save-menus

Translate Gold's title menu and SAVE screen
This commit is contained in:
bryanthaboi
2026-08-24 08:29:00 -04:00
committed by GitHub
5 changed files with 386 additions and 24 deletions
+11 -10
View File
@@ -24,6 +24,7 @@ local Logger = require("src.core.Logger")
local Music = require("src.core.Music") local Music = require("src.core.Music")
local Runtime = require("src.mods.Runtime") local Runtime = require("src.mods.Runtime")
local Save = require("src.core.gen2.Save") local Save = require("src.core.gen2.Save")
local Strings = require("src.core.Strings")
local MainMenu = {} local MainMenu = {}
MainMenu.__index = MainMenu MainMenu.__index = MainMenu
@@ -73,14 +74,14 @@ local function sameItems(_, items) return items end
function MainMenu:buildList() function MainMenu:buildList()
local items = {} local items = {}
if self.hasSave then if self.hasSave then
items[#items + 1] = { label = "CONTINUE", value = "continue" } items[#items + 1] = { label = Strings("CONTINUE"), value = "continue" }
end end
items[#items + 1] = { label = "NEW GAME", value = "new" } items[#items + 1] = { label = Strings("NEW GAME"), value = "new" }
items[#items + 1] = { label = "OPTION", value = "option" } items[#items + 1] = { label = Strings("OPTION"), value = "option" }
-- Not on the cart: a cartridge is left by switching the console off, and -- Not on the cart: a cartridge is left by switching the console off, and
-- there is no console here. Mirrors the Gen 1 port's title menu -- there is no console here. Mirrors the Gen 1 port's title menu
-- (src/ui/TitleState.lua), which adds the same row for the same reason. -- (src/ui/TitleState.lua), which adds the same row for the same reason.
items[#items + 1] = { label = "EXIT GAME", value = "exit" } items[#items + 1] = { label = Strings("EXIT GAME"), value = "exit" }
-- The same hook name and the same (game, items) payload the Gen 1 title -- The same hook name and the same (game, items) payload the Gen 1 title
-- menu raises (src/ui/TitleState.lua:openMenu), so one mod's title rows -- menu raises (src/ui/TitleState.lua:openMenu), so one mod's title rows
-- serve both games; only the row shape differs, because Chrome.List reads -- serve both games; only the row shape differs, because Chrome.List reads
@@ -170,7 +171,7 @@ function MainMenu:drawClockBox()
-- minutes; the AM/PM half is drawn by PrintHour itself. -- minutes; the AM/PM half is drawn by PrintHour itself.
local display = hour % 12 local display = hour % 12
if display == 0 then display = 12 end if display == 0 then display = 12 end
local half = hour < 12 and "AM" or "PM" local half = Strings(hour < 12 and "AM" or "PM")
Chrome.print(("%s:%s %s"):format( Chrome.print(("%s:%s %s"):format(
Chrome.number(display, 2), Chrome.number(minute, 2, true), half), 4, 16) Chrome.number(display, 2), Chrome.number(minute, 2, true), half), 4, 16)
end end
@@ -180,15 +181,15 @@ function MainMenu:drawSavePanel()
-- DisplaySaveInfoOnContinue: a box down the right side listing the trainer. -- DisplaySaveInfoOnContinue: a box down the right side listing the trainer.
Chrome.textbox(4, 0, 14, 9) Chrome.textbox(4, 0, 14, 9)
if not summary then if not summary then
Chrome.print("NO SAVE FILE", 5, 2) Chrome.print(Strings("NO SAVE FILE"), 5, 2)
return return
end end
Chrome.print("PLAYER " .. summary.name, 5, 2) Chrome.print(Strings("PLAYER %s", summary.name), 5, 2)
Chrome.print("BADGES", 5, 4) Chrome.print(Strings("BADGES"), 5, 4)
Chrome.printRight(tostring(summary.badges), 17, 4) Chrome.printRight(tostring(summary.badges), 17, 4)
Chrome.print("POKéDEX", 5, 6) Chrome.print(Strings("POKéDEX"), 5, 6)
Chrome.printRight(tostring(summary.caught), 17, 6) Chrome.printRight(tostring(summary.caught), 17, 6)
Chrome.print("TIME", 5, 8) Chrome.print(Strings("TIME"), 5, 8)
Chrome.printRight(("%d:%s"):format( Chrome.printRight(("%d:%s"):format(
summary.hours, Chrome.number(summary.minutes, 2, true)), 17, 8) summary.hours, Chrome.number(summary.minutes, 2, true)), 17, 8)
end end
+3 -2
View File
@@ -26,6 +26,7 @@ local Font = require("src.render.Font")
local Palettes = require("src.world.gen2.Palettes") local Palettes = require("src.world.gen2.Palettes")
local Phone = require("src.core.gen2.Phone") local Phone = require("src.core.gen2.Phone")
local SpriteRenderer = require("src.render.SpriteRenderer") local SpriteRenderer = require("src.render.SpriteRenderer")
local Strings = require("src.core.Strings")
local TileSheet = require("src.ui.gen2.TileSheet") local TileSheet = require("src.ui.gen2.TileSheet")
local Pokegear = {} local Pokegear = {}
@@ -1881,7 +1882,7 @@ function Pokegear:drawClock()
self:text(Chrome.number(display, 2), 6, 8) self:text(Chrome.number(display, 2), 6, 8)
self:text(":", 8, 8) self:text(":", 8, 8)
self:text(Chrome.number(minute, 2, true), 9, 8) self:text(Chrome.number(minute, 2, true), 9, 8)
self:text(hour < 12 and "AM" or "PM", 12, 8) self:text(Strings(hour < 12 and "AM" or "PM"), 12, 8)
-- The bottom Textbox is part of the card (lb bc, 4, 18 at (0,12)), and -- The bottom Textbox is part of the card (lb bc, 4, 18 at (0,12)), and
-- PokegearClock_Init prints PokegearPressButtonText straight into it -- PokegearClock_Init prints PokegearPressButtonText straight into it
@@ -2218,7 +2219,7 @@ function Pokegear:drawPlain()
if display == 0 then display = 12 end if display == 0 then display = 12 end
Chrome.print(("%s:%s %s"):format( Chrome.print(("%s:%s %s"):format(
Chrome.number(display, 2), Chrome.number(minute, 2, true), Chrome.number(display, 2), Chrome.number(minute, 2, true),
hour < 12 and "AM" or "PM"), 5, 9) Strings(hour < 12 and "AM" or "PM")), 5, 9)
Chrome.print(Clock.daytimeLabel(hour), 5, 11) Chrome.print(Clock.daytimeLabel(hour), 5, 11)
elseif id == "radio" then elseif id == "radio" then
-- Without the gear sheet there is no dial art, so the frequencies go down -- Without the gear sheet there is no dial art, so the frequencies go down
+54 -12
View File
@@ -24,8 +24,10 @@
-- one-line call. -- one-line call.
local Chrome = require("src.ui.gen2.Chrome") local Chrome = require("src.ui.gen2.Chrome")
local Logger = require("src.core.Logger")
local Save = require("src.core.gen2.Save") local Save = require("src.core.gen2.Save")
local Sound = require("src.core.Sound") local Sound = require("src.core.Sound")
local Strings = require("src.core.Strings")
local SaveMenu = {} local SaveMenu = {}
SaveMenu.__index = SaveMenu SaveMenu.__index = SaveMenu
@@ -55,10 +57,50 @@ local TIME_X, TIME_Y = 13, 8
local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5 local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 0, 7, 6, 5
-- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and -- AlreadyASaveFileText (AskOverwriteSaveFile, engine/menus/save.asm:47) and
-- SavingDontTurnOffThePower's own line, shared with the PC's CHANGE BOX save. -- 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 OVERWRITE_PROMPT = { "There is already a", "save file. Is it" }
local SAVING_PROMPT = { "SAVING… DON'T TURN", "OFF THE POWER." } 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".
--
-- 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).
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.")
-- Splits a translated "line one\nline two" string back into the two-slot
-- table drawPanel's fixed Chrome.print calls expect. No "\n" at all (a
-- single-line message, or German's one-line SAVING prompt) lands whole on
-- the first slot, matching the untranslated code's own { text, "" } shape.
--
-- Only the first "\n" splits, since this box has room for exactly two
-- lines. A third line would otherwise draw as a raw newline byte -- garbage
-- glyph data -- with no other sign anything went wrong, so this warns once
-- per string instead.
local warnedTooManyLines = {}
local function twoLines(text)
local first, second = text:match("^(.-)\n(.*)$")
if second and second:find("\n", 1, true) and not warnedTooManyLines[text] then
warnedTooManyLines[text] = true
Logger.warn("SaveMenu: translation of %q has more than two lines; " ..
"only the first two fit this box", text)
end
return { first or text, second or "" }
end
function SaveMenu:wantsFillScale() return true end function SaveMenu:wantsFillScale() return true end
function SaveMenu:drawsWidescreen() return true end function SaveMenu:drawsWidescreen() return true end
@@ -171,18 +213,18 @@ function SaveMenu:prompt()
if self.phase == "overwrite" then if self.phase == "overwrite" then
-- AlreadyASaveFileText when the file is this player's; AnotherSaveFileText -- AlreadyASaveFileText when the file is this player's; AnotherSaveFileText
-- when the ID differs. Only the first can happen here. -- when the ID differs. Only the first can happen here.
return OVERWRITE_PROMPT return twoLines(Strings(OVERWRITE_PROMPT_SOURCE))
end end
if self.phase == "saving" then if self.phase == "saving" then
return SAVING_PROMPT return twoLines(Strings(SAVING_PROMPT_SOURCE))
end end
if self.phase == "done" then if self.phase == "done" then
if self.saved then if self.saved then
return { self:playerName() .. " saved", "the game." } return twoLines(Strings("%s saved\nthe game.", self:playerName()))
end end
return { "Could not save.", "" } return twoLines(Strings("Could not save."))
end end
return { "Would you like to", "save the game?" } return twoLines(Strings("Would you like to\nsave the game?"))
end end
function SaveMenu:drawPanel() function SaveMenu:drawPanel()
@@ -190,10 +232,10 @@ function SaveMenu:drawPanel()
local summary = Save.summary(self.save) local summary = Save.summary(self.save)
Chrome.box(PANEL_X, PANEL_Y, PANEL_W, PANEL_H) Chrome.box(PANEL_X, PANEL_Y, PANEL_W, PANEL_H)
if summary then if summary then
Chrome.print("PLAYER " .. summary.name, LABEL_X, LABEL_Y) Chrome.print(Strings("PLAYER %s", summary.name), LABEL_X, LABEL_Y)
Chrome.print("BADGES", LABEL_X, LABEL_Y + 2) Chrome.print(Strings("BADGES"), LABEL_X, LABEL_Y + 2)
Chrome.print("POKéDEX", LABEL_X, LABEL_Y + 4) Chrome.print(Strings("POKéDEX"), LABEL_X, LABEL_Y + 4)
Chrome.print("TIME", LABEL_X, LABEL_Y + 6) Chrome.print(Strings("TIME"), LABEL_X, LABEL_Y + 6)
-- PrintNum fills its field from the left, space padded. -- PrintNum fills its field from the left, space padded.
Chrome.print(Chrome.number(summary.badges, 2), BADGES_X, BADGES_Y) Chrome.print(Chrome.number(summary.badges, 2), BADGES_X, BADGES_Y)
Chrome.print(Chrome.number(summary.caught, 3), DEX_X, DEX_Y) Chrome.print(Chrome.number(summary.caught, 3), DEX_X, DEX_Y)
@@ -211,8 +253,8 @@ function SaveMenu:drawPanel()
if self.phase == "confirm" or self.phase == "overwrite" then if self.phase == "confirm" or self.phase == "overwrite" then
Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H) Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H)
Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1) Chrome.print(Strings("YES"), YESNO_X + 2, YESNO_Y + 1)
Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3) Chrome.print(Strings("NO"), YESNO_X + 2, YESNO_Y + 3)
Chrome.cursor(YESNO_X + 1, YESNO_Y + (self.choice == 1 and 1 or 3)) Chrome.cursor(YESNO_X + 1, YESNO_Y + (self.choice == 1 and 1 or 3))
end end
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
@@ -0,0 +1,119 @@
-- Gold's title/main menu (src/ui/gen2/MainMenu.lua) drew every row label
-- (CONTINUE/NEW GAME/OPTION/EXIT GAME), the clock box's AM/PM half, and the
-- CONTINUE save-summary panel's labels (PLAYER <name>/BADGES/POKéDEX/TIME,
-- or NO SAVE FILE) as bare literals, invisible to a translation mod's
-- `strings` registry -- unlike the Gen 1 port's own title menu
-- (src/ui/TitleState.lua/StartMenu.lua), which already routes the same rows
-- through Strings(). Drives MainMenu:drawPanel()/:drawSavePanel() with a
-- mod-loaded Strings catalog and checks the translated text reaches
-- Font.draw, same technique as
-- tests/engine/gen2_naming_screen_translation_test.lua.
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 MainMenu = require("src.ui.gen2.MainMenu")
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).
-- List item 1 lands at (self.x, self.y) = (2, 2); the clock box's day name
-- at (1, 14) and the hour:minute half at (4, 16); the save panel's PLAYER
-- row at (5, 2).
local FIRST_ITEM_X, FIRST_ITEM_Y = 2 * 8, 2 * 8
local CLOCK_HALF_X, CLOCK_HALF_Y = 4 * 8, 16 * 8
local PANEL_PLAYER_X, PANEL_PLAYER_Y = 5 * 8, 2 * 8
local SAVE = { player = { name = "GOLD" } }
local CLOCK = { hour = 13, minute = 5, weekday = 1 } -- 1 PM, SUNDAY
-- ---------------------------------------------- vanilla: no mod catalog
do
local menu = MainMenu.new({}, { hasSave = true, save = SAVE, clock = CLOCK })
drawn = {}
menu:drawPanel()
T.eq(drawnAt(FIRST_ITEM_X, FIRST_ITEM_Y), "CONTINUE",
"the title menu's first row draws in English with no mod loaded")
T.eq(drawnAt(CLOCK_HALF_X, CLOCK_HALF_Y), " 1:05 PM",
"and the clock box's AM/PM half")
drawn = {}
menu:drawSavePanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "PLAYER GOLD",
"the CONTINUE save-summary panel too")
local noSaveMenu = MainMenu.new({}, { hasSave = false, save = false, clock = CLOCK })
drawn = {}
noSaveMenu:drawSavePanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "NO SAVE FILE",
"and its no-summary fallback")
end
-- ------------------------------------------------- a translation mod's turn
do
Strings.load({
strings = {
["CONTINUE"] = "CONTINUAR",
["NEW GAME"] = "NUEVA PARTIDA",
["OPTION"] = "OPCIÓN",
["EXIT GAME"] = "SALIR",
["PM"] = "PM_ES",
["PLAYER %s"] = "JUGADOR %s",
["BADGES"] = "MEDALLAS",
["POKéDEX"] = "POKéDEX_ES",
["TIME"] = "TIEMPO",
["NO SAVE FILE"] = "SIN PARTIDA",
},
})
local menu = MainMenu.new({}, { hasSave = true, save = SAVE, clock = CLOCK })
drawn = {}
menu:drawPanel()
T.eq(drawnAt(FIRST_ITEM_X, FIRST_ITEM_Y), "CONTINUAR",
"a mod catalog reaches the title menu's first row")
T.eq(drawnAt(CLOCK_HALF_X, CLOCK_HALF_Y), " 1:05 PM_ES",
"and the clock box's AM/PM half")
drawn = {}
menu:drawSavePanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "JUGADOR GOLD",
"the save-summary panel's PLAYER row takes the mod's own word order")
T.eq(drawnAt(5 * 8, 4 * 8), "MEDALLAS", "and BADGES")
T.eq(drawnAt(5 * 8, 6 * 8), "POKéDEX_ES", "and POKéDEX")
T.eq(drawnAt(5 * 8, 8 * 8), "TIEMPO", "and TIME")
local noSaveMenu = MainMenu.new({}, { hasSave = false, save = false, clock = CLOCK })
drawn = {}
noSaveMenu:drawSavePanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "SIN PARTIDA",
"and the no-summary fallback")
-- 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_main_menu_translation_test")
@@ -0,0 +1,199 @@
-- Gold's SAVE screen (src/ui/gen2/SaveMenu.lua) drew every prompt ("Would
-- you like to save the game?", the overwrite/saving/saved messages), the
-- YES/NO choice, and the summary panel's labels (PLAYER <name>/BADGES/
-- POKéDEX/TIME) as bare literals, invisible to a translation mod's
-- `strings` registry -- unlike the Gen 1 port's own SAVE screen
-- (src/ui/StartMenu.lua), which already routes the same rows through
-- Strings(). Drives SaveMenu:drawPanel() directly at each phase with a
-- mod-loaded Strings catalog and checks the translated text reaches
-- Font.draw, same technique as
-- tests/engine/gen2_naming_screen_translation_test.lua.
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 SaveMenu = require("src.ui.gen2.SaveMenu")
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).
-- PLAYER row at (5, 2); the two prompt lines at (1, 14)/(1, 16); YES/NO at
-- (2, 8)/(2, 10) (YESNO_X + 2, YESNO_Y + 1 / + 3).
local PANEL_PLAYER_X, PANEL_PLAYER_Y = 5 * 8, 2 * 8
local PROMPT1_X, PROMPT1_Y = 1 * 8, 14 * 8
local PROMPT2_X, PROMPT2_Y = 1 * 8, 16 * 8
local YES_X, YES_Y = 2 * 8, 8 * 8
local NO_X, NO_Y = 2 * 8, 10 * 8
local SAVE = { player = { name = "GOLD" } }
-- ---------------------------------------------- vanilla: no mod catalog
do
local menu = SaveMenu.new({}, { save = SAVE, existed = false })
drawn = {}
menu:drawPanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "PLAYER GOLD",
"the summary panel draws in English with no mod loaded")
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Would you like to",
"and the confirm prompt's first line")
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save the game?", "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.phase = "overwrite"
drawn = {}
menu:drawPanel()
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "There is already a", "the overwrite prompt")
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "save file. Is it", "its second line")
menu.phase = "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.phase, 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.phase, 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
do
Strings.load({
strings = {
["PLAYER %s"] = "JOUEUR %s",
["BADGES"] = "BADGES_FR",
["POKéDEX"] = "POKéDEX_FR",
["TIME"] = "TEMPS",
["YES"] = "OUI",
["NO"] = "NON",
["Would you like to\nsave the game?"] = "Voulez-vous\nsauvegarder ?",
["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 = SaveMenu.new({}, { save = SAVE, existed = false })
drawn = {}
menu:drawPanel()
T.eq(drawnAt(PANEL_PLAYER_X, PANEL_PLAYER_Y), "JOUEUR GOLD",
"the summary panel's PLAYER row takes the mod's own word order")
T.eq(drawnAt(5 * 8, 4 * 8), "BADGES_FR", "and BADGES")
T.eq(drawnAt(5 * 8, 6 * 8), "POKéDEX_FR", "and POKéDEX")
T.eq(drawnAt(5 * 8, 8 * 8), "TEMPS", "and TIME")
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Voulez-vous", "the confirm prompt")
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "sauvegarder ?", "its second line")
T.eq(drawnAt(YES_X, YES_Y), "OUI", "and YES")
T.eq(drawnAt(NO_X, NO_Y), "NON", "and NO")
menu.phase = "overwrite"
drawn = {}
menu:drawPanel()
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Un fichier existe", "the overwrite prompt")
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "deja. Est-ce", "its second line")
menu.phase = "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.phase, 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.phase, 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
-- 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.
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")
end
-- A translation with a THIRD line (a second embedded "\n") has nowhere on
-- screen to go -- drawPanel's box has room for exactly two Chrome.print
-- calls -- so it must not silently draw the literal newline byte as glyph
-- garbage on the second line, and should warn so a translator notices.
do
Strings.load({
strings = {
["Would you like to\nsave the game?"] = "Ligne un\nLigne deux\nLigne trois",
},
})
local warned = {}
require("src.core.Logger").warn = function(fmt, ...)
warned[#warned + 1] = select("#", ...) > 0 and fmt:format(...) or fmt
end
local menu = SaveMenu.new({}, { save = SAVE, existed = false })
drawn = {}
menu:drawPanel()
T.eq(drawnAt(PROMPT1_X, PROMPT1_Y), "Ligne un", "only the first line reaches the box")
T.eq(drawnAt(PROMPT2_X, PROMPT2_Y), "Ligne deux\nLigne trois",
"the rest lands in the second slot rather than vanishing")
T.check(#warned == 1, "and a single warning is logged")
drawn = {}
menu:drawPanel()
T.check(#warned == 1, "the warning does not repeat for the same text")
require("src.core.Logger").warn = function() end
Strings.load({})
end
T.finish("gen2_save_menu_translation_test")