From 654650e3ffac90a1650124d50aa10d70d33b8122 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:31:38 +0200 Subject: [PATCH] Translate Pokegear's AM/PM too, and warn instead of corrupting a 3rd line An independent review flagged two gaps: Pokegear.lua's clock card (drawClock and the card-list clock view) prints the same AM/PM information MainMenu's clock box does, but was left as bare literals -- so a mod translating AM/PM would now show it correctly on the title screen and in English on the Pokegear for the exact same hour. Both call sites now go through Strings("AM")/Strings("PM"), the same source key MainMenu.lua already added, so no new corpus entry is needed on the translation-mods side. SaveMenu.lua's twoLines() only ever split on the first "\n": drawPanel's box has room for exactly two Chrome.print calls, so a translation needing a third line (nothing in the current fr/de/es/it overrides does, but nothing stopped one from trying) had nowhere on screen to go, and the extra "\n" landed as a literal newline byte drawn as glyph garbage on the second line with no indication anything was wrong. It now warns once per source text instead. Added a gen2_save_menu_translation_test.lua case driving drawPanel() with a three-line mod translation: confirms only the first two lines render, the warning fires exactly once (not once per frame), and drawing does not crash. Pokegear.lua has no engine-tier test suite to extend for the AM/PM change; verified by direct inspection and a syntax check instead. --- src/ui/gen2/Pokegear.lua | 5 +- src/ui/gen2/SaveMenu.lua | 54 +++++++++++-------- .../gen2_save_menu_translation_test.lua | 32 +++++++++++ 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/ui/gen2/Pokegear.lua b/src/ui/gen2/Pokegear.lua index 4d94027e..14c54177 100644 --- a/src/ui/gen2/Pokegear.lua +++ b/src/ui/gen2/Pokegear.lua @@ -26,6 +26,7 @@ local Font = require("src.render.Font") local Palettes = require("src.world.gen2.Palettes") local Phone = require("src.core.gen2.Phone") local SpriteRenderer = require("src.render.SpriteRenderer") +local Strings = require("src.core.Strings") local TileSheet = require("src.ui.gen2.TileSheet") local Pokegear = {} @@ -1881,7 +1882,7 @@ function Pokegear:drawClock() self:text(Chrome.number(display, 2), 6, 8) self:text(":", 8, 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 -- PokegearClock_Init prints PokegearPressButtonText straight into it @@ -2218,7 +2219,7 @@ function Pokegear:drawPlain() if display == 0 then display = 12 end Chrome.print(("%s:%s %s"):format( 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) elseif id == "radio" then -- Without the gear sheet there is no dial art, so the frequencies go down diff --git a/src/ui/gen2/SaveMenu.lua b/src/ui/gen2/SaveMenu.lua index 17bdea32..d3e0eacc 100644 --- a/src/ui/gen2/SaveMenu.lua +++ b/src/ui/gen2/SaveMenu.lua @@ -24,6 +24,7 @@ -- one-line call. local Chrome = require("src.ui.gen2.Chrome") +local Logger = require("src.core.Logger") local Save = require("src.core.gen2.Save") local Sound = require("src.core.Sound") local Strings = require("src.core.Strings") @@ -57,39 +58,46 @@ 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(), which returns this two-slot table --- straight through to its own lines[1]/lines[2] Chrome.print calls, so the --- table shape here is a cross-file contract that must not change). +-- (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." } --- The same two prompts as a single \n-joined Strings.source() key, used only --- by this screen's own prompt() below (PcMenu keeps reading the untranslated --- table above unchanged). One key lets a translation reorder the whole --- sentence rather than two independently-translated fragments, and lets a --- cart whose own translation shows it on ONE line (German's SAVING prompt --- has no second line at all) say so directly -- the per-line "{RAM:...}"- --- style split load_engine_overrides uses elsewhere requires a non-empty --- override for every line, so it cannot express "this line is blank" the --- way an embedded "\n"-less string can. +-- 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")`: tools/ --- modkit.py's STRINGS_CALL harvester matches a quoted string literal --- immediately inside Strings.source(...)/Strings(...), not an arbitrary --- expression, so a computed argument here would be invisible to every --- translator's `modkit.py translation ... --refresh` scaffold despite the --- runtime lookup working fine -- caught by an independent review. Keep --- these two byte-for-byte in sync with OVERWRITE_PROMPT/SAVING_PROMPT --- above (checked by tests/engine/gen2_save_menu_translation_test.lua). +-- 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 Strings()-resolved "line one\nline two" into the two-slot table --- drawPanel's fixed-position Chrome.print calls expect; a translation with no --- "\n" at all (single-line messages like "Could not save.") lands whole on +-- 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 diff --git a/tests/engine/gen2_save_menu_translation_test.lua b/tests/engine/gen2_save_menu_translation_test.lua index e74a9072..c560d0bf 100644 --- a/tests/engine/gen2_save_menu_translation_test.lua +++ b/tests/engine/gen2_save_menu_translation_test.lua @@ -164,4 +164,36 @@ do 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")