Módulo:GameText
Saltar para a navegação
Saltar para a pesquisa
This module is intended to provide functionality to the {{gameText}} template.
--------------------------------------------------------------------------------
--
-- =============================================================================
--
-- Module:GameText
--
-- Fetching text from Terraria's localization files
--
-- =============================================================================
--
-- Code annotations:
-- This module is documented according to LuaCATS (Lua Comment and Type System).
-- LuaCATS comments are prefixed with three dashes (---) and use Markdown syntax.
-- For a full list of annotations, see the following link:
-- https://luals.github.io/wiki/annotations/
--
--------------------------------------------------------------------------------
local trim = mw.text.trim
---Holds the arguments from the template call.
---@type table<string, string>
local args_table
---Full database of all strings in Terraria's localization files for the
---different languages. Is filled by the `loadDatabase` function.
---Keys are language codes (e.g. 'de', 'en', 'fr', ...) and values are the
---respective localization tables from [[Module:GameText/db-de]],
---[[Module:GameText/db-en]], [[Module:GameText/db-fr]], ...
---@type table<string, table>
local db = {}
---Return a trimmed version of the value of the template parameter with the specified `key`.
---Return `nil` if the parameter is empty or unset.
---@param key string|integer
---@return string?
local function getArg(key)
local value = args_table[key]
if not value then
return nil
end
value = trim(value)
if value == '' then
return nil
end
return value
end
---Mapping table for Terraria's input trigger placeholders.
---Recent versions of Terraria's localization files replaced some legacy
---placeholders like '<right>' or '<left>' with semantic input triggers such as
---'{InputTrigger_ToggleOrOpen}' or '{InputTrigger_UseOrAttack}'.
---However, some localization strings still use the legacy '<right>' / '<left>'
---placeholders directly.
---
---This table defines a mapping from known input trigger identifiers to the
---corresponding legacy placeholder names ('right', 'left', ...), so they can be
---handled uniformly by this module.
---
---The normalization is performed in the `getText` function, where input trigger
---placeholders are converted back to their legacy form. Placeholders that already
---use the legacy syntax are left untouched.
---@type table<string, string>
local inputTriggerAliases = {
['{InputTrigger_ToggleOrOpen}'] = '<right>',
['{InputTrigger_InteractWithTile}'] = '<right>',
['{InputTrigger_UseOrAttack}'] = '<left>',
}
---Concatenate the values of a table as a string. Works like `table.concat()`,
---except it also accepts associative arrays, i.e. tables that don't have
---purely numerical indices, like `{['foo']='bar'}`.
---@param tbl table
---@param sep string Separator for concatenation
---@return string
local function concatTable(tbl, sep)
local arr = {}
for _, v in pairs(tbl) do
arr[#arr+1] = v
end
return table.concat(arr, sep)
end
---Fill the database of strings for a given language and return it. The database
---is read-only.
---@param lang string Language code
---@return table
local function loadDatabase(lang)
if not db[lang] then
local success, result = xpcall(
-- try to load the database for the given language
function() return mw.loadJsonData('Module:GameText/db-' .. lang .. '.json') end,
-- if it doesn't exist, fall back to the English database
function() return mw.loadJsonData('Module:GameText/db-en.json') end
)
db[lang] = result or {}
end
return db[lang]
end
---Process the template parameter input for placeholder replacements.
---Terraria's localization strings contain placeholders like '{0}' or '<right>',
---which the `getText` function of this module replaces by the custom strings
---provided by template parameters. The function uses `string.gsub()` for that
---replacement and it requires a table of replacements for it, so the template
---parameters must be converted to a replacement table for `string.gsub()`.
---That conversion is what this function does. Example:
---Template parameter input: `|x_0=Angler|y_right=Right click`.
---Replacement table: `{['{0}']='Angler', ['<right>']='Right click'}`.
---
---There are two types of placeholders: '{foo}' and '<foo>'. The first type is
---prefixed with 'x_' in the template parameters, the second with 'y_'.
---@param templateParams table<string, string> Template parameter input
---@return table? repl Replacement table suitable for `string.gsub()`
local function replacementArgs(templateParams)
local replTable = {}
---Whether the `replTable` is empty.
local anyReplacements = false
-- iterate over the template parameters and extract the ones starting with
-- 'x_' or 'y_'
for paramKey, paramValue in pairs(templateParams) do
-- first type of placeholder: 'x_foo' => '{foo}'
string.gsub(paramKey, '^x_(.+)', function(s)
replTable['{' .. s .. '}'] = paramValue
anyReplacements = true
end)
-- second type of placeholder: 'y_foo' => '<foo>'
string.gsub(paramKey, '^y_(.+)', function(s)
replTable['<' .. s .. '>'] = paramValue
anyReplacements = true
end)
end
-- return `nil` if the `replTable` is empty, i.e. if there are no relevant
-- parameters in the input
if anyReplacements then
return replTable
end
end
---Return the localization string or table of strings corresponding to the given
---key. Keys can be nested using dots, e.g. `ItemName.IronPickaxe`.
---@param key string Identifier of the localization string/table
---@param lang? string Language code
---@return string|table|nil
local function get(key, lang)
key = trim(key)
-- The `key` is a dot-separated string that identifies a string or object in
-- the localization data. The JSON localization files contain nested objects
-- like so (fictional example):
-- {
-- "Buffs": {
-- "Names": {
-- "StardustMinion": "Stardust Cell"
-- }
-- }
-- }
-- Usually there are only 2 levels of nesting but we must assume that there
-- can be any number of levels.
-- The JSON data is `db[lang]` here, with JSON objects being Lua tables.
-- We "descend" into the data by splitting the `key` on dots into subkeys.
---Index of the next "subkey" within the `key`.
local nextSubkeyStartPosition = 1
-- For the key splitting we use a for-loop in the "generic form", which
-- requires a "function-in-function":
---Return a function that returns the index of the next dot in the `key`,
---starting from the current `nextSubkeyStartPosition`.
local function findNextDot()
return function()
-- note: string.find() returns start *and* end index, but those are
-- always identical because our pattern is only 1 character long
return string.find(key, '.', nextSubkeyStartPosition, true)
end
end
-- Initially we start with the entire database.
local data = db[lang] or loadDatabase(lang or 'en')
-- We locate the first subkey:
for dotPosition in findNextDot() do
local subkey = string.sub(key, nextSubkeyStartPosition, dotPosition - 1)
-- Then we descend into the `data` using this subkey:
data = data[tonumber(subkey) or subkey]
-- (`tonumber` is needed because numerical indices are true integers,
-- not "integer-as-a-string"s)
if not data then
-- subkey is invalid
return nil
end
-- Finally we repeat it with the next subkey.
nextSubkeyStartPosition = dotPosition + 1
end
-- The above is the "generic form" of the for-loop. It is repeated until
-- `dotPosition` becomes `nil`, i.e. until there are no more dots in the
-- `key`.
-- The final part of the `key` is the subkey to look for in the `data`.
if nextSubkeyStartPosition ~= 0 then
key = string.sub(key, nextSubkeyStartPosition)
end
-- Using the example data from above, this is how the process would go:
-- We would get the first subkey from the `key` 'Buffs.Names.StardustMinion',
-- i.e. 'Buffs'. We would descend one level now by setting `data`
-- (previously the entire database) to `data['Buffs']`.
-- Then we would get the second subkey, 'Names' and descend another level by
-- setting `data` to `data['Names']`.
-- There would be no more dots in the `key` now, so the current `data` would
-- be the table in which to look for the third subkey, 'StardustMinion'.
-- Since we would have descended two levels, the `data` would be:
-- `db[lang]['Buffs']['Names']`.
-- Look for the final subkey:
local result = data[tonumber(key) or key]
if result and type(result) == 'string' then
-- some localization strings contain reference marks, such as
-- '{$CommonItemTooltip.RightClickToOpen}'; replace those
result = string.gsub(result, '({%$(.-)})', function(_, referenceKey)
return get(referenceKey, lang)
end)
end
return result
end
---Return the string corresponding to the given `key`. The value of the `key`
---must be a string, not a table. Placeholders in it like '{0}' or '<right>'
---will be replaced according to the `placeholderReplacements`, which should
---be a table suitable for `string.gsub()`, e.g.:
---```lua
---{
--- ['{0}'] = 'Angler',
--- ['<right>'] = 'Right click',
--- ['{InputTrigger_Grapple}'] = 'Grappling hook key'
---}
---```
---@param key string Identifier of the localization string
---@param lang? string Language code
---@param placeholderReplacements? table<string, string>
---@return string?
local function getText(key, lang, placeholderReplacements)
if not key then
return
end
local result = get(key, lang) or get(key, 'en')
-- error handling
if not result then
return
end
if type(result) ~= 'string' then
error('The value of "' .. key .. '" is not a string! getText can only be used for strings.')
end
-- normalize new InputTrigger placeholders to legacy <left>/<right>,
-- except where replacements are already defined in the
-- `placeholderReplacements`
result = string.gsub(result, '{InputTrigger_[^}]+}', function(trigger)
local alias = inputTriggerAliases[trigger]
if alias then
if placeholderReplacements == nil or placeholderReplacements[trigger] == nil then
return alias
end
end
return trigger
end)
-- strip condition marks like '{?Homeless}'
result = string.gsub(result, '{%?.-}', '')
-- replace placeholders
if placeholderReplacements then
result = string.gsub(result, '%b{}', placeholderReplacements)
result = string.gsub(result, '%b<>', placeholderReplacements)
end
return result
end
---Return all keys of the localization database for one language on the top level.
---@return string[]
local function getSectionList()
-- the language doesn't matter because all languages should have the same keys
local data = db['en'] or loadDatabase('en')
local arr = {}
for k, v in pairs(data) do
arr[#arr + 1] = k
end
return arr
end
---Transform the `text` to make it fit for display in wikitext.
---@param text string
---@return string
local function wikify(text)
text = string.gsub(text, '\n', '<br/>')
-- two single quotes in a row (like in "ItemTooltip.PlayerVoiceCatItem")
-- need to be escaped, otherwise they're treated as wikitext syntax for
-- italic print
text = string.gsub(text, "''", "''")
return '<span class="gameText">' .. text .. '</span>'
end
--------------------------------------------------------------------------------
---Main return object
local p = {}
---For `{{gameText}}`: return a string by key.
---@param frame table Interface to the parser (`mw.frame`)
---@return string?
p.get = function(frame)
args_table = frame:getParent().args -- global input args cache
local result = getText(getArg(1), getArg('lang') or frame.args['lang'], replacementArgs(args_table))
if result then
return wikify(result)
end
end
---For `{{gameText/raw}}`: return a string by key without any processing/formatting.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.getRaw = function(frame)
return
(frame.args['prefix'] or '')
.. (getText(frame.args[1], frame.args['lang'] or require('Module:Lang').get(), replacementArgs(frame.args)) or '')
.. (frame.args['postfix'] or '')
end
---For `{{gameText/section}}`: return one table ("section") from the database as
---an array from Extension:ArrayFunctions.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.getSection = function(frame)
args_table = frame.args -- global input args cache
return mw.af.export(get(getArg(1), getArg('lang')))
end
---For `{{gameText/sectionList}}`: return all top-level keys as an array from
---Extension:ArrayFunctions.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.getSectionList = function(frame)
args_table = frame.args -- global input args cache
return mw.af.export(getSectionList())
end
---Return all keys and strings of one table from the database, separated by the
---character '¦'. The separator between each key and string is '₪'. If no key is
---provided, then merely return all keys in the entire database.
---@param frame table Interface to the parser (`mw.frame`)
---@return string?
p.listAll = function(frame)
local lang = frame.args['lang'] or 'en'
loadDatabase(lang)
local arr = {}
if frame.args[1] then
if not db[lang][frame.args[1]] then
return
end
for k, v in pairs(db[lang][frame.args[1]]) do
arr[#arr + 1] = k .. '₪' .. v
end
else
-- no key provided
for k, v in pairs(db[lang]) do
arr[#arr + 1] = k
end
end
return table.concat(arr, '¦')
end
---Return all keys of one table from the database, separated by the character '¦'.
---If no key is provided, then merely return all keys in the entire database.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.listKeys = function(frame)
local lang = frame.args['lang'] or 'en'
loadDatabase(lang)
local data
if frame.args[1] then
data = db[lang][frame.args[1]] or {}
else
-- no key provided
data = db[lang]
end
local arr = {}
for k, v in pairs(data) do
arr[#arr + 1] = k
end
return table.concat(arr, '¦')
end
---Return a tabular visualization of the entire database, where the left column
---is the key and the right column is the string.
---@param frame table Interface to the parser (`mw.frame`)
---@return string
p.printTable = function(frame)
local lang = frame.args['lang'] or 'en'
local data = loadDatabase(lang)
local allKeys = {}
local allValues = {}
local function recurse(datatable, previousKey)
for thisSubkey, thisData in pairs(datatable) do
local nextKey
if previousKey ~= '' then
nextKey = previousKey .. '.' .. thisSubkey
else
nextKey = thisSubkey
end
if type(thisData) ~= 'table' then
table.insert(allKeys, nextKey)
allValues[nextKey] = thisData
else
recurse(thisData, nextKey)
end
end
end
recurse(data, '')
table.sort(allKeys)
local output = {'<table class="wikitable"><tr><th>Key</th><th>String</th></tr>'}
for i = 1, #allKeys do
local key = allKeys[i]
local keyCell = '<td><code>' .. key .. '</code></td>'
local valueCell = '<td>' .. wikify(allValues[key]) .. '</td>'
table.insert(output, '<tr>' .. keyCell .. valueCell .. '</tr>')
end
table.insert(output, '</table>')
return table.concat(output)
end
---For other modules: return a string.
p.getText = getText
---For other modules: return a string or table without processing/formatting.
p.getData = get
---For other modules: return an array of top-level keys.
p.getSections = getSectionList
return p