Files
OEVG 896f46f0d1
enUS Localization Update Announcement / localization-update-announcement (push) Canceled after 0s
Bagshui 1.6.4
2026-10-30 15:55:43 +00:00

243 lines
9.8 KiB
Lua

-- Bagshui Localization
-- Exposes:
-- - BSLocalization (and Bagshui.components.Localization)
-- - L[] and L_nil[]
-- Raises: BAGSHUI_LOCALIZATION_LOADED
--
-- Localization is performed in the Bagshui environment as via the L table, and
-- can be referenced as L.StringKey or L["StringKey"].
-- There is also the L_nil variant, which returns nil on a localization failure
-- instead of returning the original string.
-- Loading the Localization class happens in 2 phases:
-- 1. The LoadComponent() portion immediately injects the localization class into
-- the Bagshui namespace so that AddLocale() becomes available.
-- 2. Once everything is up and running, AddComponent() is processed and the localization is loaded.
-- Need to declare the class here so it's available to both LoadComponent() and AddComponent().
local Localization = {
-- Proxy tables that handle localization calls -- see localizationMetatable below for details.
localize = {},
localizeNilOnMiss = {}, -- Used to trigger a nil return on a localization miss instead of returning the original string.
-- Track the name of the active locale.
activeLocaleId = nil,
-- Localization tables.
activeLocale = {},
fallbackLocale = {},
-- List of available locales in the form of `{ <locale> = { <localization table> } }`.
locales = {},
-- Used by `Localization:AutoLocalize()` to keep track of what has already been automatically localized
-- so we don't double-localize (see that function for further explanation).
autoLocalized = {},
}
Bagshui.environment.BsLocalization = Localization
Bagshui.components.Localization = Localization
Bagshui.environment.L = Localization.localize
Bagshui.environment.L_nil = Localization.localizeNilOnMiss
-- Phase 1: Set up the Localization class.
Bagshui:LoadComponent(function()
-- Metatable that handles localization magic.
--
-- The `localize`/`localizeNilOnMiss` tables are always empty, so any access will trigger
-- the `__index()` method. We can then intercept it and provide the correct localization.
--
-- Similarly, by providing `__newindex()`, we can redirect any changes into the correct
-- activeLocale table.
local localizationMetatable = {
__index = function(localeTable, str)
-- Localization debugging should be enabled when Bagshui debugging is on,
-- but will short-circuit and not evaluate otherwise.
if
BS_DEBUG
and (
localeTable ~= Localization.localizeNilOnMiss
and not (Localization.activeLocale[tostring(str)] or Localization.activeLocale[string.lower(tostring(str))])
)
then
Bagshui:PrintDebug("!MISS!: " .. tostring(str), "Localization", 1, 0, 0)
end
return
-- Active locale match.
(Localization.activeLocale[tostring(str)] or Localization.activeLocale[string.lower(tostring(str))])
-- Default locale match.
or (Localization.fallbackLocale[tostring(str)] or Localization.fallbackLocale[string.lower(tostring(str))])
-- No match -- return nil if requested or the original string.
or (localeTable ~= Localization.localizeNilOnMiss and str or nil)
end,
-- Set a value in the active localization.
__newindex = function(_, str, localized)
if not Localization.activeLocaleId then
return
end
Localization.activeLocale[str] = localized
end,
}
-- Assign metatable to both of the proxy tables.
setmetatable(Localization.localize, localizationMetatable)
setmetatable(Localization.localizeNilOnMiss, localizationMetatable)
--- Set the locale and do other localization preparations.
function Localization:Init()
-- Choose the correct locale -- try the current locale first, then fall back to the default if needed.
local locale = _G.GetLocale()
if locale and self.locales[locale] then
Localization.activeLocaleId = locale
else
Bagshui:PrintWarning(
"No localization available for "
.. locale
.. "; falling back to "
.. BS_DEFAULT_LOCALE
.. ". "
.. "Many items will not be properly categorized!"
)
Localization.activeLocaleId = BS_DEFAULT_LOCALE
end
self.activeLocale = self.locales[Localization.activeLocaleId]
-- Add a the default localization as a fallback.
if Localization.activeLocaleId ~= BS_DEFAULT_LOCALE then
self.fallbackLocale = self.locales[BS_DEFAULT_LOCALE]
end
-- Localize key bindings in Blizzard UI.
for i = 1, _G.GetNumBindings() do
local localized
local bindingName = (_G.GetBinding(i))
-- First check to see if there is a localizable prefix.
local found, _, prefix, toLocalize = string.find((_G.GetBinding(i)), "Bagshui_(.-)_(.+)")
if found and L_nil["Prefix_" .. prefix] then
localized = string.format(L["Prefix_" .. prefix], (L_nil["Binding_" .. toLocalize] or L[toLocalize]))
else
-- No prefix found, so we need to localize the whole thing.
found, _, toLocalize = string.find((_G.GetBinding(i)), "Bagshui_(.+)")
if found then
-- Try to localize with a Binding_ prefix to allow for binding-only naming (Binding_Resort, Binding_Restack)
-- and fall back to just the name (Bags, Bank, etc.).
localized = (L_nil["Binding_" .. toLocalize] or L[toLocalize])
end
end
if localized then
_G["BINDING_NAME_" .. bindingName] = localized
elseif found then
Bagshui:PrintWarning("Failed to localize key binding " .. bindingName)
end
end
-- Automatic localizations (as much as we can).
-- Item slots need to be handled before item classes and subclasses, because Bag ITEMS
-- and bag SLOTS are different in some languages, and Bagshui expects L["Bag"] to refer to ITEMS.
-- (For example, in German, Bag ITEMS are Behälter, but bag SLOTS are Tasche.)
self:AutoLocalize(BsGameInfo.inventorySlots, locale)
self:AutoLocalize(BsGameInfo.itemClasses, locale)
for _, subClassList in pairs(BsGameInfo.itemSubclasses) do
self:AutoLocalize(subClassList, locale)
end
-- System chat messages.
-- Needed for CHAT_MSG_SYSTEM events in `Inventory:OnEvent()`.
self.activeLocale.ChatMsgIdentifier_LearnedRecipe = self.activeLocale.ChatMsgIdentifier_LearnedRecipe
or string.format(_G.ERR_LEARN_RECIPE_S, ".+")
-- Add lowercase versions.
-- A temp table is required here because pairs() gets confused if the table
-- being iterated is modified during iteration.
local temp = BsUtil.TableCopy(self.activeLocale)
for enUS, localized in pairs(temp) do
local lowercase = string.lower(enUS)
if not self.activeLocale[lowercase] then
self.activeLocale[lowercase] = localized
end
end
-- Populate a few useful combinations.
L.UnnamedGroup = string.format(L.Prefix_Unnamed, L.Group)
-- Initialization complete.
Bagshui:RaiseEvent("BAGSHUI_LOCALIZATION_LOADED")
end
--- Add a new localization table.
---@param locale string [Locale identifier](https://warcraft.wiki.gg/index.php?title=API_GetLocale&oldid=4228097)
---@param strings table<string,string> List of `{ ["BagshuiLocalizationString"] = "localized string" }`.
function Localization:AddLocale(locale, strings)
if self.locales[locale] then
-- When a primary localization has already been loaded, additional strings
-- can still be added by calling a second time. This won't override any
-- of the existing strings though.
for localizationString, localized in pairs(strings) do
if not self.locales[locale][localizationString] then
self.locales[locale][localizationString] = localized
end
end
else
self.locales[locale] = strings
end
-- Replace any !!placeholders!! with their actual localized values.
for localizationString, localized in pairs(self.locales[locale]) do
self.locales[locale][localizationString] = self:ReplacePlaceholders(localized, locale)
end
end
--- Build automatic localization from a provided table.
---@param enUSToLocalized table<string,string> English to localized strings.
---@param clientLocale string Current client locale.
function Localization:AutoLocalize(enUSToLocalized, clientLocale)
for english, localized in pairs(enUSToLocalized) do
if
localized
and (
-- Avoid overriding human-provided translations.
not self.activeLocale[english]
-- Go ahead and override if the default locale (enUS) is only being used
-- because there isn't a Bagshui localization for the client locale.
or clientLocale ~= BS_DEFAULT_LOCALE
)
then
self.activeLocale[english] = localized
end
end
end
--- Replace any !!placeholders!! with their actual localized values.
--- Recurses as needed.
---@param str string String to search for !!placeholders!!.
---@param locale string Locale in which placeholder keys can be found.
---@return string
function Localization:ReplacePlaceholders(str, locale)
return (
string.gsub(str, "(!!(.-)!!)", function(match, placeholderToReplace)
if self.locales[locale][placeholderToReplace] then
return Localization:ReplacePlaceholders(self.locales[locale][placeholderToReplace], locale)
else
Bagshui:PrintDebug(
"Localization placeholder " .. tostring(match) .. " not found! (from " .. tostring(str) .. ")"
)
-- Logging system isn't fully initialized yet, so this is being disabled for now.
--Bagshui:PrintWarning("Localization placeholder " .. tostring(match) .. " not found!")
return match
end
end)
)
end
end) -- LoadComponent (Phase 1) complete.
-- Phase 2: Load localization and make localization functions available.
Bagshui:AddComponent(function()
Localization:Init()
end)