--- | |
--- Specifies a comment that indicates the localization system should ignore the following code block. | |
--- This comment is used to mark code that should not be localized, such as code that generates localization keys or other metadata. | |
--- | |
--- @field localization_ignore_header string The header comment that indicates a block of code should be ignored by the localization system. | |
--- | |
localization_ignore_header = "-- [[localization-ignore]]" | |
-- command line tool shield | |
--- | |
--- Provides access to the global `const.TagLookupTable` table, which is used for looking up tag information. | |
--- | |
--- @type table | |
--- @field TagLookupTable table The global table that stores tag lookup information. | |
--- | |
const.TagLookupTable = const.TagLookupTable or {} | |
local TagLookupTable = const.TagLookupTable | |
local type = type | |
local getmetatable = getmetatable | |
local setmetatable = setmetatable | |
--- | |
--- Stores a table of random localization IDs. | |
--- | |
RandomLocIds = {} | |
--- | |
--- Specifies whether errors should be ignored when using the localization system. | |
--- | |
--- @field TIgnoreErrors boolean If true, errors will be ignored when using the localization system. | |
--- | |
local TIgnoreErrors = false | |
--- | |
--- Converts a localization ID to a light userdata value that can be used to represent the localization ID. | |
--- | |
--- @param id number The localization ID to convert. | |
--- @return lightuserdata The light userdata value representing the localization ID. | |
--- | |
function LocIDToLightUserdata(id) | |
return id and LightUserData(bor(id, locId_sig)) | |
end | |
local locId_sig = shift(0xff, 56) | |
local locId_mask = bnot(locId_sig) | |
local function LocIDToLightUserdata(id) | |
return id and LightUserData(bor(id, locId_sig)) | |
end | |
--- | |
--- Converts a light userdata value back to a localization ID. | |
--- | |
--- @param value lightuserdata The light userdata value to convert. | |
--- @return number|nil The localization ID, or nil if the value is not a valid localization ID. | |
--- | |
function LightUserdataToLocId(value) | |
value = LightUserDataValue(value) | |
if value and band(value, locId_sig) == locId_sig then | |
return band(value, locId_mask) | |
end | |
end | |
local function LightUserdataToLocId(value) | |
value = LightUserDataValue(value) | |
if value and band(value, locId_sig) == locId_sig then | |
return band(value, locId_mask) | |
end | |
end | |
-- checks for any object that may be used where a localized string is expected: | |
-- 1. Ts | |
-- 2. concatenated Ts | |
-- 3. strings that have only tags and punctuation | |
--- | |
--- Checks if a given value is compatible with the localization system. | |
--- | |
--- @param T any The value to check for compatibility. | |
--- @return boolean True if the value is compatible, false otherwise. | |
--- | |
function IsTCompatible(T) | |
return true | |
end | |
local function IsTCompatible() | |
return true | |
end | |
--- | |
--- Checks if a given value is compatible with the localization system. | |
--- | |
--- @param T any The value to check for compatibility. | |
--- @return boolean True if the value is compatible, false otherwise. | |
--- | |
function IsTCompatible(T) | |
return | |
LightUserdataToLocId(T) or | |
type(T) == "number" or | |
type(T) == "function" or | |
type(T) == "string" and IsTagsAndPunctuation(T) or | |
type(T) == "table" and (getmetatable(T) == TMeta or getmetatable(T) == TConcatMeta) | |
end | |
if Platform.debug then | |
IsTCompatible = function(T) | |
return | |
LightUserdataToLocId(T) or | |
type(T) == "number" or | |
type(T) == "function" or | |
type(T) == "string" and IsTagsAndPunctuation(T) or | |
type(T) == "table" and (getmetatable(T) == TMeta or getmetatable(T) == TConcatMeta) | |
end | |
end | |
-- checks whether the value is a localized string produced by the T function (or a UserText, which is compatible with Ts) | |
--- | |
--- Checks if a given value is compatible with the localization system. | |
--- | |
--- @param T any The value to check for compatibility. | |
--- @return boolean True if the value is compatible, false otherwise. | |
--- | |
function IsT(T) | |
return | |
T == "" or | |
LightUserdataToLocId(T) or | |
type(T) == "table" and (getmetatable(T) == TMeta or getmetatable(T) == TConcatMeta) | |
end | |
--- | |
--- Checks if a given value is a UserText object. | |
--- | |
--- @param T any The value to check. | |
--- @return boolean True if the value is a UserText object, false otherwise. | |
--- | |
function IsUserText(T) | |
return type(T) == "table" and getmetatable(T) == TMeta and T._language ~= nil | |
end | |
-- T = "" or userdata or { string,...} or { id, string,...} | |
--- | |
--- Gets the ID of a localized string. | |
--- | |
--- @param T any The localized string or table containing the localized string. | |
--- @return number|boolean The ID of the localized string, or false if the input is an empty string. | |
--- | |
function TGetID(T) | |
if T == "" then | |
return false | |
end | |
local value = LightUserdataToLocId(T) | |
if value then | |
return value | |
end | |
if type(T[1]) == "number" then | |
return T[1] | |
elseif type(T[1]) == "table" then | |
return TGetID(T[1]) | |
else | |
return LightUserdataToLocId(T[1]) | |
end | |
end | |
-- T = "" or { string,...} or { id, string,...} | |
-- debug mode only (otherwise English texts are stripped) | |
-- if 'deep' handles the case when another T is used as the string | |
--- | |
--- Gets the English text for a localized string, even in non-debug mode. | |
--- | |
--- @param T any The localized string or table containing the localized string. | |
--- @param deep boolean (optional) If true, recursively gets the English text for nested localized strings. | |
--- @param no_assert boolean (optional) If true, skips the assertion check for the input. | |
--- @return string The English text for the localized string. | |
--- | |
function TDevModeGetEnglishText(T, deep, no_assert) | |
if T == "" then | |
return "" | |
end | |
local no_assert = no_assert or not Platform.pc or Platform.ged | |
assert(no_assert or Platform.debug and IsT(T)) | |
if type(T) ~= "table" --[[shield for non-debug mode]] then | |
local id = LightUserdataToLocId(T) | |
return id and TranslationTable[id] or "Missing text" | |
end | |
local ret = type(T[1]) == "number" and T[2] or T[1] | |
ret = deep and type(ret) ~= "string" and TDevModeGetEnglishText(ret, true, no_assert) or ret | |
if not Platform.debug and type(ret)=="string" then | |
ret = ret:gsub("%(design%)%s*", ""):gsub("%(minor%)%s*", "") | |
end | |
return ret | |
end | |
--- | |
--- Sorts a table of elements by a specified field or a custom sorting function. | |
--- | |
--- @param t table The table to sort. | |
--- @param field string|function (optional) The field to sort by, or a custom sorting function. | |
--- @param case_insensitive boolean (optional) If true, the sorting will be case-insensitive. | |
--- | |
function TSort(t, field, case_insensitive) | |
local sortkey_internal_translation | |
if type(field) == "function" then | |
for i = 1, #t do | |
sortkey_internal_translation = _InternalTranslate(field(t[i])) | |
if Platform.pc then | |
t[i].__sort_key = utf8.ToUtf16(sortkey_internal_translation) | |
else | |
t[i].__sort_key = sortkey_internal_translation | |
end | |
end | |
else | |
for i = 1, #t do | |
sortkey_internal_translation = _InternalTranslate(t[i][field]) | |
if Platform.pc then | |
t[i].__sort_key = utf8.ToUtf16(sortkey_internal_translation) | |
else | |
t[i].__sort_key = sortkey_internal_translation | |
end | |
end | |
end | |
if Platform.pc then | |
local lang = table.find_value(AllLanguages, "value", GetLanguage()) | |
local wchar_locale = utf8.ToUtf16(lang and lang.locale or "en-US") | |
table.stable_sort(t, function(a,b) return LocaleCmp(a.__sort_key, b.__sort_key, wchar_locale, case_insensitive) end) | |
else | |
if case_insensitive then | |
table.stable_sort(t, function(a,b) return CmpLower(a.__sort_key, b.__sort_key) end) | |
else | |
table.stable_sort(t, function(a,b) return a.__sort_key < b.__sort_key end) | |
end | |
end | |
for i = 1, #t do | |
t[i].__sort_key = nil | |
end | |
end | |
--- | |
--- Checks if a given string contains only tags and punctuation, without any word characters. | |
--- | |
--- @param str string The input string to check. | |
--- @return boolean True if the string contains only tags and punctuation, false otherwise. | |
--- | |
function IsTagsAndPunctuation(str) | |
local untagged, tag, first, last = str:nexttag(1) | |
while tag do | |
if untagged:find("%w") then | |
return false | |
end | |
untagged, tag, first, last = str:nexttag(last+1) | |
end | |
return not untagged:find("%w") | |
end | |
--- | |
--- Checks if a given string contains only tags and punctuation, without any word characters. | |
--- | |
--- @param str string The input string to check. | |
--- @return boolean True if the string contains only tags and punctuation, false otherwise. | |
--- | |
function IsLookupTag(str) | |
local untagged, tag, first, last = str:nexttag(1) | |
if not tag then return false end | |
while tag do | |
if untagged:find("%w") then | |
return false | |
end | |
if not TagLookupTable[tag] then return false end | |
untagged, tag, first, last = str:nexttag(last+1) | |
end | |
return not untagged:find("%w") | |
end | |
--- | |
--- Checks if a given table contains any arguments besides the first one (which is assumed to be the ID). | |
--- | |
--- @param T table The input table to check. | |
--- @return boolean True if the table contains any additional arguments, false otherwise. | |
--- | |
function THasArgs(T) | |
if type(T) == "table" then | |
local hasID = type(T[1]) == "number" | |
for k,v in pairs(T) do | |
if k ~= 1 and (not hasID or k ~= 2) and k ~= "untranslated" and k ~= "_steam_id" and k ~= "_language" then | |
return true | |
end | |
if THasArgs(v) then | |
return true | |
end | |
end | |
end | |
return false | |
end | |
--- | |
--- Recursively strips any additional arguments from a localization table. | |
--- | |
--- @param _T table The localization table to strip arguments from. | |
--- @return table The localization table with only the ID and localized text. | |
--- | |
function TStripArgs(_T) | |
if type(_T) == "table" then | |
local hasID = type(_T[1]) == "number" | |
if hasID then | |
return T{_T[1], TStripArgs(_T[2])} | |
else | |
return T{TStripArgs(_T[1])} | |
end | |
end | |
return _T | |
end | |
local gender_offset = { | |
[false] = 0, | |
["m"] = 0, | |
["M"] = 0, | |
["Male"] = 0, | |
["f"] = 1, | |
["F"] = 1, | |
["Female"] = 1, | |
["n"] = 2, | |
["N"] = 2, | |
} | |
--- | |
--- Changes the localization ID to a gender-specific ID based on the provided gender. | |
--- | |
--- @param id number The original localization ID. | |
--- @param gender string|table The gender to use for the ID change. Can be a string ("m", "f", "n") or a table with a "Gender" field. | |
--- @return number The new gender-specific localization ID, or the original ID if the gender is invalid. | |
--- | |
function GenderChangedID(id, gender) | |
if type(id) ~= "number" then return id end | |
if IsT(gender) then | |
gender = GetTGender(gender) | |
elseif type(gender) == "table" then | |
gender = gender.Gender | |
end | |
local offset = gender_offset[gender or false] | |
assert(offset) | |
local new_id = id + (offset or 0) | |
return TranslationTable[new_id] and new_id or id | |
end | |
-- "T=function" syntax used to avoid LocExtract scanning of T("text") syntax | |
--- | |
--- Translates a localization ID or table into a localized string, handling gender-specific localization and random localization IDs. | |
--- | |
--- @param T table|number The localization ID or table to translate. | |
--- @param Ttext string The localized text to use if `T` is a number. | |
--- @param gender string|table The gender to use for gender-specific localization. Can be a string ("m", "f", "n") or a table with a "Gender" field. | |
--- @return string The localized string. | |
--- | |
T = function(T, Ttext, gender) | |
if type(T) == "table" then | |
if getmetatable(T[1]) == TConcatMeta then | |
return T[1] | |
end | |
local id = T[1] | |
local text = type(id) == "number" and T[2] or T[1] | |
-- we can use the dev.mode function here before processing the T | |
if text == "" then | |
return "" -- this is here to allow checking localized strings for == "" and ~= "" | |
end | |
if type(id) == "number" and type(text) == "string" then | |
if IsRandomLocId(id) then | |
RandomLocIds[id] = true | |
end | |
gender = T.TGender | |
if gender then | |
assert(not T.__gender_updated) -- the same T should not be passed more than once to this function | |
dbg(rawset(T, "__gender_updated", true)) -- mark the T so we can recognise it if we get it again | |
T[1] = GenderChangedID(id, gender) | |
end | |
if not Platform.debug and not Platform.ged and TranslationTable[id] and not THasArgs(T) then | |
return LocIDToLightUserdata(id) | |
end | |
end | |
return setmetatable(T, TMeta) | |
else | |
local id = T | |
local text = type(id) == "number" and Ttext or T | |
if text == "" then | |
return "" | |
end | |
if type(id) == "number" and type(text) == "string" then | |
if IsRandomLocId(id) then | |
RandomLocIds[id] = true | |
end | |
if gender then | |
id = GenderChangedID(id, gender) | |
end | |
if not Platform.debug and not Platform.ged and TranslationTable[id] then | |
return LocIDToLightUserdata(id) | |
end | |
end | |
return setmetatable({T, Ttext}, TMeta) | |
end | |
end | |
local locId_random_start = 100000000000 | |
local locId_random_range = 899999000000 | |
--- | |
--- Checks if the given ID is a random localization ID. | |
--- | |
--- @param id number The ID to check. | |
--- @return boolean True if the ID is a random localization ID, false otherwise. | |
--- | |
function IsRandomLocId(id) | |
if type(id) == "number" then | |
id = id - locId_random_start | |
return id >= 0 and id < locId_random_range | |
end | |
end | |
--- | |
--- Generates a random localization ID that has not been used before. | |
--- | |
--- @return number A unique random localization ID. | |
--- | |
function RandomLocId() | |
for i = 1, 1000 do | |
local id = locId_random_start + AsyncRand(locId_random_range) | |
if not RandomLocIds[id] and not RandomLocIds[id - 1] and not RandomLocIds[id - 2] and not RandomLocIds[id + 1] and not RandomLocIds[id + 2] then | |
RandomLocIds[id] = true | |
return id | |
end | |
end | |
assert(not "Failed to allocate translation ID, ID range full?") -- highly unlikely as range is huge; probably something more nefarious | |
end | |
--- | |
--- Converts the given value to a localized string, marking it as untranslated. | |
--- | |
--- @param _T any The value to convert to a localized string. | |
--- @return table A localized string table with the untranslated flag set. | |
--- | |
function Untranslated(_T) | |
if IsT(_T) then return _T end | |
if type(_T) == "table" then return T(_T) end | |
assert(not _T or type(_T) == "string" or type(_T) == "number") | |
assert(type(_T) ~= "string" or not IsLookupTag(_T), "In this case you should use TLookupTag('<tag>') instead of Untranslated('<tag>')") | |
return T{tostring(_T or ""), untranslated = true} | |
end | |
-- This exists so we can always clothe tags with T{} (even in non-debug) | |
--- | |
--- Converts a lookup tag to a localized string. | |
--- | |
--- @param _T string The lookup tag to convert. | |
--- @return table A localized string table with the lookup tag. | |
--- | |
function TLookupTag(_T) | |
assert(IsLookupTag(_T), "Use TLookupTag only for lookup tags") | |
return T{tostring(_T)} | |
end | |
--- | |
--- Initializes the localization system on first load. | |
--- | |
--- This code sets up the metatable for localized strings (`TMeta`) and the metatable for concatenated localized strings (`TConcatMeta`). It also initializes the `TranslationTable` and `TranslationGenderTable` dictionaries, and sets the `g_ignore_translation_errors` flag to `false`. | |
--- | |
--- This code is executed only on the first load of the module, and is responsible for setting up the necessary infrastructure for the localization system. | |
--- | |
if FirstLoad then | |
TMeta = { __name = "T" } | |
TConcatMeta = { __name = "T(concat)" } | |
LightUserDataSetMetatable(TMeta) | |
oldTableConcat = table.concat | |
TranslationTable = {} | |
TranslationGenderTable = {} | |
g_ignore_translation_errors = false | |
end | |
--- | |
--- Persists the localization metatable definitions to the given permanents table. | |
--- | |
--- This function is called during the game's persistence system to ensure that the | |
--- localization system's metatable definitions are properly serialized and restored | |
--- when the game is loaded. | |
--- | |
--- @param permanents table The table to store the metatable definitions in. | |
--- | |
function OnMsg.PersistGatherPermanents(permanents) | |
permanents["T.meta"] = TMeta | |
permanents["TConcat.meta"] = TConcatMeta | |
permanents["func:type"] = type | |
end | |
--- | |
--- Concatenates two localized string values. | |
--- | |
--- This function is the implementation of the `__concat` metamethod for the `TMeta` metatable. | |
--- It handles the concatenation of two localized string values, converting them to a table of localized strings if necessary. | |
--- | |
--- @param T1 table|string A localized string value or a table of localized strings to concatenate. | |
--- @param T2 table|string A localized string value or a table of localized strings to concatenate. | |
--- @return table A new table of localized strings, representing the concatenation of `T1` and `T2`. | |
--- | |
TMeta.__concat = function(T1, T2) | |
-- convert first parameter to a concatenated T type | |
if IsTCompatible(T1) then | |
if type(T1) == "table" and getmetatable(T1) == TConcatMeta then | |
T1 = table.copy(T1) | |
else | |
T1 = { T1 } | |
end | |
elseif type(T1) == "string" then | |
assert(false, string.format("Attempt to concatenate plain text or numbers '%s' to a localized string", T1), 1) | |
T1 = { T1 } | |
else | |
assert(false, string.format("Attempt to concatenate invalid value '%s' to a localized string", tostring(T1)), 1) | |
return T2 | |
end | |
-- append the second parameter into the "concatenated T" table | |
if IsTCompatible(T2) then | |
if type(T2) == "table" and getmetatable(T2) == TConcatMeta then | |
local num = #T1 | |
for i = 1, #T2 do | |
T1[num+i] = T2[i] | |
end | |
else | |
T1[1+#T1] = T2 | |
end | |
elseif type(T2) == "string" then | |
assert(false, string.format("Attempt to concatenate plain text or numbers '%s' to a localized string", T2), 1) | |
T1[1+#T1] = T2 | |
else | |
assert(false, string.format("Attempt to concatenate invalid value '%s' to a localized string", tostring(T2)), 1) | |
end | |
return setmetatable(T1, TConcatMeta) | |
end | |
--- | |
--- Prevents modifying localized strings. | |
--- | |
--- This function is the implementation of the `__newindex` metamethod for the `TMeta` metatable. | |
--- It asserts that modifying localized strings is forbidden, as in Gold Master they could be a userdata instead of a table. | |
--- | |
TMeta.__newindex = function() | |
assert(false, "Modifying localized strings is forbidden - in Gold Master they could be a userdata instead of a table", 1) | |
end | |
--- | |
--- Provides a copy of the localized string table. | |
--- | |
--- This function is the implementation of the `__copy` metamethod for the `TMeta` metatable. | |
--- It allows the localized string table to be copied using the `table.copy` function. | |
--- | |
--- @param self table The localized string table to be copied. | |
--- @return table A new table that is a copy of the localized string table. | |
--- | |
TMeta.__copy = function(self) | |
return self -- support for table.copy - treat Ts as simple values | |
end | |
--- | |
--- Converts the localized string table to Lua code. | |
--- | |
--- This function is the implementation of the `__toluacode` metamethod for the `TConcatMeta` metatable. | |
--- It generates Lua code that represents the concatenated list of localized strings. | |
--- | |
--- @param self table The concatenated list of localized strings to be converted to Lua code. | |
--- @param indent string|number The indentation level for the generated Lua code. | |
--- @param pstr string An optional string to which the generated Lua code will be appended. | |
--- @return string The generated Lua code that represents the concatenated list of localized strings. | |
--- | |
TMeta.__toluacode = function(self, indent, pstr) | |
return TToLuaCode(self, ContextCache[self], pstr) | |
end | |
--- | |
--- Compares two localized strings for equality. | |
--- | |
--- This function is the implementation of the `__eq` metamethod for the `TMeta` metatable. | |
--- It compares two localized strings for equality by checking if they are both `T` values and if their English text is the same. | |
--- | |
--- @param op1 table The first localized string to compare. | |
--- @param op2 table The second localized string to compare. | |
--- @return boolean `true` if the two localized strings are equal, `false` otherwise. | |
--- | |
TMeta.__eq = function(op1, op2) | |
return IsT(op1) and IsT(op2) and TDevModeGetEnglishText(op1, not "deep", "no assert") == TDevModeGetEnglishText(op2, not "deep", "no assert") | |
end | |
--- | |
--- Serializes a localized string table for transmission over the network. | |
--- | |
--- This function is the implementation of the `__serialize` metamethod for the `TMeta` metatable. | |
--- It asserts that only `UserText` `T` values should be serialized and sent over the network, and returns a table containing the serialized data. | |
--- | |
--- @param T table The localized string table to be serialized. | |
--- @return string, table The serialized data, which includes the metatable name and a raw copy of the table. | |
--- | |
TMeta.__serialize = function(T) | |
assert(IsUserText(T), "Only UserText T values should go through the network.") | |
return "TMeta", table.raw_copy(T) | |
end | |
--- | |
--- Deserializes a localized string table that was serialized for transmission over the network. | |
--- | |
--- This function is the implementation of the `__unserialize` metamethod for the `TMeta` metatable. | |
--- It asserts that only `UserText` `T` values should be deserialized and received over the network, and returns the deserialized table. | |
--- | |
--- @param serialized_data table The serialized data, which includes the metatable name and a raw copy of the table. | |
--- @return table The deserialized localized string table. | |
--- | |
TMeta.__unserialize = function(serialized_data) | |
local T = setmetatable(serialized_data, TMeta) | |
assert(IsUserText(T), "Only UserText T values should go through the network.") | |
return T | |
end | |
ContextCache = {} | |
--- | |
--- Concatenates two localized strings. | |
--- | |
--- This function is the implementation of the `__concat` metamethod for the `TConcatMeta` metatable. | |
--- It concatenates two localized strings, ensuring that the resulting string is also a localized string. | |
--- | |
--- @param op1 table The first localized string to concatenate. | |
--- @param op2 table The second localized string to concatenate. | |
--- @return table The concatenated localized string. | |
--- | |
TConcatMeta.__concat = TMeta.__concat | |
--- | |
--- Prevents modifying a concatenated list of localized strings. | |
--- | |
--- This function is the implementation of the `__newindex` metamethod for the `TConcatMeta` metatable. | |
--- It asserts that attempting to modify a concatenated list of localized strings is not allowed. | |
--- | |
--- @param ... any Arguments passed to the `__newindex` metamethod. | |
--- | |
TConcatMeta.__newindex = function(...) | |
assert(false, "Attempt to modify a concatenated list of localized strings", 1) | |
end | |
TConcatMeta.__newindex = function() | |
assert(false, "Attempt to modify a concatenated list of localized strings", 1) | |
end | |
--- | |
--- Generates Lua code for a concatenated list of localized strings. | |
--- | |
--- This function is the implementation of the `__toluacode` metamethod for the `TConcatMeta` metatable. | |
--- It generates Lua code that creates a `TConcat` object from the list of localized strings in the `self` table. | |
--- | |
--- @param self table The concatenated list of localized strings. | |
--- @param indent string|number The indentation level for the generated Lua code. | |
--- @param pstr string An optional string to prepend to the generated Lua code. | |
--- @return string The generated Lua code for the concatenated list of localized strings. | |
--- | |
TConcatMeta.__toluacode = function(self, indent, pstr) | |
-- ... | |
end | |
TConcatMeta.__toluacode = function(self, indent, pstr) -- for T_list properties | |
local lines, context = {}, ContextCache[self] | |
for _, value in ipairs(self) do | |
lines[#lines + 1] = TToLuaCode(value, context) | |
end | |
if type(indent) ~= "string" then | |
indent = string.rep("\t", indent or 0) | |
end | |
lines = "{\n\t" .. indent .. table.concat(lines, ",\n\t" .. indent) .. "\n" .. indent .. "}" | |
if pstr then | |
return pstr:append("TConcat(", lines, ")") | |
else | |
return string.format("TConcat(%s)", lines) | |
end | |
end | |
--- | |
--- Creates a new `TConcat` object from the given table. | |
--- | |
--- The `TConcat` object is a metatable-wrapped table that represents a concatenated list of localized strings. This function is used to create such objects, which can be used to safely concatenate localized strings without modifying the original strings. | |
--- | |
--- @param table table The table of localized strings to be concatenated. | |
--- @return table A new `TConcat` object representing the concatenated localized strings. | |
--- | |
function TConcat(table) | |
return setmetatable(table, TConcatMeta) | |
end | |
-- supports concatenation of Ts and concatenated Ts only (can't intermix with plain strings/numbers) | |
--- | |
--- Concatenates a table of localized strings, handling special cases such as concatenating `TConcat` objects and ensuring that the separator is a localized string. | |
--- | |
--- @param t table The table of localized strings to concatenate. | |
--- @param sep string|TConcat The separator to use between the localized strings. | |
--- @param i number The starting index of the table to concatenate. | |
--- @param j number The ending index of the table to concatenate. | |
--- @return string The concatenated string of localized strings. | |
--- | |
function table.concat(t, sep, i, j) | |
if not next(t) then return "" end | |
i = i or 1 | |
j = j or #t | |
local idx, item = i, t[i] | |
if i == j then return item end | |
while item == "" and idx < j do | |
idx = idx + 1 | |
item = t[idx] | |
end | |
if IsT(item) and item ~= "" then | |
for n = i, j do | |
local item = t[n] | |
assert(IsT(item), "All items in table.concat must be localized strings") | |
end | |
assert(not sep or IsTCompatible(sep), "Separator in table.concat must be a localized string or tags&punctuation only") | |
return setmetatable({ setmetatable({table = t, sep = sep, i = i, j = j}, TConcatMeta) }, TConcatMeta) | |
end | |
if IsT(sep) then | |
sep = _InternalTranslate(sep) | |
end | |
return oldTableConcat(t, sep, i, j) | |
end | |
-- first look in the nested Ts, then in the T table itself | |
--- | |
--- Recursively evaluates an identifier in the context of a localized string. | |
--- | |
--- This function is used to resolve identifiers within localized strings, such as parameters or member access. It first looks for the identifier in the inner `T` objects, then in the direct parameters of the `T` object, then in the `context_obj`, and finally in the `TagLookupTable`. | |
--- | |
--- @param T table The localized string object. | |
--- @param context_obj table The context object to use for resolving identifiers. | |
--- @param id string The identifier to evaluate. | |
--- @return any The value of the evaluated identifier. | |
--- | |
local function evalIdentifier(T, context_obj, id) | |
local value | |
if type(T) == "table" then | |
local format_string_index = type(T[1]) == "number" and 2 or 1 | |
-- 1. Recursively try the parameters of the inner T | |
local innerT = T[format_string_index] | |
if IsT(innerT) then | |
value = evalIdentifier(innerT, context_obj, id) | |
end | |
-- 2. Try direct parameters | |
value = value or T[id] | |
-- 3. Try context_obj | |
if not value and context_obj then | |
value = ResolveValue(context_obj, id) | |
end | |
if not value then | |
-- 4. Look into parameters passed in tables | |
for j = format_string_index + 1, #T do | |
local obj = T[j] | |
if context_obj ~= obj then | |
value = ResolveValue(obj, id) | |
if value then break end | |
end | |
end | |
end | |
else | |
-- 3. Try context_obj | |
if not value and context_obj then | |
value = ResolveValue(context_obj, id) | |
end | |
end | |
-- 5. apply TagLookupTable | |
if not value then -- carefully avoid changing 'false' to 'nil' | |
local lookup = TagLookupTable[id] | |
if lookup then | |
value = lookup | |
end | |
end | |
-- 6. call if func | |
if type(value) == "function" then | |
value = value(context_obj) | |
end | |
return value | |
end | |
--- | |
--- Recursively evaluates a string of identifiers, resolving each identifier against the provided `context_obj`. | |
--- | |
--- @param T table The table containing the identifiers to evaluate. | |
--- @param context_obj table The context object to use for resolving identifiers. | |
--- @param ids string The string of identifiers to evaluate. | |
--- @return table The final resolved context object. | |
--- | |
function evalIdentifiers(T, context_obj, ids) | |
-- Implementation details... | |
end | |
local function evalIdentifiers(T, context_obj, ids) | |
local first = 1 | |
while first do | |
local rest = ids:find(".", first, true) | |
context_obj = evalIdentifier(T, context_obj, ids:sub(first, (rest or 0) - 1)) | |
first = rest and rest + 1 | |
end | |
return context_obj | |
end | |
--- | |
--- Evaluates a function call with the provided parameters. | |
--- | |
--- @param T table The table containing the function to call. | |
--- @param context_obj table The context object to use for resolving identifiers in the parameters. | |
--- @param fn string The name of the function to call. | |
--- @param tag string The full tag string containing the function call and parameters. | |
--- @param param_start number The starting index of the parameters in the tag string. | |
--- @return any The result of the function call. | |
--- | |
local function evalFunctionCall(T, context_obj, fn, tag, param_start) | |
-- Implementation details... | |
end | |
local evalFunctionCall | |
--- | |
--- Evaluates the parameters in a tag string and returns them as a list of values. | |
--- | |
--- @param T table The table containing the identifiers and functions to evaluate. | |
--- @param context_obj table The context object to use for resolving identifiers in the parameters. | |
--- @param tag string The full tag string containing the parameters. | |
--- @param start number The starting index of the parameters in the tag string. | |
--- @return any, any The evaluated parameters and the remaining part of the tag string. | |
--- | |
local function evalParams(T, context_obj, tag, start) | |
local param, cont = tag:match("^%s*([%a_][%w_.]*)%s*[,)]()", start) | |
if param == "true" or param == "false" then -- bool | |
param = param == "true" | |
return param, evalParams(T, context_obj, tag, cont) | |
end | |
if param then -- identifiers? normal lookup in T params, members of T objs etc. | |
return evalIdentifiers(T, context_obj, param), evalParams(T, context_obj, tag, cont) | |
end | |
local param_start | |
param, param_start, cont = tag:match("^%s*([%a_][%w_]*)()%b()%s*[,)]()", start) | |
if param then -- function call? normal lookup in T params, members of T objs etc. | |
return evalFunctionCall(T, context_obj, param, tag, param_start + 1), evalParams(T, context_obj, tag, cont) | |
end | |
param, cont = tag:match("^%s*%'(.-)%'%s*[,)]()", start) | |
if param then -- literal string in ''s | |
return param, evalParams(T, context_obj, tag, cont) | |
end | |
param, cont = tag:match("^%s*(%-?%d+)%s*[,)]()", start) | |
if param then -- integer | |
param = tonumber(param) | |
return param, evalParams(T, context_obj, tag, cont) | |
end | |
end | |
--- | |
--- Evaluates a function call with the provided parameters. | |
--- | |
--- @param T table The table containing the function to call. | |
--- @param context_obj table The context object to use for resolving identifiers in the parameters. | |
--- @param fn string The name of the function to call. | |
--- @param tag string The full tag string containing the function call and parameters. | |
--- @param param_start number The starting index of the parameters in the tag string. | |
--- @return any The result of the function call. | |
--- | |
local function evalFunctionCall(T, context_obj, fn, tag, param_start) | |
local f = TFormat[fn] | |
if f then | |
return f(context_obj, evalParams(T, context_obj, tag, param_start)) | |
end | |
local f, obj = ResolveFunc(context_obj, fn) | |
if f then | |
return f(obj or context_obj, evalParams(T, context_obj, tag, param_start)) | |
end | |
assert(f, "unknown TFormat or context function specified in tag " .. tag) | |
end | |
evalFunctionCall = function (T, context_obj, fn, tag, param_start) | |
local f = TFormat[fn] | |
if f then | |
return f(context_obj, evalParams(T, context_obj, tag, param_start)) | |
end | |
local f, obj = ResolveFunc(context_obj, fn) | |
if f then | |
return f(obj or context_obj, evalParams(T, context_obj, tag, param_start)) | |
end | |
assert(f, "unknown TFormat or context function specified in tag " .. tag) | |
end | |
--- | |
--- Evaluates a tag in the localization system. | |
--- | |
--- @param T table The table containing the localization data. | |
--- @param context_obj table The context object to use for resolving identifiers in the tag. | |
--- @param tag string The tag to evaluate. | |
--- @return any The result of evaluating the tag. | |
--- | |
local function evalTag(T, context_obj, tag) | |
local func, param_start = tag:match("^(/?[%a_][%w_]*)()%b()$") -- find function and parameters start | |
if func then | |
return evalFunctionCall(T, context_obj, func, tag, param_start + 1) -- ATTN: Multiple return values possible (2nd value is true for preventing error checking on the resulting string) | |
end | |
return evalIdentifiers(T, context_obj, tag) | |
end | |
--- | |
--- Concatenates a table of translated strings, optionally with a separator. | |
--- | |
--- @param T table The table containing the strings to concatenate. | |
--- @param context_obj table The context object to use for translating the strings. | |
--- @param check boolean Whether to perform error checking on the translated strings. | |
--- @param tags_off boolean Whether to disable tag evaluation in the translated strings. | |
--- @return string The concatenated string. | |
--- | |
local function evalConcat(T, context_obj, check, tags_off) | |
local pieces = {} | |
local t = T.table | |
if t then | |
for i = T.i, T.j do | |
table.insert(pieces, _InternalTranslate(t[i], context_obj, check, tags_off)) | |
end | |
return oldTableConcat(pieces, T.sep and _InternalTranslate(T.sep, context_obj, check, tags_off)) | |
end | |
for i = 1, #T do | |
table.insert(pieces, _InternalTranslate(T[i], context_obj, check, tags_off)) | |
end | |
return oldTableConcat(pieces) | |
end | |
--- | |
--- Appends a translation function call to the provided string buffer. | |
--- | |
--- @param _pstr string The string buffer to append the translation to. | |
--- @param T table The localization data table. | |
--- @param context_obj table The context object to use for resolving identifiers in the tag. | |
--- @param fn string The name of the translation function to call. | |
--- @param tag string The full tag string, including the function name and parameters. | |
--- @param param_start number The starting index of the parameters in the tag string. | |
--- @param check boolean Whether to perform error checking on the translated string. | |
--- @return boolean|string False on success, or an error string on failure. | |
--- | |
local function appendTranslateFunctionCall(_pstr, T, context_obj, fn, tag, param_start, check) | |
local append_f = TFormatPstr[fn] | |
if append_f then | |
local err = append_f(_pstr, context_obj, evalParams(T, context_obj, tag, param_start)) | |
return err | |
end | |
local eval_f = TFormat[fn] | |
if eval_f then | |
local value, ignore_check = eval_f(context_obj, evalParams(T, context_obj, tag, param_start)) | |
if value == nil then | |
return "not_a_tag" | |
end | |
if not value then | |
return "failed" | |
end | |
return AppendTTranslate(_pstr, value, context_obj, check ~= false and not ignore_check) | |
end | |
local eval_f, obj = ResolveFunc(context_obj, fn) | |
if eval_f then | |
local value, ignore_check = eval_f(obj or context_obj, evalParams(T, context_obj, tag, param_start)) | |
if value == nil then | |
return "not_a_tag" | |
end | |
if not value then | |
return "failed" | |
end | |
return AppendTTranslate(_pstr, value, context_obj, check ~= false and not ignore_check) | |
end | |
assert(eval_f, "unknown TFormat or context function specified in tag " .. tag) | |
end | |
--- | |
--- Appends a translation tag to the provided string buffer. | |
--- | |
--- @param _pstr string The string buffer to append the translation to. | |
--- @param T table The localization data table. | |
--- @param context_obj table The context object to use for resolving identifiers in the tag. | |
--- @param tag string The full tag string, including the function name and parameters. | |
--- @param check boolean Whether to perform error checking on the translated string. | |
--- @return boolean|string False on success, or an error string on failure. | |
--- | |
local function appendTranslateTag(_pstr, T, context_obj, tag, check) | |
local func, param_start = tag:match("^(/?[%a_][%w_]*)()%b()$") -- find function and parameters start | |
if func then | |
local err = appendTranslateFunctionCall(_pstr, T, context_obj, func, tag, param_start + 1, check) -- ATTN: Multiple return values possible (2nd value is true for preventing error checking on the resulting string) | |
return err | |
else | |
local value, ignore_check = evalIdentifiers(T, context_obj, tag) | |
if value == nil then | |
return "not_a_tag" | |
end | |
if not value then | |
return "failed" | |
end | |
local err = AppendTTranslate(_pstr, value, context_obj, check ~= false and not ignore_check) | |
return err | |
end | |
return false | |
end | |
--- | |
--- Appends a translated string to the provided string buffer, handling any translation tags within the string. | |
--- | |
--- @param _pstr string The string buffer to append the translation to. | |
--- @param T userdata|table|string|number The localization data to translate. | |
--- @param context_obj table The context object to use for resolving identifiers in any translation tags. | |
--- @param check boolean Whether to perform error checking on the translated string. | |
--- @param tags_off boolean Whether to skip processing any translation tags in the string. | |
--- @return boolean False on success, or an error string on failure. | |
--- | |
local function appendTranslateT(_pstr, T, context_obj, check, tags_off) | |
local id = TGetID(T) | |
local str = (not Platform.debug or GetLanguage() ~= "English" or type(T) == "userdata") and TranslationTable[id] | |
or TDevModeGetEnglishText(T, "deep", "no_assert") or string.format("{#%d}", id) | |
if tags_off then | |
_pstr:append(str) | |
return false | |
end | |
local untagged, tag, first, last = str:nexttag(1) | |
context_obj = context_obj or type(T) == "table" and T[type(T[1]) == "number" and 3 or 2] or nil | |
while tag do | |
_pstr:append(untagged) | |
local success, err = procall(appendTranslateTag, _pstr, T, context_obj, tag, check) | |
if not success then | |
print("once", "evalTag", tag, "failed for", str) | |
untagged = "" | |
break | |
end | |
if err == "not_a_tag" then | |
_pstr:append_sub(str, first, last) | |
elseif err then | |
untagged = "" | |
break | |
end | |
untagged, tag, first, last = str:nexttag(last + 1) | |
end | |
_pstr:append(untagged) | |
return false | |
end | |
--- | |
--- Appends a concatenated localized string to the provided string buffer, handling any translation tags within the strings. | |
--- | |
--- @param _pstr string The string buffer to append the translation to. | |
--- @param T table The concatenated localization data to translate. | |
--- @param context_obj table The context object to use for resolving identifiers in any translation tags. | |
--- @param check boolean Whether to perform error checking on the translated strings. | |
--- @param tags_off boolean Whether to skip processing any translation tags in the strings. | |
--- @return boolean False on success, or an error string on failure. | |
--- | |
local function appendTranslateConcat(_pstr, T, context_obj, check, tags_off) | |
local AppendTTranslate = AppendTTranslate | |
local t = T.table | |
if t then | |
local t_start = T.i | |
local t_end = T.j | |
if T.sep then | |
for i = t_start, t_end - 1 do | |
AppendTTranslate(_pstr, t[i], context_obj, check, tags_off) | |
AppendTTranslate(_pstr, T.sep, context_obj, check, tags_off) | |
end | |
AppendTTranslate(_pstr, t[t_end], context_obj, check, tags_off) | |
else | |
for i = t_start, t_end do | |
AppendTTranslate(_pstr, t[i], context_obj, check, tags_off) | |
end | |
end | |
return false | |
end | |
for i = 1, #T do | |
AppendTTranslate(_pstr, T[i], context_obj, check, tags_off) | |
end | |
return false | |
end | |
--- | |
--- Appends a filtered user text string to the provided string buffer. | |
--- | |
--- @param _pstr string The string buffer to append the user text to. | |
--- @param T table The user text to append. | |
--- @param check boolean Whether to perform error checking on the user text. | |
--- @return boolean False on success, or an error string on failure. | |
--- | |
local function appendTranslateUserText(_pstr, T, check) | |
local text = GetFilteredText(T) | |
assert(not check or text, "Trying to use a UserText before AsyncFilterUserTexts or SetCustomFilteredUserText(s) call\n" .. TDevModeGetEnglishText(T, not "deep", "no_assert")) | |
_pstr:append(text or TDevModeGetEnglishText(T, not "deep", "no_assert")) | |
return false | |
end | |
--- | |
--- Appends a localized string to the provided string buffer, handling any translation tags within the strings. | |
--- | |
--- @param _pstr string The string buffer to append the translation to. | |
--- @param T table|string|number|userdata The localization data to translate. | |
--- @param context_obj table The context object to use for resolving identifiers in any translation tags. | |
--- @param check boolean Whether to perform error checking on the translated strings. | |
--- @param tags_off boolean Whether to skip processing any translation tags in the strings. | |
--- @return boolean False on success, or an error string on failure. | |
--- | |
function AppendTTranslate(_pstr, T, context_obj, check, tags_off) | |
-- TODO: assert if it's too early to translate (see locutils for a too-early translation)? | |
if T == "" then | |
return false | |
end | |
local Ttype = type(T) | |
if Ttype == "userdata" then | |
local err = appendTranslateT(_pstr, T, context_obj, check) | |
if err then | |
return err | |
end | |
elseif Ttype == "string" then | |
assert(not Platform.debug or check == false or IsTagsAndPunctuation(T), string.format("Attempt to use plain text or numbers '%s' as a localized string", T)) | |
_pstr:append(T) | |
elseif Ttype == "number" then | |
_pstr:append(LocaleInt(T)) | |
elseif IsUserText(T) then | |
local err = appendTranslateUserText(_pstr, T, check) | |
if err then | |
return err | |
end | |
elseif Ttype == "table" and getmetatable(T) == TMeta then | |
local err = appendTranslateT(_pstr, T, context_obj, check, tags_off) | |
if err then | |
return err | |
end | |
elseif Ttype == "table" and getmetatable(T) == TConcatMeta then | |
local err = appendTranslateConcat(_pstr, T, context_obj, check, tags_off) | |
if err then | |
return err | |
end | |
else | |
assert(false, string.format("Attempt to translate invalid value '%s'", tostring(T))) | |
return true | |
end | |
return false | |
end | |
--- | |
--- A boolean flag that controls whether localized string IDs should be prepended to the translated strings. | |
--- | |
--- When this flag is true, the localized string ID will be prepended to the translated string, separated by a colon. | |
--- This can be useful for debugging and identifying the source of translated strings. | |
--- | |
--- @type boolean | |
--- | |
local g_TranslatePrependIDs | |
--- | |
--- Toggles whether localized string IDs should be prepended to the translated strings. | |
--- | |
--- When this flag is true, the localized string ID will be prepended to the translated string, separated by a colon. | |
--- This can be useful for debugging and identifying the source of translated strings. | |
--- | |
function ToggleTranslatePrependIDs() | |
g_TranslatePrependIDs = not g_TranslatePrependIDs | |
Msg("TranslationChanged") | |
end | |
--- | |
--- A temporary cache for the TTranslate function's pstr object. | |
--- This allows the TTranslate function to reuse the same pstr object instead of creating a new one every time. | |
--- | |
--- @type pstr | |
--- | |
local TTranslatePstrCache = pstr("", 256) | |
--- | |
--- Translates the given value `T` using the provided context object and options. | |
--- | |
--- @param T any The value to translate. | |
--- @param context_obj table The context object to use for translation. | |
--- @param check boolean Whether to check for translation errors. | |
--- @param tags_off boolean Whether to disable HTML tag translation. | |
--- @return string The translated string. | |
--- | |
function TTranslate(T, context_obj, check, tags_off) | |
local _pstr = TTranslatePstrCache | |
if not _pstr then | |
_pstr = pstr("", 256) | |
else | |
TTranslatePstrCache = false | |
_pstr:clear() | |
end | |
local err = AppendTTranslate(_pstr, T, context_obj, check ~= false and not TIgnoreErrors, tags_off) | |
assert(not err, "translation error") | |
TTranslatePstrCache = _pstr -- return to the cache | |
if g_TranslatePrependIDs then | |
local id = TGetID(T) | |
if id then | |
return id .. ":" .. _pstr:str() | |
end | |
end | |
return _pstr:str() | |
end | |
_InternalTranslate = TTranslate | |
local ThousandsSeparator | |
--- | |
--- Formats a number with a thousands separator. | |
--- | |
--- @param x number The number to format. | |
--- @return string The formatted number string. | |
--- | |
function LocaleInt(x) | |
ThousandsSeparator = ThousandsSeparator or TTranslate(T(433967674729, --[[thousands separator]] ",")) | |
local ts = ThousandsSeparator | |
local r = "" | |
if x < 0 then | |
r = "-" | |
x = -x | |
end | |
if x < 1000 then | |
r = r .. tostring(x) | |
elseif x < 1000*1000 then | |
r = string.format("%s%d%s%03d", r, x/1000, ts, x%1000) | |
elseif x < 1000*1000*1000 then | |
r = string.format("%s%d%s%03d%s%03d", r, x/1000000, ts, (x/1000)%1000, ts, x%1000) | |
else | |
r = string.format("%s%d%s%03d%s%03d%s%03d", r, x/1000000000, ts, (x/1000000)%1000, ts, (x/1000)%1000, ts, x%1000) | |
end | |
return r | |
end | |
--- | |
--- Called when the translation system has changed. | |
--- Clears the thousands separator cache and marks the PreGameButtons object as modified. | |
--- | |
function OnMsg.TranslationChanged() | |
ThousandsSeparator = false | |
ObjModified("PreGameButtons") | |
end | |
--- | |
--- Formats a date and time string in the user's locale. | |
--- | |
--- @param os_time number The Unix timestamp to format. | |
--- @return string The formatted date and time string. | |
--- | |
function LocaleDateTime(os_time) | |
return os.date(GetLanguage() == "Japanese" and "%Y.%m.%d %H:%M" or "%d %b %Y %H:%M", os_time) | |
end | |
-- Returns the order of month, day and year. | |
-- This handles transforming the output of the various systems into an array. | |
-- Don't call this on a hot path please :) | |
-- Example: YYYYMMDD -> { year, month, day } | |
-- M/d/YYYY -> { month, day, year } | |
-- dd mmm Y -> { day, month, year } | |
--- | |
--- Returns the order of month, day and year based on the system date format. | |
--- | |
--- @return table The order of month, day and year as an array of strings. | |
--- | |
function GetDateTimeOrder() | |
local format = GetSystemDateFormat() | |
local lastC = false | |
local order = {} | |
for i = 1, #format do | |
local c = format:sub(i, i) | |
local isMonthChar = c == "m" or c == "M" | |
local isYearChar = c == "Y" or c == "y" | |
local isDayChar = c == "D" or c == "d" | |
local isValidChar = isMonthChar or isYearChar or isDayChar | |
if isValidChar then | |
if c ~= lastC then | |
if isMonthChar then | |
order[#order + 1] = "month" | |
elseif isYearChar then | |
order[#order + 1] = "year" | |
elseif isDayChar then | |
order[#order + 1] = "day" | |
end | |
end | |
lastC = c | |
end | |
end | |
return order | |
end | |
--- | |
--- Converts a localization table or ID to a Lua code string. | |
--- | |
--- @param T table|number The localization table or ID to convert. | |
--- @param context string The context of the localization. | |
--- @param pstr string An optional string to append the Lua code to. | |
--- @return string The Lua code representation of the localization. | |
--- | |
function TToLuaCode(T, context, pstr) | |
if IsUserText(T) then | |
return UserTextToLuaCode(T, context, pstr) | |
end | |
assert(not THasArgs(T)) | |
return IDTextToLuaCode(TGetID(T), TDevModeGetEnglishText(T, not "deep", "no assert"), context, pstr) | |
end | |
--- | |
--- Converts a user text localization object to a Lua code string. | |
--- | |
--- @param T table The user text localization object to convert. | |
--- @param context string The context of the localization. | |
--- @param pstr string An optional string to append the Lua code to. | |
--- @return string The Lua code representation of the user text localization. | |
--- | |
function UserTextToLuaCode(T, context, pstr) | |
local lua_str = string.format("T%s", TableToLuaCode(T)) | |
if pstr then | |
return pstr:appendf(lua_str) | |
end | |
return string.format(lua_str) | |
end | |
--- | |
--- Converts a localization ID and text to a Lua code string. | |
--- | |
--- @param id number The localization ID. | |
--- @param text string The localization text. | |
--- @param context string The context of the localization. | |
--- @param pstr string An optional string to append the Lua code to. | |
--- @return string The Lua code representation of the localization. | |
--- | |
function IDTextToLuaCode(id, text, context, pstr) | |
-- ... | |
end | |
function IDTextToLuaCode(id, text, context, pstr) | |
local context_str = context and context ~= "" and string.format("--[[%s]] ", context) or "" | |
if id then | |
if text ~= "" then | |
if pstr then | |
return pstr:appendf("T(%d, %s%v)", id, context_str, text) | |
end | |
return string.format("T(%d, %s%s)", id, context_str, StringToLuaCode(text)) | |
else | |
if pstr then | |
return pstr:append('""') | |
end | |
return '""' | |
end | |
else | |
if pstr then | |
return pstr:appendf("T(%s%v)", context_str, text) | |
end | |
return string.format("T(%s%s)", context_str, StringToLuaCode(text)) | |
end | |
end | |
local csv_load_fields = { [1] = "id", [2] = "text", [5] = "translated", [3] = "translated_new", [7] = "gender" } | |
--- | |
--- Loads translation tables from a CSV file. | |
--- | |
--- @param filename string The path to the CSV file containing the translation data. | |
--- @return boolean Whether the translation tables were successfully loaded. | |
--- | |
function LoadTranslationTableFile(filename) | |
local loaded = {} | |
LoadCSV(filename, loaded, csv_load_fields, "omit_captions") | |
return ProcessLoadedTables(loaded, GetLanguage(), TranslationTable, TranslationGenderTable) | |
end | |
--- | |
--- Loads translation tables from a folder containing CSV files. | |
--- | |
--- @param path string The path to the folder containing the CSV files. | |
--- @param language string The language to load the translation tables for. | |
--- @param out_table table The table to store the loaded translations. | |
--- @param out_gendertable table The table to store the gender information for the translations. | |
--- @return boolean Whether the translation tables were successfully loaded. | |
--- | |
function LoadTranslationTablesFolder(path, language, out_table, out_gendertable) | |
local loaded = {} | |
local files = io.listfiles(path, "*.csv") or {} | |
table.sort(files) | |
for _, filename in ipairs(files) do | |
LoadCSV(filename, loaded, csv_load_fields, "omit_captions") | |
end | |
return ProcessLoadedTables(loaded, language, out_table, out_gendertable) | |
end | |
--- | |
--- Processes the loaded translation tables, extracting the appropriate translation text and storing it in the output tables. | |
--- | |
--- @param loaded table The table containing the loaded translation data. | |
--- @param language string The language to process the translations for. | |
--- @param out_table table The table to store the loaded translations. | |
--- @param out_gendertable table The table to store the gender information for the translations. | |
--- @return boolean Whether the translation tables were successfully loaded. | |
--- | |
function ProcessLoadedTables(loaded, language, out_table, out_gendertable) | |
local order = { "translated_new", "translated", "text" } | |
if language == "English" then | |
order = { "translated_new", "text", "translated" } | |
end | |
for _, entry in ipairs(loaded) do | |
local translation | |
if entry[order[1]] and entry[order[1]] ~= "" then | |
translation = entry[order[1]] | |
elseif entry[order[2]] and entry[order[2]] ~= "" then | |
translation = entry[order[2]] | |
else | |
translation = entry[order[3]] | |
end | |
local id = tonumber(entry.id) | |
if id then | |
out_table[id] = translation | |
if out_gendertable then | |
out_gendertable[id] = entry.gender | |
end | |
end | |
end | |
return next(loaded) ~= nil | |
end | |
--- A table of languages that should always wrap text, regardless of the user's text wrapping settings. | |
--- | |
--- This table is used to ensure that certain languages, such as Chinese, Japanese, and Korean, always wrap text properly, even if the user has disabled text wrapping in their settings. | |
--- | |
--- @field Schinese boolean Whether Simplified Chinese should always wrap text. | |
--- @field Tchinese boolean Whether Traditional Chinese should always wrap text. | |
--- @field Japanese boolean Whether Japanese should always wrap text. | |
--- @field Koreana boolean Whether Korean should always wrap text. | |
local AlwaysWrapLanguages = { | |
Schinese = true, | |
Tchinese = true, | |
Japanese = true, | |
Koreana = true, | |
} | |
--- | |
--- Loads the translation tables for the current language. | |
--- | |
--- This function loads the translation tables for the current language, and stores them in the `TranslationTable` and `TranslationGenderTable` global variables. It first attempts to load the tables from the `CurrentLanguage` directory in the executable directory, and if that fails, it tries to load them from the `CurrentLanguage/` directory. If that also fails, and the game is not in debug or command-line mode, it asserts an error. | |
--- | |
--- The function also sets the `config.TextWrapAnywhere` flag based on whether the current language is one of the languages that should always wrap text, as defined in the `AlwaysWrapLanguages` table. | |
--- | |
--- Finally, the function sends a "TranslationChanged" message to notify other parts of the application that the translation tables have been updated. | |
--- | |
function LoadTranslationTables() | |
TranslationTable = {} | |
collectgarbage("collect") | |
local path = GetExecDirectory() .. "CurrentLanguage" | |
if not LoadTranslationTablesFolder(path, GetLanguage(), TranslationTable, TranslationGenderTable) then | |
if not LoadTranslationTablesFolder("CurrentLanguage/", GetLanguage(), TranslationTable, TranslationGenderTable) and | |
not Platform.debug and not Platform.cmdline | |
then | |
assert(false, "Localization table not found in non-developer mode! (For testing you could copy the game.csv from LocalizationOut/English/CurrentLanguage to Bin/CurrentLanguage. Build the table with the LocExtract build command if it's not present.)") | |
end | |
end | |
config.TextWrapAnywhere = AlwaysWrapLanguages[GetLanguage()] or false | |
if not Loading then | |
Msg("TranslationChanged") | |
end | |
collectgarbage("collect") | |
end | |
-- used by build | |
g_BuildLocTables = false | |
g_BuildLocTablesSignal = false | |
--- | |
--- Loads the localization tables for the specified project path. | |
--- | |
--- This function loads the localization tables for all languages found in the `LocalizationOut` directory of the specified project path. It first checks if the tables have already been loaded, and if so, waits for the first thread to finish loading them. Otherwise, it loads the tables and stores them in the `g_BuildLocTables` global variable. | |
--- | |
--- The function first lists all the language folders in the `LocalizationOut` directory, then for each language, it loads all the CSV files in the `CurrentLanguage` subdirectory. It parses the CSV files and stores the localization data in a table, with the language name as the key. | |
--- | |
--- Once all the localization tables have been loaded, the function sets the `g_BuildLocTables` global variable and signals any waiting threads that the tables have been loaded. | |
--- | |
--- @param project_path string The path to the project directory containing the localization files. | |
--- | |
function LoadBuildLocTables(project_path) | |
if g_BuildLocTables then return end | |
if g_BuildLocTablesSignal then | |
WaitMsg(g_BuildLocTablesSignal) -- if several build threads try to load the table concurrently, all wait for the first to finish | |
return | |
end | |
g_BuildLocTablesSignal = {} | |
local loctables = {} | |
local err, languages = AsyncListFiles(project_path .. "/LocalizationOut", "*", "folders,relative") | |
if err then print("Error loading translation tables: ", err) return end | |
for _, language in ipairs(languages) do | |
local path = project_path .. "/LocalizationOut/" .. language .. "/CurrentLanguage" | |
local err, files = AsyncListFiles(path, "*.csv") | |
if not err then | |
local loaded = {} | |
local needed_fields = { [1] = "id", [2] = "text", [3] = "translated_new" } | |
table.sort(files) | |
for i = 1, #files do | |
LoadCSV(files[i], loaded, needed_fields, "omit_captions") | |
end | |
local order = { "translated_new" } | |
if language == "English" then | |
order = { "translated_new", "text" } | |
end | |
local lang_result = {} | |
for _, entry in ipairs(loaded) do | |
if entry[order[1]] and entry[order[1]] ~= "" then | |
lang_result[tonumber(entry.id)] = entry[order[1]] | |
elseif order[2] and entry[order[2]] and entry[order[2]] ~= "" then | |
lang_result[tonumber(entry.id)] = entry[order[2]] | |
end | |
end | |
loctables[language] = lang_result | |
end | |
end | |
g_BuildLocTables = loctables | |
Msg(g_BuildLocTablesSignal) | |
g_BuildLocTablesSignal = false | |
end | |
--- | |
--- Returns the gender of the given translation table. | |
--- | |
--- @param T table The translation table. | |
--- @return string|false The gender of the translation table, or `false` if no gender is defined. | |
--- | |
function GetTGender(T) | |
return TranslationGenderTable[TGetID(T) or false] or false | |
end | |
-- gender can be: | |
-- * a string (M/F/N) | |
-- * a T and we use GetTGender(T) | |
-- * a table and we use gender.Gender | |
-- T can be: | |
-- * a simple T (no parameters) in which case we return the alternative gender variant of T | |
-- * a T with parameters in which case we MODIFY it in-place to reflect the requestsed gender | |
--- | |
--- Returns a translation table with the gender variant specified. | |
--- | |
--- @param T table The translation table. | |
--- @param gender string The gender variant to use. Can be "M", "F", or "N". | |
--- @return table The translation table with the specified gender variant. | |
--- | |
function GetTByGender(T, gender) | |
if (T or "") == "" then return T end | |
if THasArgs(T) then -- in-place modification of a T with parameters - the assumption is that the function gets called with a newly created T table | |
assert(type(T) == "table" and getmetatable(T) == TMeta) -- we do not work with concatenated strings | |
assert(not T.__gender_updated) -- the same T should not be passed more than once to this function | |
dbg(rawset(T, "__gender_updated", true)) -- mark the T so we can recognise it if we get it again | |
T[1] = GenderChangedID(T[1], gender) | |
return T | |
else | |
local id = GenderChangedID(TGetID(T), gender) | |
return TranslationTable[id or false] and LocIDToLightUserdata(id) or T | |
end | |
end | |
--- | |
--- Returns the gender-specific suffix for the given translation table ID. | |
--- | |
--- @param T table The translation table. | |
--- @param id string The translation table ID. | |
--- @return string The gender-specific suffix for the given ID. | |
--- | |
function IdGenderSuffix(T, id) | |
local gender = TranslationGenderTable[TGetID(T) or false] or "M" | |
if gender == "F" then | |
return id .. "_f" | |
elseif gender == "N" then | |
return id .. "_n" | |
else | |
return id .. "_m" | |
end | |
end | |
--global functions for controlling/getting windows ime state | |
local IME_languages = {"Koreana","Japanese","Schinese","Tchinese"} | |
--- | |
--- Initializes the Windows IME (Input Method Editor) state for the current language. | |
--- | |
--- This function is responsible for enabling or disabling the IME based on the current language, and controlling the visibility of the IME candidate window. | |
--- | |
--- The IME is enabled for languages that have been properly implemented, and disabled for languages that have not. The IME candidate window is also disabled for the "Koreana" language, as the game's fonts do not support Hanja input. | |
--- | |
--- Additionally, the function sets the `hr.HideIme` flag to `true`, which is used to hide the IME window when the user has no editable controls in focus, allowing them to control the game without the IME window interfering. | |
--- | |
--- It is the responsibility of the Lua controls to enable/disable the IME as needed. | |
--- | |
function InitWindowsImeState() | |
--since ime has different behaviour for each language, disable it for languages that have | |
--not been properly implemented. | |
local lang = GetLanguage() | |
config.EnableIme = Platform.pc and table.find(IME_languages,lang) | |
--print("config.EnableIme:", config.EnableIme) | |
config.EnableImeCandidateWindow = lang ~= "Koreana" --we don't support hanja in fonts so kill the candidate window to avoid hanja input. | |
--print("config.EnableImeCandidateWindow:", config.EnableImeCandidateWindow) | |
--we want ime hidden when the user has no editable controls on focus | |
--so that the user is able to control the game without the ime window poping up and eating input | |
--it is the responsibility of lua controls to enable/disable as needed. | |
hr.HideIme = true | |
end | |
--- | |
--- Checks if the Input Method Editor (IME) is enabled. | |
--- | |
--- @return boolean True if the IME is enabled, false otherwise. | |
--- | |
function IsImeEnabled() | |
return config.EnableIme | |
end | |
--- | |
--- Sets the position of the Windows Input Method Editor (IME) window. | |
--- | |
--- This function is responsible for setting the position of the IME window relative to the upper left corner of the window. It assumes that the user will only edit one editable control at a time, and subsequent calls within the same frame are okay, but the final position will be processed at the end of the frame. | |
--- | |
--- @param x number The x-coordinate of the IME window position. | |
--- @param y number The y-coordinate of the IME window position. | |
--- @param fontId number The font ID to use for the IME window. | |
--- | |
function SetImePosition(x, y, fontId) | |
end | |
function SetImePosition(x, y, fontId) --relative to upper left corner of window, or so msdn claims. | |
if IsImeEnabled() then | |
--this assumes that the user will edit only one editable control @ a time. | |
--subsequent calls in the same frame are ok, but keep in mind hr. vars will get processed @ the end of the frame. | |
--this means only the last call per frame would actually get processed. | |
hr.WindowsImePositionX = x | |
hr.WindowsImePositionY = y | |
hr.WindowsImeFontId = fontId or -1 | |
hr.WindowsImePosChanged = hr.WindowsImePosChanged + 1 | |
end | |
end | |
--will temporariliy disable ime from showing up so the player can control the game | |
--- | |
--- Hides the Windows Input Method Editor (IME) window. | |
--- | |
--- This function is responsible for hiding the IME window so that the user can control the game without the IME window popping up and eating input. It is the responsibility of the Lua controls to enable/disable the IME as needed. | |
--- | |
--- @return nil | |
--- | |
function HideIme() | |
if IsImeEnabled() then | |
if not hr.HideIme then | |
hr.HideIme = true | |
end | |
end | |
end | |
--reverts changes done by HideIme() | |
--- | |
--- Reverts the changes made by `HideIme()`, allowing the Windows Input Method Editor (IME) window to be shown again. | |
--- | |
--- This function is responsible for restoring the ability for the IME window to be displayed, after it has been hidden by the `HideIme()` function. It is the responsibility of the Lua controls to enable/disable the IME as needed. | |
--- | |
--- @return nil | |
--- | |
function ShowIme() | |
if IsImeEnabled() then | |
if hr.HideIme then | |
hr.HideIme = false | |
end | |
end | |
end | |
--- | |
--- Gets the width and height of the IME window based on the specified font ID. | |
--- | |
--- This function is used to determine the size of the IME window, which is necessary for positioning the IME window correctly on the screen. It uses the `terminal.GetWindowsImeCompositionString()` function to retrieve the current composition string, and then measures the text using `UIL.MeasureText()` to get the width and height. | |
--- | |
--- @param fontId number The font ID to use for measuring the IME window size. | |
--- @return number, number The width and height of the IME window. | |
--- | |
function GetImeWindowWidthHeight(fontId) | |
local compStr = terminal.GetWindowsImeCompositionString() | |
if compStr then | |
return UIL.MeasureText(compStr, fontId) --another hack because ImmGetCompositionWindow returns empty rect. | |
end | |
return 0,0 | |
end | |
--- | |
--- A table of localization data for various languages, including their display names, PlayStation locale codes, and other locale codes. | |
--- | |
--- This table contains information about the supported localization languages, including the display name, PlayStation locale code, locale code, Paradox locale code, and Epic Games locale code for each language. | |
--- | |
--- @field value string The unique identifier for the language. | |
--- @field text string The display name for the language. | |
--- @field ps_locale string The PlayStation locale code for the language. | |
--- @field locale string The locale code for the language. | |
--- @field pdx_locale string The Paradox locale code for the language. | |
--- @field epic_locale string The Epic Games locale code for the language. | |
--- | |
AllLanguages = { | |
{ value = "Brazilian", text = T(699854757080, "Brazilian Portuguese"), ps_locale = "pt-BR", locale = "pt-BR", pdx_locale = "pt", epic_locale = "pt-BR", }, | |
{ value = "Bulgarian", text = T(385829073168, "Bulgarian"), ps_locale = "bg-BG", locale = "bg-BG", pdx_locale = "bg", epic_locale = false, }, | |
{ value = "Czech", text = T(552240423015, "Czech"), ps_locale = "cs-CZ", locale = "cs-CZ", pdx_locale = "cs", epic_locale = false, }, | |
{ value = "Danish", text = T(782416127227, "Danish"), ps_locale = "da-DK", locale = "da-DK", pdx_locale = "da", epic_locale = "da", }, | |
{ value = "Dutch", text = T(675114896426, "Dutch"), ps_locale = "nl-NL", locale = "nl-NL", pdx_locale = "nl", epic_locale = "nl", }, | |
{ value = "English", text = T(147611982706, "English"), ps_locale = "en-US", locale = "en-US", pdx_locale = "en", epic_locale = "en-US", }, | |
{ value = "Finnish", text = T(283206621979, "Finnish"), ps_locale = "fi-FI", locale = "fi-FI", pdx_locale = "fi", epic_locale = "fi", }, | |
{ value = "French", text = T(170273676234, "French"), ps_locale = "fr-FR", locale = "fr-FR", pdx_locale = "fr", epic_locale = "fr", }, | |
{ value = "German", text = T(505552009073, "German"), ps_locale = "de-DE", locale = "de-DE", pdx_locale = "de", epic_locale = "de", }, | |
{ value = "Hungarian", text = T(646055054297, "Hungarian"), ps_locale = "hu-HU", locale = "hu-HU", pdx_locale = "hu", epic_locale = false, }, | |
{ value = "Indonesian", text = T(596539604344, "Indonesian"), ps_locale = "id-ID", locale = "id-ID", pdx_locale = "id", epic_locale = false, }, | |
{ value = "Italian", text = T(330877865785, "Italian"), ps_locale = "it-IT", locale = "it-IT", pdx_locale = "it", epic_locale = "it", }, | |
{ value = "Japanese", text = T(527962174587, "Japanese"), ps_locale = "ja-JP", locale = "ja-JP", pdx_locale = "ja", epic_locale = "ja", }, | |
{ value = "Koreana", text = T(585811408758, "Korean"), ps_locale = "ko-KR", locale = "ko-KR", pdx_locale = "ko", epic_locale = "ko", }, | |
{ value = "Norwegian", text = T(369233670775, "Norwegian"), ps_locale = "nb-NO", locale = "nb-NO", pdx_locale = "no", epic_locale = "no", }, | |
{ value = "Polish", text = T(197791212449, "Polish"), ps_locale = "pl-PL", locale = "pl-PL", pdx_locale = "pl", epic_locale = "pl", }, | |
{ value = "Portuguese", text = T(661132086100, "Portuguese"), ps_locale = "pt-PT", locale = "pt-PT", pdx_locale = "pt", epic_locale = false, }, | |
{ value = "Romanian", text = T(375694388084, "Romanian"), ps_locale = "ro-RO", locale = "ro-RO", pdx_locale = "ro", epic_locale = false, }, | |
{ value = "Russian", text = T(794451731349, "Russian"), ps_locale = "ru-RU", locale = "ru-RU", pdx_locale = "ru", epic_locale = "ru", }, | |
{ value = "Schinese", text = T(465743231919, "Chinese (Simplified)"), ps_locale = "zh-Hans", locale = "zh-CN", pdx_locale = "zh", epic_locale = "zh-Hans", }, | |
{ value = "Spanish", text = T(277226277909, "Spanish (Spain)"), ps_locale = "es-ES", locale = "es-ES", pdx_locale = "es", epic_locale = "es-ES", }, | |
{ value = "Latam", text = T(342769994919, "Spanish (Latin America)"), ps_locale = "es-MX", locale = "es-MX", pdx_locale = "es", epic_locale = "es-MX", }, | |
{ value = "Swedish", text = T(487752404194, "Swedish"), ps_locale = "sv-SE", locale = "sv-SE", pdx_locale = "sv", epic_locale = "sv", }, | |
{ value = "Tchinese", text = T(508880261610, "Chinese (Traditional)"), ps_locale = "zh-Hant", locale = "zh-TW", pdx_locale = "zh", epic_locale = "zh-Hant", }, | |
{ value = "Thai", text = T(681908731541, "Thai"), ps_locale = "th-TH", locale = "th-TH", pdx_locale = "th", epic_locale = "th", }, | |
{ value = "Turkish", text = T(218295023775, "Turkish"), ps_locale = "tr-TR", locale = "tr-TR", pdx_locale = "tr", epic_locale = "tr", }, | |
} | |
--- Indicates that the language names for Traditional Chinese and Simplified Chinese start with the "Tchinese" and "Schinese" family names, respectively. | |
LanguagesWithNamesStartWithFamily = { | |
["Tchinese"] = true, | |
["Schinese"] = true, | |
} | |
-- TODO(mitko): Move to PlayStationRules.lua when trophies building stop depending on DataInstances | |
-- Copied from: | |
-- https://ps4.siedev.net/resources/documents/Misc/current/Live_Item_Admin_Tool-Users_Guide/0003.html#__document_toc_00000006 | |
-- https://ps4.siedev.net/resources/documents/Misc/current/Param_File_Editor-Users_Guide/0004.html#0_Ref368663318 | |
--- Defines a mapping between PlayStation language codes and their corresponding language names. | |
-- The mapping is used to convert between PlayStation language codes and language names used in the game. | |
-- The table contains the language name, the corresponding PlayStation language code, and the PlayStation game code. | |
-- This mapping is used in various parts of the game to handle localization and language-specific functionality. | |
PlayStationLanguageCodes = { | |
-- hg pack, sfo, gp | |
"Japanese", "00", -- Japanese | |
"English", "01", -- English (United States) | |
"French", "02", -- French | |
"Spanish", "03", -- Spanish | |
"German", "04", -- German | |
"Italian", "05", -- Italian | |
"", "06", -- Dutch | |
"Portuguese", "07", -- Portuguese (Portugal) | |
"Russian", "08", -- Russian | |
"Koreana", "09", -- Korean | |
"Tchinese", "10", -- Chinese (traditional) | |
"Schinese", "11", -- Chinese (simplified) | |
"", "12", -- Finnish | |
"", "13", -- Swedish | |
"", "14", -- Danish | |
"", "15", -- Norwegian | |
"Polish", "16", -- Polish | |
"Brazilian", "17", -- Portuguese (Brazil) | |
"English", "18", -- English (United Kingdom) | |
"", "19", -- Turkish | |
"Latam", "20", -- Spanish (Latin America) | |
"French", "22", -- French (Canada) | |
"Czech", "23", -- Czech | |
"Hungarian", "24", -- Hungarian | |
"", "25", -- Greek | |
"Romanian", "26", -- Romanian | |
"Thai", "27", -- Thai | |
"", "28", -- Vietnamese | |
"Indonesian", "29", -- Indonesian | |
} | |
--- Converts a locale string to the corresponding PlayStation locale code. | |
--- | |
--- @param locale string The locale string to convert. | |
--- @return string The PlayStation locale code corresponding to the input locale. | |
function LocaleToPlayStationLocale(locale) | |
return table.find_value(AllLanguages, "locale", locale).ps_locale | |
end | |
--- | |
--- Checks if a localization language is available. | |
--- | |
--- @param language string The language to check for availability. | |
--- @return boolean True if the localization language is available, false otherwise. | |
function IsLocalizationLanguageAvailable(language) | |
local folder_or_pack = | |
(config.UnpackedLocalization or config.UnpackedLocalization == nil and IsFSUnpacked()) | |
and ("svnProject/LocalizationOut/" .. language .. "/CurrentLanguage/") | |
or ("Local/" .. language .. ".hpk") | |
return io.exists(folder_or_pack) | |
end | |
--- | |
--- Initializes the localization options for the game. | |
--- | |
--- This function is called on game startup and sets up the available localization options. | |
--- It first adds an "Auto" option, which automatically selects the system language. | |
--- Then, it checks if the game is running on a desktop platform and if the `OptionsData` table exists. | |
--- If so, it iterates through the `AllLanguages` table and adds any available localization languages to the options list. | |
--- Finally, it sets the `OptionsData.Options.Language` table to the resulting list of localization options. | |
--- | |
--- @return nil | |
function OnMsg.Autorun() | |
local result = { | |
{ value = "Auto", text = T(388818321440, "Auto"), iso_639_1 = "en" } | |
} | |
if Platform.desktop and rawget(_G, "OptionsData") then | |
for _, language in ipairs(AllLanguages) do | |
if IsLocalizationLanguageAvailable(language.value) then | |
result[#result+1] = language | |
end | |
end | |
OptionsData.Options.Language = result | |
end | |
end | |
local list_separator = T(651365107459, --[[list separator]] ", ") | |
--- | |
--- Concatenates a list of values into a single string, using the provided separator. | |
--- | |
--- If no separator is provided, the default list separator from the localization system is used. | |
--- | |
--- @param list table The list of values to concatenate. | |
--- @param separator string (optional) The separator to use between the list items. | |
--- @return string The concatenated string. | |
function TList(list, separator) | |
return table.concat(list, separator or _InternalTranslate(list_separator)) | |
end | |
-- given "abra keyword:cad abra", "keyword" -> returns "abra abra", "cad" | |
--- | |
--- Extracts a value from a string based on a given needle pattern. | |
--- | |
--- If the needle pattern is found in the haystack string, this function will extract the value that follows the needle pattern and return the haystack string with the extracted value removed. | |
--- | |
--- @param haystack string The string to search and extract from. | |
--- @param needle string The pattern to search for in the haystack. | |
--- @return string, string The haystack string with the extracted value removed, and the extracted value. | |
function match_and_remove(haystack, needle) | |
if not haystack then return end | |
local extracted = string.match(haystack, needle .. "(%g*)") | |
if extracted then | |
local st, nd = string.find(haystack, needle .. extracted, 1, "plain") | |
return string.sub(haystack, 1, st-1) .. string.sub(haystack, nd+1), extracted | |
else | |
return haystack | |
end | |
end |