diff --git a/MacroToolkit/MacroToolkit.lua b/MacroToolkit/MacroToolkit.lua
index d6ea391..a6da6a4 100644
--- a/MacroToolkit/MacroToolkit.lua
+++ b/MacroToolkit/MacroToolkit.lua
@@ -48,7 +48,7 @@ MT.defaults = {
fonts = {edfont = "Friz Quadrata TT", edsize = 10, errfont = "Friz Quadrata TT", errsize = 10,
mfont = "Friz Quadrata TT", mifont = "Friz Quadrata TT", misize = 10},
},
- global = {custom = {}, extended = {}, extra = {}, allcharmacros = true},
+ global = {custom = {}, extended = {}, extra = {}, allcharmacros = true, spellTranslations = {}},
char = {extended = {}, wodupgrade = false, macros = {}},
}
@@ -105,6 +105,7 @@ function MT:eventHandler(this, event, arg1, ...)
if not C_AddOns.IsAddOnLoaded("Blizzard_MacroUI") then C_AddOns.LoadAddOn("Blizzard_MacroUI") end
if not MT.db.global.custom then MT.db.global.custom = {} end
if not MT.db.global.extra then MT.db.global.extra = {} end
+ if not MT.db.global.spellTranslations then MT.db.global.spellTranslations = {} end
for _, c in ipairs(MT.db.global.custom) do
_G[format("SLASH_MACROTOOLKIT_CUSTOM_%s%d", string.upper(c.n), 1)] = format("%s%s", MT.slash, c.n)
SlashCmdList[format("MACROTOOLKIT_CUSTOM_%s", string.upper(c.n))] = function(input) MT:DoCustomCommand(c.s, input) end
@@ -280,6 +281,7 @@ function MT:eventHandler(this, event, arg1, ...)
MacroToolkit.usingiconlib = true
end
end
+ if MT.ScanSpellTranslations then MT:ScanSpellTranslations() end
end
end
@@ -936,6 +938,7 @@ function MT:MacroFrameUpdate()
if numMacros > 0 then MacroToolkitClear:Enable()
else MacroToolkitClear:Disable() end
+ if MT.UpdateTranslationWorkspace then MT:UpdateTranslationWorkspace() end
end
function MT:ContainerOnLoad(container)
@@ -1097,6 +1100,7 @@ function MT:SaveMacro()
MT:UpdateIcon(_G[n])
else EditMacro(MTF.selectedMacro, nil, nil, MacroToolkitText:GetText()) end
end
+ if MT.CaptureMacroSpellTokens then MT:CaptureMacroSpellTokens(MacroToolkitText:GetText()) end
MTF.textChanged = nil
end
end
diff --git a/MacroToolkit/locales/enUS.lua b/MacroToolkit/locales/enUS.lua
index a7491e0..9b4b696 100644
--- a/MacroToolkit/locales/enUS.lua
+++ b/MacroToolkit/locales/enUS.lua
@@ -146,6 +146,21 @@ L["Spell ID"] = true
L["Search by spell ID"] = true
L["(experimental)"] = true
L["Reset position"] = true
+L["Spell translation"] = true
+L["Translation preview"] = true
+L["Saved translations"] = true
+L["Save spells"] = true
+L["Convert"] = true
+L["Overwrite"] = true
+L["Select a macro to translate"] = true
+L["Convert the selected macro to preview localized spell names"] = true
+L["Saved %d spell translations"] = true
+L["Saved %d macro spells and %d spellbook spells"] = true
+L["Saved %d macro spells, %d spellbook spells, and updated %d spells from saved data"] = true
+L["Translated %d spell references"] = true
+L["No stored spell translations matched this macro"] = true
+L["Convert a macro before overwriting it"] = true
+L["Overwrote the selected macro with translated spell names"] = true
--conditions
L["Condition Builder"] = true
diff --git a/MacroToolkit/modules/mainframe.lua b/MacroToolkit/modules/mainframe.lua
index d4429d1..7320adc 100644
--- a/MacroToolkit/modules/mainframe.lua
+++ b/MacroToolkit/modules/mainframe.lua
@@ -400,10 +400,162 @@ function MT:CreateMTFrame()
mtmfscroll:SetScript("OnSizeChanged", function(_, w, _) mtmfscrollchild:SetWidth(w) end)
end
+ local mttranslationbg = CreateFrame("Frame", "MacroToolkitTranslationBg", UIParent, BackdropTemplateMixin and "BackdropTemplate")
+ do
+ mttranslationbg:SetFrameStrata(mtframe:GetFrameStrata())
+ mttranslationbg:SetToplevel(true)
+ mttranslationbg:SetPoint("TOPLEFT", mtframe, "TOPRIGHT", 5, 0)
+ mttranslationbg:SetPoint("BOTTOMLEFT", mtframe, "BOTTOMRIGHT", 5, 0)
+ mttranslationbg:SetWidth(302)
+ mttranslationbg:SetBackdrop({
+ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
+ edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
+ tile = true,
+ tileSize = 32,
+ edgeSize = 32,
+ insets = {left = 11, right = 12, top = 12, bottom = 11},
+ })
+ mttranslationbg:Hide()
+ end
+
+ local mttranslationtitle = mttranslationbg:CreateFontString("MacroToolkitTranslationLabel", "ARTWORK", "GameFontHighlightSmall")
+ do
+ mttranslationtitle:SetText(L["Spell translation"])
+ mttranslationtitle:SetFontObject("GameFontNormal")
+ mttranslationtitle:SetPoint("TOP", 0, -18)
+ end
+
+ local mttranslationlineleft = mttranslationbg:CreateTexture(nil, "ARTWORK")
+ do
+ mttranslationlineleft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar")
+ mttranslationlineleft:SetSize(96, 12)
+ mttranslationlineleft:SetPoint("TOPLEFT", 18, -34)
+ mttranslationlineleft:SetTexCoord(0, 0.75, 0, 0.25)
+ end
+
+ local mttranslationlineright = mttranslationbg:CreateTexture(nil, "ARTWORK")
+ do
+ mttranslationlineright:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar")
+ mttranslationlineright:SetSize(96, 12)
+ mttranslationlineright:SetPoint("TOPRIGHT", -18, -34)
+ mttranslationlineright:SetTexCoord(0, 0.75, 0, 0.25)
+ end
+
+ local mttranslationdesc = mttranslationbg:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ do
+ mttranslationdesc:SetText(L["Convert the selected macro to preview localized spell names"])
+ mttranslationdesc:SetJustifyH("CENTER")
+ mttranslationdesc:SetPoint("TOPLEFT", 24, -48)
+ mttranslationdesc:SetPoint("TOPRIGHT", -24, -48)
+ mttranslationdesc:SetTextColor(0.75, 0.75, 0.75, 1)
+ end
+
+ local mttranslationsave = CreateFrame("Button", "MacroToolkitTranslateSave", mttranslationbg, "BackdropTemplate,UIPanelButtonTemplate")
+ do
+ mttranslationsave:SetSize(86, 22)
+ mttranslationsave:SetPoint("TOPLEFT", 16, -82)
+ mttranslationsave:SetText(L["Save spells"])
+ mttranslationsave:SetScript("OnClick", function() MT:SaveCurrentSpellTranslations() end)
+ end
+
+ local mttranslateconvert = CreateFrame("Button", "MacroToolkitTranslateConvert", mttranslationbg, "BackdropTemplate,UIPanelButtonTemplate")
+ do
+ mttranslateconvert:SetSize(86, 22)
+ mttranslateconvert:SetPoint("LEFT", mttranslationsave, "RIGHT", 5, 0)
+ mttranslateconvert:SetText(L["Convert"])
+ mttranslateconvert:SetScript("OnClick", function() MT:ConvertSelectedMacroToCurrentLocale() end)
+ end
+
+ local mttranslateoverwrite = CreateFrame("Button", "MacroToolkitTranslateOverwrite", mttranslationbg, "BackdropTemplate,UIPanelButtonTemplate")
+ do
+ mttranslateoverwrite:SetSize(86, 22)
+ mttranslateoverwrite:SetPoint("LEFT", mttranslateconvert, "RIGHT", 5, 0)
+ mttranslateoverwrite:SetText(L["Overwrite"])
+ mttranslateoverwrite:SetScript("OnClick", function() MT:OverwriteSelectedMacroWithTranslation() end)
+ end
+
+ local mttranslationstatus = mttranslationbg:CreateFontString("MacroToolkitTranslationStatus", "ARTWORK", "GameFontNormalSmall")
+ do
+ mttranslationstatus:SetPoint("TOPLEFT", mttranslationsave, "BOTTOMLEFT", 0, -8)
+ mttranslationstatus:SetPoint("TOPRIGHT", -16, -112)
+ mttranslationstatus:SetJustifyH("LEFT")
+ mttranslationstatus:SetJustifyV("TOP")
+ mttranslationstatus:SetHeight(28)
+ mttranslationstatus:SetTextColor(0.75, 0.75, 0.75, 1)
+ end
+
+ local mttranslationpreviewlabel = mttranslationbg:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ do
+ mttranslationpreviewlabel:SetText(L["Translation preview"])
+ mttranslationpreviewlabel:SetPoint("TOPLEFT", 18, -140)
+ end
+
+ local mttranslationinset = CreateFrame("Frame", nil, mttranslationbg, BackdropTemplateMixin and "BackdropTemplate")
+ do
+ mttranslationinset:SetPoint("TOPLEFT", 14, -156)
+ mttranslationinset:SetPoint("TOPRIGHT", -14, -156)
+ mttranslationinset:SetHeight(120)
+ mttranslationinset:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16, tileSize = 16, tile = true, insets = {left = 5, right = 5, top = 5, bottom = 5}})
+ mttranslationinset:SetBackdropBorderColor(_G.TOOLTIP_DEFAULT_COLOR.r, _G.TOOLTIP_DEFAULT_COLOR.g, _G.TOOLTIP_DEFAULT_COLOR.b)
+ mttranslationinset:SetBackdropColor(_G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, _G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, _G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
+ end
+
+ local mttranslationscroll = CreateFrame("ScrollFrame", "MacroToolkitTranslationScrollFrame", mttranslationinset, "BackdropTemplate,UIPanelScrollFrameTemplate")
+ do
+ mttranslationscroll:SetPoint("TOPLEFT", 10, -6)
+ mttranslationscroll:SetPoint("BOTTOMRIGHT", -26, 4)
+ end
+
+ local mttranslationscrollchild = CreateFrame("EditBox", "MacroToolkitTranslationText", mttranslationscroll, "BackdropTemplate")
+ do
+ mttranslationscrollchild:SetMultiLine(true)
+ mttranslationscrollchild:SetAutoFocus(false)
+ mttranslationscrollchild:SetCountInvisibleLetters(true)
+ mttranslationscrollchild:SetAllPoints()
+ mttranslationscrollchild:SetScript("OnEscapePressed", EditBox_ClearFocus)
+ local font = LSM:Fetch(LSM.MediaType.FONT, MT.db.profile.fonts.edfont)
+ mttranslationscrollchild:SetFont(font, MT.db.profile.fonts.errsize, "")
+ mttranslationscroll:SetScrollChild(mttranslationscrollchild)
+ mttranslationscroll:SetScript("OnSizeChanged", function(_, w, _) mttranslationscrollchild:SetWidth(w) end)
+ end
+
+ local mtsavedpreviewlabel = mttranslationbg:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
+ do
+ mtsavedpreviewlabel:SetText(L["Saved translations"])
+ mtsavedpreviewlabel:SetPoint("TOPLEFT", mttranslationinset, "BOTTOMLEFT", 4, -10)
+ end
+
+ local mtsavedpreviewinset = CreateFrame("Frame", nil, mttranslationbg, BackdropTemplateMixin and "BackdropTemplate")
+ do
+ mtsavedpreviewinset:SetPoint("TOPLEFT", mttranslationinset, "BOTTOMLEFT", 0, -26)
+ mtsavedpreviewinset:SetPoint("BOTTOMRIGHT", -14, 14)
+ mtsavedpreviewinset:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16, tileSize = 16, tile = true, insets = {left = 5, right = 5, top = 5, bottom = 5}})
+ mtsavedpreviewinset:SetBackdropBorderColor(_G.TOOLTIP_DEFAULT_COLOR.r, _G.TOOLTIP_DEFAULT_COLOR.g, _G.TOOLTIP_DEFAULT_COLOR.b)
+ mtsavedpreviewinset:SetBackdropColor(_G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, _G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, _G.TOOLTIP_DEFAULT_BACKGROUND_COLOR.b)
+ end
+
+ local mtsavedpreviewscroll = CreateFrame("ScrollFrame", "MacroToolkitSavedTranslationScrollFrame", mtsavedpreviewinset, "BackdropTemplate,UIPanelScrollFrameTemplate")
+ do
+ mtsavedpreviewscroll:SetPoint("TOPLEFT", 10, -6)
+ mtsavedpreviewscroll:SetPoint("BOTTOMRIGHT", -26, 4)
+ end
+
+ local mtsavedpreviewtext = CreateFrame("EditBox", "MacroToolkitSavedTranslationText", mtsavedpreviewscroll, "BackdropTemplate")
+ do
+ mtsavedpreviewtext:SetMultiLine(true)
+ mtsavedpreviewtext:SetAutoFocus(false)
+ mtsavedpreviewtext:SetCountInvisibleLetters(true)
+ mtsavedpreviewtext:SetEnabled(false)
+ mtsavedpreviewtext:SetAllPoints()
+ local font = LSM:Fetch(LSM.MediaType.FONT, MT.db.profile.fonts.errfont)
+ mtsavedpreviewtext:SetFont(font, MT.db.profile.fonts.errsize, "")
+ mtsavedpreviewscroll:SetScrollChild(mtsavedpreviewtext)
+ mtsavedpreviewscroll:SetScript("OnSizeChanged", function(_, w, _) mtsavedpreviewtext:SetWidth(w) end)
+ end
+
local mterrorbg = CreateFrame("Frame", "MacroToolkitErrorBg", mtframe, BackdropTemplateMixin and "BackdropTemplate")
do
mterrorbg:SetPoint("TOP", mttextbg, "TOP", 0, 0)
- --mterrorbg:SetPoint("TOP", 328, -289)
mterrorbg:SetPoint("BOTTOMRIGHT", -8, 40)
mterrorbg:SetWidth(302)
mterrorbg:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16, tileSize = 16, tile = true, insets = {left = 5, right = 5, top = 5, bottom = 5}})
@@ -720,6 +872,9 @@ function MT:CreateMTFrame()
mtframe:ClearAllPoints()
mtframe:SetPoint("BOTTOMLEFT", MT.db.profile.x, MT.db.profile.y)
mtframe:Raise()
+ mttranslationbg:Show()
+ mttranslationbg:SetFrameStrata(mtframe:GetFrameStrata())
+ mttranslationbg:SetFrameLevel(mtframe:GetFrameLevel())
MT:MacroFrameUpdate()
--PlaySound("igCharacterInfoOpen")
PlaySound(839)
@@ -729,10 +884,12 @@ function MT:CreateMTFrame()
else mtcustomflyout:Enable() end
if MacroBox and MT.db.profile.vismacrobox then mtmb:Show() else mtmb:Hide() end
dwfunc()
+ MT:UpdateTranslationWorkspace()
end)
mtframe:SetScript("OnHide", function()
if MT.MTPF then MT.MTPF:Hide() end
+ mttranslationbg:Hide()
MT:SaveMacro()
--PlaySound("igCharacterInfoClose")
PlaySound(840)
diff --git a/MacroToolkit/modules/modules.xml b/MacroToolkit/modules/modules.xml
index 38bd420..ef5fa37 100644
--- a/MacroToolkit/modules/modules.xml
+++ b/MacroToolkit/modules/modules.xml
@@ -3,6 +3,7 @@ https://raw.githubusercontent.com/Gethe/wow-ui-source/refs/heads/live/Interface/
+
diff --git a/MacroToolkit/modules/options.lua b/MacroToolkit/modules/options.lua
index 5073b48..ddba652 100644
--- a/MacroToolkit/modules/options.lua
+++ b/MacroToolkit/modules/options.lua
@@ -37,6 +37,7 @@ function MT:SetScale(value)
MacroToolkitFrame:SetSize(MT.db.profile.width or 638, MT.db.profile.height)
MT.db.profile.scale = value
MacroToolkitFrame:SetScale(value)
+ if MacroToolkitTranslationBg then MacroToolkitTranslationBg:SetScale(value) end
if MacroToolkitRestoreFrame then MacroToolkitRestoreFrame:SetScale(value) end
if MacroTtoolkitPopup then MacroToolkitPopup:SetScale(value) end
if MacroToolkitScriptFrame then MacroToolkitScriptFrame:SetScale(value) end
diff --git a/MacroToolkit/modules/translation.lua b/MacroToolkit/modules/translation.lua
new file mode 100644
index 0000000..19b356c
--- /dev/null
+++ b/MacroToolkit/modules/translation.lua
@@ -0,0 +1,511 @@
+local _G = _G
+--- @class MacroToolkit
+local MT = MacroToolkit
+local L = MT.L
+local format, gsub, lower, match = string.format, string.gsub, string.lower, string.match
+local ipairs, pairs, tonumber, type = ipairs, pairs, tonumber, type
+local GetLocale = GetLocale
+local GetMacroInfo, GetNumMacros = GetMacroInfo, GetNumMacros
+local GetNumSpellTabs = GetNumSpellTabs or (C_SpellBook and C_SpellBook.GetNumSpellBookSkillLines)
+
+local function trim(value)
+ return match(value or "", "^%s*(.-)%s*$") or ""
+end
+
+local function getSpellInfoCompat(spellIDOrName)
+ local numericSpellID = tonumber(spellIDOrName, 10)
+ if C_Spell and C_Spell.GetSpellInfo then
+ local spellInfo = C_Spell.GetSpellInfo(numericSpellID or spellIDOrName)
+ if spellInfo then return spellInfo.name, spellInfo.spellID end
+ end
+ if _G.GetSpellInfo then
+ local name, _, _, _, _, _, spellID = _G.GetSpellInfo(numericSpellID or spellIDOrName)
+ if name then return name, spellID or numericSpellID end
+ end
+end
+
+local function getSpellBookItemData(slot)
+ if C_SpellBook and C_SpellBook.GetSpellBookItemInfo and Enum and Enum.SpellBookSpellBank then
+ local info = C_SpellBook.GetSpellBookItemInfo(slot, Enum.SpellBookSpellBank.Player)
+ if info then return info.itemType, info.spellID end
+ return
+ end
+ if _G.GetSpellBookItemInfo then
+ local spellType, spellID = _G.GetSpellBookItemInfo(slot, "spell")
+ return spellType, spellID
+ end
+end
+
+local function buildAliasSet(prefix)
+ local aliases = {}
+ for i = 1, 99 do
+ local alias = _G[format("%s%d", prefix, i)]
+ if not alias then break end
+ aliases[lower(string.sub(alias, 2))] = true
+ end
+ return aliases
+end
+
+local CAST_ALIASES = buildAliasSet("SLASH_CAST")
+local USE_ALIASES = buildAliasSet("SLASH_USE")
+local CAST_SEQUENCE_ALIASES = buildAliasSet("SLASH_CASTSEQUENCE")
+local CAST_RANDOM_ALIASES = buildAliasSet("SLASH_CASTRANDOM")
+local USE_RANDOM_ALIASES = buildAliasSet("SLASH_USERANDOM")
+local SHOW_ALIASES = {show = true}
+local SHOW_TOOLTIP_ALIASES = {showtooltip = true}
+
+for alias in pairs(USE_ALIASES) do
+ CAST_ALIASES[alias] = true
+end
+for alias in pairs(USE_RANDOM_ALIASES) do
+ CAST_RANDOM_ALIASES[alias] = true
+end
+
+local function getTranslationCommandType(command)
+ command = lower(command or "")
+ if CAST_SEQUENCE_ALIASES[command] then return "sequence" end
+ if CAST_RANDOM_ALIASES[command] then return "random" end
+ if CAST_ALIASES[command] then return "single" end
+ if SHOW_ALIASES[command] or SHOW_TOOLTIP_ALIASES[command] then return "single" end
+end
+
+local function transformDelimited(text, delimiter, transformer)
+ local output, index, changed = {}, 1, false
+ while true do
+ local delimiterStart = string.find(text, delimiter, index, true)
+ local segment = delimiterStart and string.sub(text, index, delimiterStart - 1) or string.sub(text, index)
+ local newSegment, segmentChanged = transformer(segment)
+ output[#output + 1] = newSegment
+ changed = changed or segmentChanged
+ if not delimiterStart then break end
+ output[#output + 1] = delimiter
+ index = delimiterStart + #delimiter
+ end
+ return table.concat(output), changed
+end
+
+local function splitConditionPrefix(clause)
+ local leadingWhitespace = match(clause, "^%s*") or ""
+ local prefix = leadingWhitespace
+ local remainder = string.sub(clause, #leadingWhitespace + 1)
+ while string.sub(remainder, 1, 1) == "[" do
+ local block = match(remainder, "^(%b[])")
+ if not block then break end
+ prefix = prefix .. block
+ remainder = string.sub(remainder, #block + 1)
+ local spaces = match(remainder, "^%s*") or ""
+ prefix = prefix .. spaces
+ remainder = string.sub(remainder, #spaces + 1)
+ end
+ return prefix, remainder
+end
+
+local function parseMacroLine(line)
+ local leadingWhitespace = match(line, "^%s*") or ""
+ local trimmedLine = string.sub(line, #leadingWhitespace + 1)
+ local prefix = string.sub(trimmedLine, 1, 1)
+ if prefix == "#" then
+ local command, parameters = match(trimmedLine, "^(#%S+)%s*(.*)$")
+ if command then return leadingWhitespace, command, parameters or "", getTranslationCommandType(string.sub(command, 2)) end
+ elseif prefix == MT.slash then
+ local command, parameters = match(trimmedLine, "^" .. MT.slash .. "(%S+)%s*(.*)$")
+ if command then return leadingWhitespace, MT.slash .. command, parameters or "", getTranslationCommandType(command) end
+ end
+end
+
+function MT:EnsureSpellTranslationStorage()
+ if not self.db or not self.db.global then return end
+ self.db.global.spellTranslations = self.db.global.spellTranslations or {}
+end
+
+function MT:SaveSpellTranslation(spellID, spellName, locale)
+ spellID = tonumber(spellID)
+ spellName = trim(spellName)
+ locale = locale or GetLocale()
+ if not spellID or spellName == "" then return false end
+ self:EnsureSpellTranslationStorage()
+ local key = tostring(spellID)
+ local data = self.db.global.spellTranslations[key]
+ if not data then
+ data = {}
+ self.db.global.spellTranslations[key] = data
+ end
+ if data[locale] == spellName then return false end
+ data[locale] = spellName
+ return true
+end
+
+function MT:GetSpellTranslationReverseIndex()
+ self:EnsureSpellTranslationStorage()
+ local reverseIndex = {}
+ for spellID, locales in pairs(self.db.global.spellTranslations) do
+ for locale, spellName in pairs(locales) do
+ local key = lower(spellName)
+ if key ~= "" and not reverseIndex[key] then
+ reverseIndex[key] = {spellID = tonumber(spellID), locale = locale, name = spellName}
+ end
+ end
+ end
+ return reverseIndex
+end
+
+function MT:GetTranslatedSpellName(spellID, locale)
+ spellID = tonumber(spellID)
+ locale = locale or GetLocale()
+ if not spellID then return end
+ self:EnsureSpellTranslationStorage()
+ local data = self.db.global.spellTranslations[tostring(spellID)]
+ if data and data[locale] then return data[locale] end
+ if locale == GetLocale() then
+ local currentName = getSpellInfoCompat(spellID)
+ if currentName then
+ self:SaveSpellTranslation(spellID, currentName, locale)
+ return currentName
+ end
+ end
+end
+
+function MT:CountSavedSpellTranslations()
+ self:EnsureSpellTranslationStorage()
+ local count = 0
+ for spellID, locales in pairs(self.db.global.spellTranslations) do
+ if tonumber(spellID) and type(locales) == "table" then
+ count = count + 1
+ end
+ end
+ return count
+end
+
+function MT:ResolveSpellToken(token, reverseIndex)
+ local tokenName = trim(token)
+ if tokenName == "" then return end
+ local bangPrefix = ""
+ if string.sub(tokenName, 1, 1) == "!" then
+ bangPrefix = "!"
+ tokenName = trim(string.sub(tokenName, 2))
+ end
+ if tokenName == "" then return end
+
+ local currentName, spellID = getSpellInfoCompat(tokenName)
+ if spellID then
+ self:SaveSpellTranslation(spellID, currentName, GetLocale())
+ return spellID, currentName, GetLocale(), bangPrefix
+ end
+
+ if reverseIndex then
+ local entry = reverseIndex[lower(tokenName)]
+ if entry then return entry.spellID, entry.name, entry.locale, bangPrefix end
+ end
+end
+
+function MT:TransformSpellParameters(parameters, commandType, transformer)
+ return transformDelimited(parameters or "", ";", function(clause)
+ local prefix, action = splitConditionPrefix(clause)
+ if action == "" then return clause, false end
+ if commandType == "sequence" or commandType == "random" then
+ local resetPrefix = ""
+ local remainder = action
+ if commandType == "sequence" then
+ local foundResetPrefix, resetRemainder = match(action, "^(%s*reset%s*=%s*[^%s]+%s+)(.*)$")
+ if foundResetPrefix then
+ resetPrefix = foundResetPrefix
+ remainder = resetRemainder
+ end
+ end
+ local translatedRemainder, changed = transformDelimited(remainder, ",", transformer)
+ return prefix .. resetPrefix .. translatedRemainder, changed
+ end
+ local translatedAction, changed = transformer(action)
+ return prefix .. translatedAction, changed
+ end)
+end
+
+function MT:CaptureMacroSpellTokens(macroBody, reverseIndex)
+ if not macroBody or macroBody == "" then return 0 end
+ reverseIndex = reverseIndex or self:GetSpellTranslationReverseIndex()
+ local saved = 0
+ for line in string.gmatch(macroBody .. "\n", "(.-)\n") do
+ local _, _, parameters, commandType = parseMacroLine(line)
+ if commandType then
+ self:TransformSpellParameters(parameters, commandType, function(token)
+ local spellID, spellName, locale, bangPrefix = self:ResolveSpellToken(token, reverseIndex)
+ if not spellID then return token, false end
+ local leadingWhitespace = match(token, "^%s*") or ""
+ local trailingWhitespace = match(token, "%s*$") or ""
+ local tokenName = trim(token)
+ if bangPrefix == "!" then tokenName = trim(string.sub(tokenName, 2)) end
+ if locale then saved = saved + (self:SaveSpellTranslation(spellID, tokenName, locale) and 1 or 0) end
+ local currentName = self:GetTranslatedSpellName(spellID, GetLocale()) or spellName
+ if currentName then saved = saved + (self:SaveSpellTranslation(spellID, currentName, GetLocale()) and 1 or 0) end
+ return leadingWhitespace .. bangPrefix .. tokenName .. trailingWhitespace, false
+ end)
+ end
+ end
+ return saved
+end
+
+function MT:CollectMacroSpellIDs(macroBody, reverseIndex, spellIDs)
+ if not macroBody or macroBody == "" then return spellIDs or {} end
+ reverseIndex = reverseIndex or self:GetSpellTranslationReverseIndex()
+ spellIDs = spellIDs or {}
+ for line in string.gmatch(macroBody .. "\n", "(.-)\n") do
+ local _, _, parameters, commandType = parseMacroLine(line)
+ if commandType then
+ self:TransformSpellParameters(parameters, commandType, function(token)
+ local spellID = self:ResolveSpellToken(token, reverseIndex)
+ if spellID then spellIDs[spellID] = true end
+ return token, false
+ end)
+ end
+ end
+ return spellIDs
+end
+
+local function countKeys(source)
+ local count = 0
+ for _ in pairs(source or {}) do
+ count = count + 1
+ end
+ return count
+end
+
+function MT:ScanSpellbookTranslations()
+ local saved = 0
+ local spellbookSpellIDs = {}
+
+ if C_SpellBook and C_SpellBook.GetSpellBookItemInfo and Enum and Enum.SpellBookSpellBank then
+ local slot = 1
+ while true do
+ local itemType, itemID = getSpellBookItemData(slot)
+ if not itemType and not itemID then break end
+ if itemType == Enum.SpellBookItemType.Spell then
+ local spellID = itemID
+ local spellName = spellID and getSpellInfoCompat(spellID)
+ if spellID and spellName then
+ spellbookSpellIDs[spellID] = true
+ saved = saved + (self:SaveSpellTranslation(spellID, spellName, GetLocale()) and 1 or 0)
+ end
+ end
+ slot = slot + 1
+ end
+ return saved, countKeys(spellbookSpellIDs)
+ end
+
+ if not GetNumSpellTabs then return 0, 0 end
+ for tabIndex = 1, GetNumSpellTabs() do
+ local name, _, offset, numSpells = _G.GetSpellTabInfo and _G.GetSpellTabInfo(tabIndex)
+ if name then
+ offset = (offset or 0) + 1
+ for slot = offset, offset + (numSpells or 0) - 1 do
+ local itemType, itemID = getSpellBookItemData(slot)
+ if itemType == "SPELL" or itemType == "PETACTION" then
+ local spellID = itemID
+ local spellName = spellID and getSpellInfoCompat(spellID)
+ if spellID and spellName then
+ spellbookSpellIDs[spellID] = true
+ saved = saved + (self:SaveSpellTranslation(spellID, spellName, GetLocale()) and 1 or 0)
+ end
+ end
+ end
+ end
+ end
+ return saved, countKeys(spellbookSpellIDs)
+end
+
+function MT:UpdateSavedTranslationsForCurrentLocale()
+ self:EnsureSpellTranslationStorage()
+ local updated = 0
+ local locale = GetLocale()
+ for spellID, locales in pairs(self.db.global.spellTranslations) do
+ local numericSpellID = tonumber(spellID)
+ if numericSpellID and type(locales) == "table" and not locales[locale] then
+ local spellName = getSpellInfoCompat(numericSpellID)
+ if spellName and self:SaveSpellTranslation(numericSpellID, spellName, locale) then
+ updated = updated + 1
+ end
+ end
+ end
+ return updated
+end
+
+function MT:ScanSpellTranslations()
+ if not self.db or not self.db.global then return {saved = 0, macroCount = 0, spellbookCount = 0, savedDataCount = 0} end
+ self:EnsureSpellTranslationStorage()
+ local saved, spellbookCount = self:ScanSpellbookTranslations()
+ local reverseIndex = self:GetSpellTranslationReverseIndex()
+ local seenBodies = {}
+ local macroSpellIDs = {}
+
+ local function scanBody(body)
+ if body and body ~= "" and not seenBodies[body] then
+ seenBodies[body] = true
+ self:CollectMacroSpellIDs(body, reverseIndex, macroSpellIDs)
+ saved = saved + self:CaptureMacroSpellTokens(body, reverseIndex)
+ end
+ end
+
+ local numAccountMacros, numCharacterMacros = GetNumMacros()
+ for macroIndex = 1, numAccountMacros + numCharacterMacros do
+ local _, _, body = GetMacroInfo(macroIndex)
+ scanBody(body)
+ end
+
+ for _, data in pairs(self.db.global.extended or {}) do
+ scanBody(data.body)
+ end
+ for _, data in pairs(self.db.char.extended or {}) do
+ scanBody(data.body)
+ end
+ for _, data in pairs(self.db.global.extra or {}) do
+ scanBody(data.body)
+ end
+
+ local savedDataCount = self:UpdateSavedTranslationsForCurrentLocale()
+
+ return {
+ saved = saved,
+ macroCount = countKeys(macroSpellIDs),
+ spellbookCount = spellbookCount,
+ savedDataCount = savedDataCount,
+ }
+end
+
+function MT:TranslateMacro(macroBody, targetLocale)
+ targetLocale = targetLocale or GetLocale()
+ local reverseIndex = self:GetSpellTranslationReverseIndex()
+ local changedTokens = 0
+ local translatedLines = {}
+
+ for line in string.gmatch((macroBody or "") .. "\n", "(.-)\n") do
+ local leadingWhitespace, commandToken, parameters, commandType = parseMacroLine(line)
+ if commandType then
+ local translatedParameters, lineChanged = self:TransformSpellParameters(parameters, commandType, function(token)
+ local spellID, _, _, bangPrefix = self:ResolveSpellToken(token, reverseIndex)
+ if not spellID then return token, false end
+ local targetName = self:GetTranslatedSpellName(spellID, targetLocale)
+ if not targetName then return token, false end
+ local leading = match(token, "^%s*") or ""
+ local trailing = match(token, "%s*$") or ""
+ local rawToken = trim(token)
+ if bangPrefix == "!" then rawToken = trim(string.sub(rawToken, 2)) end
+ if rawToken == targetName then return token, false end
+ changedTokens = changedTokens + 1
+ return leading .. bangPrefix .. targetName .. trailing, true
+ end)
+ local spacer = translatedParameters ~= "" and " " or ""
+ translatedLines[#translatedLines + 1] = leadingWhitespace .. commandToken .. spacer .. translatedParameters
+ if not lineChanged and parameters == "" then
+ translatedLines[#translatedLines] = line
+ end
+ else
+ translatedLines[#translatedLines + 1] = line
+ end
+ end
+
+ return table.concat(translatedLines, "\n"), changedTokens
+end
+
+function MT:SetTranslationStatus(message, r, g, b)
+ if not MacroToolkitTranslationStatus then return end
+ MacroToolkitTranslationStatus:SetText(message or "")
+ MacroToolkitTranslationStatus:SetTextColor(r or 0.75, g or 0.75, b or 0.75)
+end
+
+function MT:BuildSavedTranslationPreview()
+ if not self.db or not self.db.global or not self.db.global.spellTranslations then return "" end
+ local rows = {}
+ for spellID, locales in pairs(self.db.global.spellTranslations) do
+ if tonumber(spellID) and type(locales) == "table" then
+ local localeNames = {}
+ for locale, spellName in pairs(locales) do
+ if type(spellName) == "string" and spellName ~= "" then
+ localeNames[#localeNames + 1] = format("%s=%s", locale, spellName)
+ end
+ end
+ table.sort(localeNames)
+ if #localeNames > 0 then
+ rows[#rows + 1] = { id = tonumber(spellID), text = format("%s: %s", spellID, table.concat(localeNames, ", ")) }
+ end
+ end
+ end
+ table.sort(rows, function(a, b) return a.id < b.id end)
+ for i, row in ipairs(rows) do
+ rows[i] = row.text
+ end
+ return table.concat(rows, "\n")
+end
+
+function MT:UpdateSavedTranslationPreview()
+ if not MacroToolkitSavedTranslationText then return end
+ MacroToolkitSavedTranslationText:SetText(self:BuildSavedTranslationPreview())
+end
+
+function MT:UpdateTranslationWorkspace()
+ if not MacroToolkitTranslationText then return end
+ self:UpdateSavedTranslationPreview()
+ local selectedMacro = MacroToolkitFrame and MacroToolkitFrame.selectedMacro
+ local currentBody = MacroToolkitText and MacroToolkitText:GetText() or ""
+
+ if self.translationPreviewFor ~= selectedMacro or self.translationPreviewSource ~= currentBody then
+ MacroToolkitTranslationText:SetText("")
+ self.translationPreviewFor = selectedMacro
+ self.translationPreviewSource = currentBody
+ end
+
+ if not selectedMacro then
+ MacroToolkitTranslateConvert:Disable()
+ MacroToolkitTranslateOverwrite:Disable()
+ MacroToolkitTranslateSave:Enable()
+ self:SetTranslationStatus(L["Select a macro to translate"])
+ return
+ end
+
+ MacroToolkitTranslateConvert:Enable()
+ MacroToolkitTranslateSave:Enable()
+ if trim(MacroToolkitTranslationText:GetText()) == "" then
+ MacroToolkitTranslateOverwrite:Disable()
+ self:SetTranslationStatus(L["Convert the selected macro to preview localized spell names"])
+ else
+ MacroToolkitTranslateOverwrite:Enable()
+ end
+end
+
+function MT:SaveCurrentSpellTranslations()
+ local result = self:ScanSpellTranslations()
+ self:SetTranslationStatus(format(L["Saved %d macro spells, %d spellbook spells, and updated %d spells from saved data"], result.macroCount or 0, result.spellbookCount or 0, result.savedDataCount or 0), 0.45, 0.85, 0.45)
+ self:UpdateSavedTranslationPreview()
+end
+
+function MT:ConvertSelectedMacroToCurrentLocale()
+ if not MacroToolkitFrame or not MacroToolkitFrame.selectedMacro then return end
+ self:ScanSpellTranslations()
+ local body = MacroToolkitText:GetText() or ""
+ local translatedBody, changedTokens = self:TranslateMacro(body, GetLocale())
+ MacroToolkitTranslationText:SetText(translatedBody)
+ self.translationPreviewFor = MacroToolkitFrame.selectedMacro
+ self.translationPreviewSource = body
+ if changedTokens > 0 then
+ self:SetTranslationStatus(format(L["Translated %d spell references"], changedTokens), 0.45, 0.85, 0.45)
+ else
+ self:SetTranslationStatus(L["No stored spell translations matched this macro"])
+ end
+ self:UpdateTranslationWorkspace()
+end
+
+function MT:OverwriteSelectedMacroWithTranslation()
+ if not MacroToolkitFrame or not MacroToolkitFrame.selectedMacro then return end
+ local translatedBody = MacroToolkitTranslationText:GetText() or ""
+ if trim(translatedBody) == "" then
+ self:SetTranslationStatus(L["Convert a macro before overwriting it"], 0.85, 0.45, 0.45)
+ return
+ end
+ MacroToolkitText:SetText(translatedBody)
+ MacroToolkitFrame.textChanged = true
+ self:SaveMacro()
+ self:CaptureMacroSpellTokens(translatedBody)
+ self.translationPreviewFor = MacroToolkitFrame.selectedMacro
+ self.translationPreviewSource = translatedBody
+ self:SetTranslationStatus(L["Overwrote the selected macro with translated spell names"], 0.45, 0.85, 0.45)
+ self:UpdateSavedTranslationPreview()
+ self:MacroFrameUpdate()
+end
diff --git a/MacroToolkit/spell-translation-guide.md b/MacroToolkit/spell-translation-guide.md
new file mode 100644
index 0000000..ab4be18
--- /dev/null
+++ b/MacroToolkit/spell-translation-guide.md
@@ -0,0 +1,66 @@
+# Macro Toolkit Spell Translation
+
+This feature helps you work with spell names that change between WoW client locales.
+
+## What It Does
+
+- Scans your current character spellbook
+- Scans your saved macros for spell names
+- Stores spell names by spell ID in `spellTranslations`
+- Converts the selected macro into the current client locale (i.e. "Shadow Bolt" in enUS becomes "暗影箭" in zhTW)
+- Lets you preview the converted result before overwriting the macro
+- Shows the saved translation data in the side panel
+
+## Where To Find It
+
+Open Macro Toolkit via `/mac`, Spell Translation panel appears as a separate window next to the main Macro Toolkit frame.
+
+## Buttons
+
+### Save spells
+
+Use this first in your original locale that your macros were created in.
+
+It scans:
+
+- macros saved
+- your current character spellbook
+- existing `spellTranslations` data's and updates it with any missing spell names it finds in current locale. (i.e. you saved zhTW warlock, and login as enUS priest, it will add any missing spell names for warlock in enUS)
+
+Then it saves spell names into the translation database.
+
+### Convert
+
+Use this after selecting a macro in Macro Toolkit.
+
+It reads the selected macro body, looks for supported spell references, and writes the localized result into the top preview box in the Spell Translation panel. This let's you preview the converted macro text to ensure it looks correct before overwriting the macro.
+
+### Overwrite
+
+This takes the converted text from the top preview box and replaces the selected macro body with it.
+
+Use this only after checking the preview.
+
+### Saved translations
+
+This is the lower text area.
+
+It shows the currently saved `spellTranslations` data only.
+
+Each line is shown as:
+
+```text
+spellID: locale=name, locale=name
+```
+
+Example:
+
+```text
+686: enUS=Shadow Bolt, zhTW=暗影箭
+```
+
+## Notes
+
+- The scan only knows what exists in your saved macro data and the current character spellbook.
+- If a spell is missing from saved translations, convert may leave that spell name unchanged.
+- The feature is intended for spell-name translation. It does not rewrite unrelated macro text.