mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
f0a88ea473
* audio timing stuff * bug fixes and translation additions * translation stuff * Update modkit.py * better asset resolution
135 lines
5.2 KiB
Lua
135 lines
5.2 KiB
Lua
-- The engine's own player-facing text, made overridable.
|
|
--
|
|
-- Extracted dialogue already had a home: `Data.text`, keyed by pokered's
|
|
-- labels, which a mod reaches through `mod.content.text`. The strings the
|
|
-- engine *authors* had none. Battle messages, item results, menu labels
|
|
-- and the link-play screens were literals in Lua, so a translator could
|
|
-- reach two thirds of the game and no more (#186, #245).
|
|
--
|
|
-- Those literals stay where they are, wrapped in `S(...)`, and the English
|
|
-- source doubles as the catalog key:
|
|
--
|
|
-- self:say(S("But, it failed!"))
|
|
-- self:say(S("Wild %s\nappeared!", self.enemy.name))
|
|
--
|
|
-- Keying on the source rather than on an invented id is deliberate. It
|
|
-- keeps the English readable at the point it is used (the alternative
|
|
-- scatters a thousand `battle.it_failed` ids that have to be looked up to
|
|
-- review a diff), it needs no id registry to stay in sync, and an entry a
|
|
-- translation has not covered yet falls through to English instead of
|
|
-- rendering a raw id at the player. The cost is that editing an English
|
|
-- string orphans its translations; `tools/modkit.py translation --refresh`
|
|
-- reports those as changed keys rather than silently dropping them.
|
|
--
|
|
-- Same-source-different-meaning is the one case source keys cannot hold on
|
|
-- their own ("OFF" as a filter setting vs "OFF" as a toggle, which some
|
|
-- languages render differently). Those sites pass a context:
|
|
--
|
|
-- S("OFF", "options.musicFilter") -- key: "options.musicFilter|OFF"
|
|
--
|
|
-- A mod supplies the catalog through the `strings` registry:
|
|
--
|
|
-- mod.content.strings:override("But, it failed!", "Echec !")
|
|
--
|
|
-- With no mod loaded the catalog is empty and `S` is an identity function
|
|
-- guarded by one boolean, so a vanilla boot draws byte-identical text.
|
|
|
|
local Strings = {}
|
|
|
|
local catalog = nil -- Data.strings once a mod has put something in it
|
|
local missing = {} -- format-arity complaints, reported once each
|
|
|
|
-- Called from Game after the mod merge, and again on dev-mode hot reload.
|
|
-- Holding the table (not a copy) means a mod that registers late still
|
|
-- takes effect without a second load.
|
|
function Strings.load(data)
|
|
local t = data and data.strings
|
|
catalog = nil
|
|
if type(t) ~= "table" then return end
|
|
for _ in pairs(t) do
|
|
catalog = t
|
|
return
|
|
end
|
|
end
|
|
|
|
function Strings.active()
|
|
return catalog ~= nil
|
|
end
|
|
|
|
-- The lookup itself. `context` is optional and only disambiguates sources
|
|
-- that collide; the plain key is tried after it, so a translation that does
|
|
-- not care about the distinction can supply one entry for both.
|
|
function Strings.lookup(source, context)
|
|
if not catalog then return source end
|
|
if context then
|
|
local hit = catalog[context .. "|" .. source]
|
|
if type(hit) == "string" then return hit end
|
|
end
|
|
local hit = catalog[source]
|
|
if type(hit) == "string" then return hit end
|
|
return source
|
|
end
|
|
|
|
-- Count `%`-directives so a translation that drops or adds one is caught
|
|
-- here rather than as a mid-battle `string.format` error.
|
|
local function specifiers(s)
|
|
local n = 0
|
|
for spec in s:gmatch("%%(.)") do
|
|
if spec ~= "%" then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
-- S(source) -> translated source
|
|
-- S(source, ...) -> translated source, string.format'ed
|
|
-- S(source, context) -> context-disambiguated lookup, no formatting
|
|
--
|
|
-- The two-argument forms are told apart by whether the source carries any
|
|
-- format directives: a source with no `%s` cannot be formatting, so a lone
|
|
-- string second argument is a context.
|
|
function Strings.get(source, ...)
|
|
local argc = select("#", ...)
|
|
if argc == 0 then return Strings.lookup(source) end
|
|
|
|
local wants = specifiers(source)
|
|
if wants == 0 and argc == 1 and type((...)) == "string" then
|
|
return Strings.lookup(source, (...))
|
|
end
|
|
|
|
local text = Strings.lookup(source)
|
|
-- A translation with the wrong arity would raise inside string.format,
|
|
-- which in a battle means a crash the player cannot escape. Fall back to
|
|
-- the English source, which is known to match, and say so once.
|
|
if specifiers(text) ~= wants then
|
|
if not missing[source] then
|
|
missing[source] = true
|
|
require("src.core.Logger").warn(
|
|
"strings: translation of %q has %d format directives, source has %d"
|
|
.. " -- using the source", source, specifiers(text), wants)
|
|
end
|
|
text = source
|
|
end
|
|
local ok, out = pcall(string.format, text, ...)
|
|
if not ok then return source end
|
|
return out
|
|
end
|
|
|
|
-- A marker, not a lookup: returns its argument untouched.
|
|
--
|
|
-- Some templates are declared in a module-level table and formatted much
|
|
-- later (BattleState's charge-move lines, for one). Translating at the
|
|
-- declaration would freeze the English, because those tables are built at
|
|
-- require time and Strings.load has no catalog yet; the use site therefore
|
|
-- calls Strings(template, ...) and looks the source up then. That works at
|
|
-- runtime but leaves the literal invisible to the catalog generator, which
|
|
-- only sees what is spelled out at a call site. Wrapping the declaration in
|
|
-- Strings.source puts it back in the harvest while changing nothing at all
|
|
-- about when the lookup happens.
|
|
function Strings.source(text)
|
|
return text
|
|
end
|
|
|
|
setmetatable(Strings, { __call = function(_, ...) return Strings.get(...) end })
|
|
|
|
return Strings
|