Files
Questie-X/Localization/l10n.lua
T
Xurkon c7af49d8bd perf(l10n): add zero-arg fast path to translate
_l10n:translate now short-circuits the common no-argument case
(select('#', ...) == 0), returning the translation directly and skipping
the {...} allocation, the tostring loop, and safeFormat. Behavior matches
the slow path: a successful no-arg lookup already returned the raw value,
and missing/invalid entries fall back to the key. Cherry-picked from
phase2-lua50-sweep.
2026-06-08 16:25:09 -05:00

291 lines
9.6 KiB
Lua

---@class l10n
---@field continentLookup table
---@field zoneLookup table
---@field zoneCategoryLookup table
---@field questCategoryLookup table
local l10n = QuestieLoader:CreateModule("l10n")
local _l10n = {}
l10n.translations = {}
l10n.itemLookup = {}
l10n.npcNameLookup = {}
l10n.objectNameLookup = {}
l10n.objectLookup = {}
l10n.questLookup = {}
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local locale = 'enUS'
local supportedLocals = {
['enUS'] = true,
['esES'] = true,
['esMX'] = true,
['ptBR'] = true,
['frFR'] = true,
['deDE'] = true,
['ruRU'] = true,
['zhCN'] = true,
['zhTW'] = true,
['koKR'] = true,
}
function l10n:InitializeLocaleOverride()
local overridingLocale = QUESTIE_LOCALES_OVERRIDE.locale
supportedLocals[overridingLocale] = true
l10n.itemLookup[overridingLocale] = QUESTIE_LOCALES_OVERRIDE.itemLookup
l10n.questLookup[overridingLocale] = QUESTIE_LOCALES_OVERRIDE.questLookup
l10n.npcNameLookup[overridingLocale] = QUESTIE_LOCALES_OVERRIDE.npcNameLookup
l10n.objectLookup[overridingLocale] = QUESTIE_LOCALES_OVERRIDE.objectLookup
for id, _ in pairs(l10n.translations) do
if QUESTIE_LOCALES_OVERRIDE.translations[id] ~= nil then
l10n.translations[id][overridingLocale] = QUESTIE_LOCALES_OVERRIDE.translations[id]
else
l10n.translations[id][overridingLocale] = false
end
end
end
---@param zoneName string
---@return number
function l10n:GetAreaIdByLocalName(zoneName)
if not zoneName or zoneName == "" then return 0 end
for _, zoneTable in pairs(l10n.zoneLookup) do
for areaId, name in pairs(zoneTable) do
if name == zoneName then return areaId end
end
end
return 0
end
---@param areaId number
---@return string
function l10n:GetLocalNameByAreaId(areaId)
if not areaId or areaId <= 0 then return l10n("Unknown Zone") end
for _, zoneTable in pairs(l10n.zoneLookup) do
if zoneTable[areaId] then
return zoneTable[areaId]
end
end
return l10n("Unknown Zone")
end
---@param areaId number
---@return number
function l10n:GetContinentIdByAreaId(areaId)
if not areaId or areaId <= 0 then return 0 end
for continentId, zoneTable in pairs(l10n.zoneLookup) do
if zoneTable[areaId] then
return continentId
end
end
return 0
end
function l10n:Initialize()
-- Load item locales
if l10n.itemLookup and l10n.itemLookup[locale] then
for id, name in pairs(l10n.itemLookup[locale]) do
if QuestieDB.itemData[id] and name then
QuestieDB.itemData[id][QuestieDB.itemKeys.name] = name
end
end
end
-- data is {<questName>, {<questDescription>,...}, {<questObjective>,...}}
-- Load quest locales
if l10n.questLookup and l10n.questLookup[locale] then
for id, data in pairs(l10n.questLookup[locale]) do
if QuestieDB.questData[id] then
if data[1] then
QuestieDB.questData[id][QuestieDB.questKeys.name] = data[1]
end
-- TODO add details text to questDB.lua (data[2])
if data[3] then
-- needs to be saved as a table for tooltips to have lines
if type(data[3]) == "string" then
QuestieDB.questData[id][QuestieDB.questKeys.objectivesText] = {data[3]}
else
QuestieDB.questData[id][QuestieDB.questKeys.objectivesText] = data[3]
end
end
end
end
end
-- Load NPC locales
if l10n.npcNameLookup and l10n.npcNameLookup[locale] then
for id, data in pairs(l10n.npcNameLookup[locale]) do
if QuestieDB.npcData[id] and data then
if type(data) == "string" then
QuestieDB.npcData[id][QuestieDB.npcKeys.name] = data
else
QuestieDB.npcData[id][QuestieDB.npcKeys.name] = data[1]
QuestieDB.npcData[id][QuestieDB.npcKeys.subName] = data[2]
end
end
end
end
-- Load object locales
if l10n.objectLookup and l10n.objectLookup[locale] then
for id, name in pairs(l10n.objectLookup[locale]) do
if QuestieDB.objectData[id] and name then
QuestieDB.objectData[id][QuestieDB.objectKeys.name] = name
end
end
end
end
--Must be run in a coroutine as it yields
function l10n:PostBoot()
local count = 0
-- Create {['name'] = {ID, },} table for lookup of possible object IDs by name
if not QuestieDB.ObjectPointers then return end
for id in pairs(QuestieDB.ObjectPointers) do
local name = QuestieDB.QueryObjectSingle(id, "name")
if name then -- We (meaning me, BreakBB) introduced Fake IDs for objects to show additional locations, so we need to check this
local entry = l10n.objectNameLookup[name]
if not entry then
l10n.objectNameLookup[name] = { id }
else
entry[#entry+1] = id
end
end
if count > 300 then
count = 0
coroutine.yield()
end
count = count + 1
end
end
local format, unpack, tostring = string.format, unpack, tostring
-- Safe wrapper around string.format that never errors. string.format throws
-- "invalid option 'X' to 'format'" when the format string contains a stray %
-- followed by a non-format character, or when the caller's args don't match
-- the placeholders. l10n callers pass user-controlled data (quest names,
-- npc names, error messages from xpcall) that can contain %X edge cases;
-- crashing the chat with a Lua error for a missing translation is much worse
-- than showing a slightly ugly fallback string. See user report: 8x
-- l10n.lua:181 "invalid option in 'format'" during initial quest log sync.
local function safeFormat(fmt, args)
if not args or #args == 0 then
-- No args, so just return the format string as-is (no format parsing
-- needed). This handles trailing-% edge cases too.
return fmt
end
local ok, result = pcall(format, fmt, unpack(args))
if ok then return result end
-- Fallback: concatenate the format string with the args separated by spaces.
-- This loses the original formatting intent but never crashes the user.
return fmt .. " " .. table.concat(args, " ")
end
function _l10n:translate(key, ...)
if key == nil then
return ""
end
key = tostring(key)
local argCount = select("#", ...)
if argCount == 0 then
local translationEntry = l10n.translations[key]
if not translationEntry then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translations for '" .. tostring(key) .. "' are missing completely!") end
return key
end
local translationValue = translationEntry[locale]
if (not translationValue) then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translations for '" .. tostring(key) .. "' are missing the entry for language" , locale, "!") end
return key
end
if translationValue == true then
-- Fallback to enUS which is the key
return key
end
if type(translationValue) ~= "string" then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translation for '" .. tostring(key) .. "' is not a string!") end
return key
end
return translationValue
end
local args = {...}
for i, v in ipairs(args) do
args[i] = tostring(v);
end
local translationEntry = l10n.translations[key]
if not translationEntry then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translations for '" .. tostring(key) .. "' are missing completely!") end
return safeFormat(key, args)
end
local translationValue = translationEntry[locale]
if (not translationValue) then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translations for '" .. tostring(key) .. "' are missing the entry for language" , locale, "!") end
return safeFormat(key, args)
end
if translationValue == true then
-- Fallback to enUS which is the key
return safeFormat(key, args)
end
if type(translationValue) ~= "string" then
if (Questie.db.profile.debugEnabled) then Questie:Debug(Questie.DEBUG_ELEVATED, "ERROR: Translation for '" .. tostring(key) .. "' is not a string!") end
return safeFormat(key, args)
end
if #args == 0 then
return translationValue
end
return safeFormat(translationValue, args)
end
setmetatable(l10n, { __call = function(_, ...) return _l10n:translate(...) end})
function _l10n:GetFallbackLocale(lang)
if (not lang) then
return 'enUS'
end
if supportedLocals[lang] then
return lang
elseif lang == 'enGB' then
return 'enUS'
elseif lang == 'enCN' then
return 'zhCN'
elseif lang == 'enTW' then
return 'zhTW'
elseif lang == 'esMX' then
return 'esES'
elseif lang == 'ptPT' then
return 'ptBR'
else
return 'enUS'
end
end
function l10n:SetUILocale(lang)
if lang then
locale = _l10n:GetFallbackLocale(lang)
else
locale = _l10n:GetFallbackLocale(GetLocale())
end
end
function l10n:GetUILocale()
return locale
end