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 frames = 0
|
||||||
local CI_FRAMES = 60
|
local CI_FRAMES = 60
|
||||||
|
|
||||||
local TITLE = "Sporel Launcher"
|
-- L.8: All UI-facing strings now live in strings.lua and are accessed
|
||||||
local QUIT_PROMPT = "Sporel beenden?"
|
-- via ctx_panels.t(key, ...). The previous TITLE / QUIT_PROMPT /
|
||||||
local YES_LABEL = "Ja"
|
-- YES_LABEL / NO_LABEL / EMPTY_LABEL locals were retired in this slice.
|
||||||
local NO_LABEL = "Nein"
|
local STRINGS = dofile(MODULE_DIR .. "/strings.lua")
|
||||||
local EMPTY_LABEL = "Keine Module installiert."
|
|
||||||
|
|
||||||
local FONT_TITLE = 28
|
local FONT_TITLE = 28
|
||||||
local FONT_BUTTON = 20
|
local FONT_BUTTON = 20
|
||||||
@@ -198,7 +197,7 @@ function init(ctx)
|
|||||||
ctx_panels = {
|
ctx_panels = {
|
||||||
-- Persistent UI context, threaded through the panels.
|
-- Persistent UI context, threaded through the panels.
|
||||||
module_entries = {}, -- filled below from engine.module.list
|
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,
|
selected_entry = nil,
|
||||||
scroll_y = 0,
|
scroll_y = 0,
|
||||||
list_tint = nil, -- nil | "warn" | "error" — set in L.4
|
list_tint = nil, -- nil | "warn" | "error" — set in L.4
|
||||||
@@ -206,12 +205,23 @@ function init(ctx)
|
|||||||
detail_panel_rect = nil,
|
detail_panel_rect = nil,
|
||||||
bg_texture = nil, -- L.10 loads it
|
bg_texture = nil, -- L.10 loads it
|
||||||
default_teaser_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
|
carousel_state = {}, -- per-module-id state, L.5 populates
|
||||||
manifest_cache = {}, -- L.3 populates
|
manifest_cache = {}, -- L.3 populates
|
||||||
launcher_dir = MODULE_DIR, -- L.4: detail.lua dofiles markdown.lua via this
|
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.
|
-- Window split: 1/3 list, 2/3 detail. Margin 12px between panels.
|
||||||
local SW, SH = screen_w, screen_h
|
local SW, SH = screen_w, screen_h
|
||||||
ctx_panels.list_panel_rect = { x = 0, y = 0, w = SW / 3, h = SH }
|
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
|
-- 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
|
-- 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
|
do
|
||||||
local news_path = MODULE_DIR .. "/news.md"
|
local news_path = MODULE_DIR .. "/news.md"
|
||||||
local f = io.open(news_path, "rb")
|
local f = io.open(news_path, "rb")
|
||||||
@@ -281,14 +291,15 @@ function init(ctx)
|
|||||||
local md = dofile(MODULE_DIR .. "/markdown.lua")
|
local md = dofile(MODULE_DIR .. "/markdown.lua")
|
||||||
local meta, body = fm.parse(content)
|
local meta, body = fm.parse(content)
|
||||||
if meta then
|
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)
|
ctx_panels.news_body_blocks = md.parse(body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON)
|
local wy, hy = engine.render.measure_text(ctx_panels.t("yes"), FONT_BUTTON)
|
||||||
local wn, hn = engine.render.measure_text(NO_LABEL, 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_w = math.max(wy, wn) + 2 * PAD_X
|
||||||
local qbtn_h = math.max(hy, hn) + 2 * PAD_Y
|
local qbtn_h = math.max(hy, hn) + 2 * PAD_Y
|
||||||
local total_w = 2 * qbtn_w + 24
|
local total_w = 2 * qbtn_w + 24
|
||||||
@@ -349,7 +360,7 @@ function update(ctx, dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- L.7: Conflict-modal takes priority over STATE_LIST input. esc /
|
-- 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.
|
-- a no-op until engine.module.switch_module_supports_fallback ships.
|
||||||
if state == fsm.STATE_MODAL_CONFLICT then
|
if state == fsm.STATE_MODAL_CONFLICT then
|
||||||
if input.was_action_pressed("ui_back") 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,
|
engine.render.draw_rect((screen_w - modal_w) / 2,
|
||||||
(screen_h - modal_h) / 2,
|
(screen_h - modal_h) / 2,
|
||||||
modal_w, modal_h, COLOR_MODAL)
|
modal_w, modal_h, COLOR_MODAL)
|
||||||
local pw = engine.render.measure_text(QUIT_PROMPT, FONT_BUTTON)
|
local prompt = ctx_panels.t("quit_prompt")
|
||||||
engine.render.draw_text(QUIT_PROMPT, (screen_w - pw) / 2,
|
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)
|
screen_h / 2 - 24, FONT_BUTTON, COLOR_TEXT)
|
||||||
|
|
||||||
for _, key in ipairs({ "yes", "no" }) do
|
for _, key in ipairs({ "yes", "no" }) do
|
||||||
local b = quit_buttons[key]
|
local b = quit_buttons[key]
|
||||||
local color = hit(b, mx, my) and COLOR_HOVER or COLOR_BTN
|
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)
|
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)
|
local lw = engine.render.measure_text(label, FONT_BUTTON)
|
||||||
engine.render.draw_text(label, b.x + (b.w - lw) / 2,
|
engine.render.draw_text(label, b.x + (b.w - lw) / 2,
|
||||||
b.y + PAD_Y, FONT_BUTTON, COLOR_TEXT)
|
b.y + PAD_Y, FONT_BUTTON, COLOR_TEXT)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ function M.handle_click(ctx, mx, my)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- L.7: Launch button → either switch directly (clean deps) or open
|
-- 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
|
-- requires engine.module.switch_module_supports_fallback (separate
|
||||||
-- engine slice); until that ships the button stays disabled.
|
-- 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
|
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
|
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_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)
|
rect.x + PADDING, rect.y + PADDING, FONT, COLOR_TEXT)
|
||||||
ctx.news_entry.rect = rect
|
ctx.news_entry.rect = rect
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ local FONT_BTN = 18
|
|||||||
local PADDING = 18
|
local PADDING = 18
|
||||||
|
|
||||||
-- def = {
|
-- def = {
|
||||||
-- title = "Confirm",
|
-- title = <localized-title-string>,
|
||||||
-- body = "text or array of lines",
|
-- body = <line-or-array-of-localized-lines>,
|
||||||
-- buttons = { { id="cancel", label="Cancel" }, { id="ok", label="Launch anyway", disabled=false } },
|
-- buttons = { { id=<key>, label=<localized-label>, disabled=<bool> }, ... },
|
||||||
-- }
|
-- }
|
||||||
function M.render(def, screen_w, screen_h)
|
function M.render(def, screen_w, screen_h)
|
||||||
-- Backdrop.
|
-- 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