Módulo:GameText

Fonte: Terraria Wiki
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, "''", "'&#39;")
	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