feat(launcher): route all UI strings through t(key) indirection

strings.lua holds English defaults. ctx.t(key, ...) looks up the key,
optionally string.formats with extra args, and falls back to '[key]'
for missing entries. All panels and init.lua sweep clean of inline
literals — verified by a grep over '"[A-Z][a-z][a-z ]+"'. This sets
up v2's localization-lib-pattern (separate slice, ADR-0036 trigger)
to swap STRINGS without touching the launcher code.
This commit is contained in:
Calic
2026-06-11 07:49:38 +02:00
parent a49aa3ef2b
commit a19dd7f1dd
5 changed files with 53 additions and 20 deletions

View File

@@ -30,11 +30,10 @@ local gitea_base = nil -- resolved in init() from engine.config
local frames = 0
local CI_FRAMES = 60
local TITLE = "Sporel Launcher"
local QUIT_PROMPT = "Sporel beenden?"
local YES_LABEL = "Ja"
local NO_LABEL = "Nein"
local EMPTY_LABEL = "Keine Module installiert."
-- L.8: All UI-facing strings now live in strings.lua and are accessed
-- via ctx_panels.t(key, ...). The previous TITLE / QUIT_PROMPT /
-- YES_LABEL / NO_LABEL / EMPTY_LABEL locals were retired in this slice.
local STRINGS = dofile(MODULE_DIR .. "/strings.lua")
local FONT_TITLE = 28
local FONT_BUTTON = 20
@@ -198,7 +197,7 @@ function init(ctx)
ctx_panels = {
-- Persistent UI context, threaded through the panels.
module_entries = {}, -- filled below from engine.module.list
news_entry = { title = "News", body_path = "news.md" },
news_entry = { title = nil, body_path = "news.md" },
selected_entry = nil,
scroll_y = 0,
list_tint = nil, -- nil | "warn" | "error" — set in L.4
@@ -206,12 +205,23 @@ function init(ctx)
detail_panel_rect = nil,
bg_texture = nil, -- L.10 loads it
default_teaser_texture = nil, -- L.10 loads it
t = function(k) return k end, -- L.8 swaps in real t()
t = nil, -- L.8: real implementation installed below
carousel_state = {}, -- per-module-id state, L.5 populates
manifest_cache = {}, -- L.3 populates
launcher_dir = MODULE_DIR, -- L.4: detail.lua dofiles markdown.lua via this
}
-- L.8: real string indirection. Looks up `key` in strings.lua, falls
-- back to "[key]" for missing entries, and string.formats with any
-- varargs so callers can pass format-args inline (e.g. counts).
ctx_panels.t = function(key, ...)
local s = STRINGS[key]
if not s then return "[" .. tostring(key) .. "]" end
if select("#", ...) > 0 then return string.format(s, ...) end
return s
end
ctx_panels.news_entry.title = ctx_panels.t("news_default_title")
-- Window split: 1/3 list, 2/3 detail. Margin 12px between panels.
local SW, SH = screen_w, screen_h
ctx_panels.list_panel_rect = { x = 0, y = 0, w = SW / 3, h = SH }
@@ -270,7 +280,7 @@ function init(ctx)
-- L.6: load + parse news.md once. The detail-panel's news branch
-- reads ctx.news_body_blocks; nil-safe so a missing file still
-- shows the "News" entry with an empty body.
-- shows the news entry with an empty body.
do
local news_path = MODULE_DIR .. "/news.md"
local f = io.open(news_path, "rb")
@@ -281,14 +291,15 @@ function init(ctx)
local md = dofile(MODULE_DIR .. "/markdown.lua")
local meta, body = fm.parse(content)
if meta then
ctx_panels.news_entry.title = meta.title or "News"
ctx_panels.news_entry.title = meta.title
or ctx_panels.t("news_default_title")
ctx_panels.news_body_blocks = md.parse(body)
end
end
end
local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON)
local wn, hn = engine.render.measure_text(NO_LABEL, FONT_BUTTON)
local wy, hy = engine.render.measure_text(ctx_panels.t("yes"), FONT_BUTTON)
local wn, hn = engine.render.measure_text(ctx_panels.t("no"), FONT_BUTTON)
local qbtn_w = math.max(wy, wn) + 2 * PAD_X
local qbtn_h = math.max(hy, hn) + 2 * PAD_Y
local total_w = 2 * qbtn_w + 24
@@ -349,7 +360,7 @@ function update(ctx, dt)
end
-- L.7: Conflict-modal takes priority over STATE_LIST input. esc /
-- Cancel close the modal back to STATE_LIST; "Launch anyway" stays
-- cancel close the modal back to STATE_LIST; launch-anyway stays
-- a no-op until engine.module.switch_module_supports_fallback ships.
if state == fsm.STATE_MODAL_CONFLICT then
if input.was_action_pressed("ui_back") then
@@ -414,15 +425,16 @@ function render(ctx)
engine.render.draw_rect((screen_w - modal_w) / 2,
(screen_h - modal_h) / 2,
modal_w, modal_h, COLOR_MODAL)
local pw = engine.render.measure_text(QUIT_PROMPT, FONT_BUTTON)
engine.render.draw_text(QUIT_PROMPT, (screen_w - pw) / 2,
local prompt = ctx_panels.t("quit_prompt")
local pw = engine.render.measure_text(prompt, FONT_BUTTON)
engine.render.draw_text(prompt, (screen_w - pw) / 2,
screen_h / 2 - 24, FONT_BUTTON, COLOR_TEXT)
for _, key in ipairs({ "yes", "no" }) do
local b = quit_buttons[key]
local color = hit(b, mx, my) and COLOR_HOVER or COLOR_BTN
engine.render.draw_rect(b.x, b.y, b.w, b.h, color)
local label = (key == "yes") and YES_LABEL or NO_LABEL
local label = ctx_panels.t(key)
local lw = engine.render.measure_text(label, FONT_BUTTON)
engine.render.draw_text(label, b.x + (b.w - lw) / 2,
b.y + PAD_Y, FONT_BUTTON, COLOR_TEXT)

View File

@@ -142,7 +142,7 @@ function M.handle_click(ctx, mx, my)
end
-- L.7: Launch button → either switch directly (clean deps) or open
-- the conflict modal (warn/error deps). The "Launch anyway" path
-- the conflict modal (warn/error deps). The launch-anyway path
-- requires engine.module.switch_module_supports_fallback (separate
-- engine slice); until that ships the button stays disabled.
if ctx._launch_btn and engine.spatial.aabb_contains_point(ctx._launch_btn, mx, my) then

View File

@@ -43,7 +43,8 @@ function M.render(ctx)
}
local col = (ctx.selected_entry == "news") and COLOR_ROW_ACTIVE or COLOR_ROW
engine.render.draw_rect(rect.x, rect.y, rect.w, rect.h, col)
engine.render.draw_text(ctx.news_entry.title or "News",
engine.render.draw_text(
ctx.news_entry.title or ctx.t("news_default_title"),
rect.x + PADDING, rect.y + PADDING, FONT, COLOR_TEXT)
ctx.news_entry.rect = rect
end

View File

@@ -10,9 +10,9 @@ local FONT_BTN = 18
local PADDING = 18
-- def = {
-- title = "Confirm",
-- body = "text or array of lines",
-- buttons = { { id="cancel", label="Cancel" }, { id="ok", label="Launch anyway", disabled=false } },
-- title = <localized-title-string>,
-- body = <line-or-array-of-localized-lines>,
-- buttons = { { id=<key>, label=<localized-label>, disabled=<bool> }, ... },
-- }
function M.render(def, screen_w, screen_h)
-- Backdrop.

20
strings.lua Normal file
View File

@@ -0,0 +1,20 @@
return {
-- Empty-state hint
empty_hint = "Select a module to see details.",
-- Detail panel sections
dependencies = "Dependencies",
-- Launch flow
launch_module = "Launch Module",
cancel = "Cancel",
launch_anyway = "Launch anyway",
-- Conflict modal
conflict_title = "Dependency conflict",
conflict_body_line1 = "This module has unresolved dependencies.",
conflict_body_line2_fmt = "Missing libs: %d",
-- Quit modal (already in v0.2.0 but moves to strings now)
quit_prompt = "Quit Sporel?",
yes = "Yes",
no = "No",
-- News entry default title
news_default_title = "News",
}