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:
42
init.lua
42
init.lua
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
20
strings.lua
Normal 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",
|
||||
}
|
||||
Reference in New Issue
Block a user