mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 21:35:42 -04:00
Let a Strings() translation number its format directives
Strings() formats with string.format, which fills directives in argument order. A language that puts two values the other way round from English ("ATTACK de PIKACHU" for "PIKACHU's ATTACK") could not say so: the translation either kept the English order or swapped the values. The official FireRed, Gold and Red localizations reorder values like this in dozens of battle and menu lines.
A translation can now number its directives the way POSIX printf does ("%2$s de\n%1$s monte!"), and Strings() hands the arguments over in that order. Flags and widths are kept ("%3$03d") and "%%" stays a literal percent. A translation numbers all of its directives or none, and may only number arguments the source has; it may leave one out, as the official translations sometimes drop a name. Otherwise it falls back to the English source and logs one warning, like a translation with the wrong number of directives.
Sources never carry numbered directives, and a translation without them takes the same string.format path as before, so English and existing catalogs render the same text. A translation that still cannot format its arguments falls back to the English source formatted with them, instead of the raw template. An older engine given a numbered translation cannot format it and prints the template, so a catalog using numbered directives needs this engine.
This commit is contained in:
+75
-10
@@ -27,6 +27,18 @@
|
||||
--
|
||||
-- S("OFF", "options.musicFilter") -- key: "options.musicFilter|OFF"
|
||||
--
|
||||
-- string.format fills directives in argument order, so a translation whose
|
||||
-- language puts the values the other way round cannot say so with `%s`
|
||||
-- alone. It numbers them instead, the way POSIX printf does:
|
||||
--
|
||||
-- S("%s's %s\nrose!", name, stat) -- English: "PIKACHU's ATTACK\nrose!"
|
||||
-- -> "%2$s de\n%1$s monte!" -- French: "ATTACK de\nPIKACHU monte!"
|
||||
--
|
||||
-- A translation numbers all of its directives or none, and numbers only
|
||||
-- arguments the source has (it may leave one out, as the official
|
||||
-- translations sometimes drop a name); anything else falls back to the
|
||||
-- English.
|
||||
--
|
||||
-- A mod supplies the catalog through the `strings` registry:
|
||||
--
|
||||
-- mod.content.strings:override("But, it failed!", "Echec !")
|
||||
@@ -80,6 +92,47 @@ local function specifiers(s)
|
||||
return n
|
||||
end
|
||||
|
||||
-- A translation that numbers its directives ("%2$s ... %1$s"): the plain
|
||||
-- format string and the argument order it asks for. nil when nothing is
|
||||
-- numbered; false when numbered and plain directives are mixed.
|
||||
local function positional(text)
|
||||
if not text:find("%%%d+%$") then return nil end
|
||||
local out, order = {}, {}
|
||||
local i, n = 1, #text
|
||||
while i <= n do
|
||||
local c = text:sub(i, i)
|
||||
if c ~= "%" then
|
||||
out[#out + 1] = c
|
||||
i = i + 1
|
||||
elseif text:sub(i + 1, i + 1) == "%" then
|
||||
out[#out + 1] = "%%"
|
||||
i = i + 2
|
||||
else
|
||||
local index, spec, stop = text:match("^%%(%d+)%$([-+ #0]*%d*%.?%d*%a)()", i)
|
||||
if not index then return false end
|
||||
order[#order + 1] = tonumber(index)
|
||||
out[#out + 1] = "%" .. spec
|
||||
i = stop
|
||||
end
|
||||
end
|
||||
if #order == 0 then return nil end
|
||||
return table.concat(out), order
|
||||
end
|
||||
|
||||
-- Every numbered directive names one of the source's arguments.
|
||||
local function withinArguments(order, wants)
|
||||
for _, index in ipairs(order) do
|
||||
if index < 1 or index > wants then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function complain(source, fmt, ...)
|
||||
if missing[source] then return end
|
||||
missing[source] = true
|
||||
require("src.core.Logger").warn(fmt, ...)
|
||||
end
|
||||
|
||||
-- S(source) -> translated source
|
||||
-- S(source, ...) -> translated source, string.format'ed
|
||||
-- S(source, context) -> context-disambiguated lookup, no formatting
|
||||
@@ -100,18 +153,30 @@ function Strings.get(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
|
||||
local plain, order = positional(text)
|
||||
if plain == false or (plain and not withinArguments(order, wants)) then
|
||||
complain(source, "strings: translation of %q numbers its format directives"
|
||||
.. " wrongly for %d argument(s) -- using the source", source, wants)
|
||||
plain, text = nil, source
|
||||
elseif not plain and specifiers(text) ~= wants then
|
||||
complain(source, "strings: translation of %q has %d format directives, source has %d"
|
||||
.. " -- using the source", source, specifiers(text), wants)
|
||||
text = source
|
||||
end
|
||||
local ok, out = pcall(string.format, text, ...)
|
||||
if not ok then return source end
|
||||
return out
|
||||
local ok, out
|
||||
if plain then
|
||||
local args, picked = { ... }, {}
|
||||
for k, index in ipairs(order) do picked[k] = args[index] end
|
||||
ok, out = pcall(string.format, plain, (table.unpack or unpack)(picked, 1, #order))
|
||||
else
|
||||
ok, out = pcall(string.format, text, ...)
|
||||
end
|
||||
if ok then return out end
|
||||
-- A translation the checks above let through can still fail to format (a
|
||||
-- %d handed a string); the English source is known to match its arguments.
|
||||
ok, out = pcall(string.format, source, ...)
|
||||
if ok then return out end
|
||||
return source
|
||||
end
|
||||
|
||||
-- A marker, not a lookup: returns its argument untouched.
|
||||
|
||||
@@ -87,6 +87,12 @@ return function(mod)
|
||||
mod.content.strings:override("OFF", "NON")
|
||||
mod.content.strings:override("options.musicFilter|OFF", "AUCUN")
|
||||
mod.content.strings:override("%d of %d", "%d perdus")
|
||||
mod.content.strings:override("%s's %s\nrose!", "%2$s de\n%1$s monte!")
|
||||
mod.content.strings:override("%s gave %s\n%d items.", "%3$03d objets\ndonnes a %2$s par %1$s (%%).")
|
||||
mod.content.strings:override("%s's %s\nfell!", "%2$s de\n%s baisse!")
|
||||
mod.content.strings:override("%s's %s\nhurt %s!", "%2$s de %1$s\nblesse %4$s!")
|
||||
mod.content.strings:override("%s is\nabout to use %s.\nWill %s change?", "%2$s va être envoyé\npar %1$s. Changer?")
|
||||
mod.content.strings:override("Got %s!", "%1$d obtenu!")
|
||||
end
|
||||
]],
|
||||
}
|
||||
@@ -144,6 +150,26 @@ do
|
||||
eq(complaints(), mid, "the arity complaint is not repeated")
|
||||
end
|
||||
|
||||
-- ------- a translation can number its directives to reorder the values
|
||||
|
||||
do
|
||||
eq(Strings("%s's %s\nrose!", "PIKACHU", "ATTACK"), "ATTACK de\nPIKACHU monte!",
|
||||
"numbered directives take the arguments in the order they name")
|
||||
eq(Strings("%s gave %s\n%d items.", "RED", "BLUE", 7), "007 objets\ndonnes a BLUE par RED (%).",
|
||||
"numbered directives keep their flags, and %% stays a literal percent")
|
||||
|
||||
local before = complaints()
|
||||
eq(Strings("%s's %s\nfell!", "PIKACHU", "ATTACK"), "PIKACHU's ATTACK\nfell!",
|
||||
"mixing numbered and plain directives falls back to the English source")
|
||||
eq(Strings("%s's %s\nhurt %s!", "A", "B", "C"), "A's B\nhurt C!",
|
||||
"a numbered translation that names a missing argument falls back to the English source")
|
||||
check(complaints() >= before + 2, "both numbering mistakes are reported")
|
||||
eq(Strings("%s is\nabout to use %s.\nWill %s change?", "BLUE", "PIDGEY", "RED"), "PIDGEY va être envoyé\npar BLUE. Changer?",
|
||||
"a numbered translation may leave an argument out")
|
||||
eq(Strings("Got %s!", "POTION"), "Got POTION!",
|
||||
"a translation that cannot format its arguments falls back to the formatted English")
|
||||
end
|
||||
|
||||
-- The catalog is module state, and the aggregator runs every suite in one
|
||||
-- Lua process: leaving it loaded translated the battle text underneath the
|
||||
-- suites that run after this one (parity_J asserted on "But, it failed!"
|
||||
|
||||
Reference in New Issue
Block a user