Module:Lang: Difference between revisions

Jump to navigation Jump to search
No edit summary
No edit summary
Line 4: Line 4:
local dictionary_module = require("Module:Dictionary")
local dictionary_module = require("Module:Dictionary")


-- Overrides applied to searches by key. Designed to handle edge cases where
-- Caches to prevent repetitive file loading and table generation
-- the expected key does not have a localization entry
local lang_file_cache = {}
local en_reverse_cache = nil
 
-- Overrides applied to searches by key.
local KEY_OVERRIDES = {
local KEY_OVERRIDES = {
BurnDurationBase_label = 'AfterburnDurationBase_label',
BurnDurationBase_label = 'AfterburnDurationBase_label',
Line 39: Line 42:
SummonCount_label = 'GhoulCount_label',
SummonCount_label = 'GhoulCount_label',
TetherDuration_label = 'ImmobilizeTrap_CurseDuration_label'
TetherDuration_label = 'ImmobilizeTrap_CurseDuration_label'
}
-- Moved static configuration mappings outside the function to avoid garbage collection pressure
local REPLACEMENTS = {
    BonusFireRate = {icon='{{Icon/Brown|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    BonusMoveSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    BonusSprintSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Sprint speed|20px]]}}', label_color='#ffefd7', link='Sprint speed'},
    BonusSpiritDamage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    BonusWeaponDamage = {icon='{{Icon/NoColor|[[File:Bullet Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    bullet_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
    bullet_damage = {icon='{{Icon/NoColor|[[File:Bullet_damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    BulletResist = {icon='{{Icon/Brown|[[File:Bullet_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#ec981a', link='Damage_Resistance'},
    cast = {icon='{{Icon/White|[[File:Cast.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    CombatBarrier = {icon='{{Icon/White|[[File:Bullet Armor (item).png|link=Damage_Resistance|20px]]}}', label_color='#ffefd7', link='Barrier'},
    cooldown = {icon='{{Icon/White|[[File:AttributeIconTechDuration.png|link=Ability Cooldown|20px]]}}', label_color='#ffefd7', link='Ability Cooldown'},
    damage = {icon='{{Icon/NoColor|[[File:Damage heart.png|link=Damage|20px]]}}', label_color='#ec981a', link='Damage'},
    DamageAmp = {icon='{{Icon/White|[[File:Damage heart.png|link=Damage Amp|20px]]}}', label_color='#ffefd7', link='Damage Amp'},
    FireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    fire_rate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    Heal = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    Healing = {icon='{{Icon/Green|[[File:Healing.png|link=Healing|20px]]}}', label_color='#13f278', link='Healing'},
    Immobilize = {icon='{{Icon/White|[[File:Immobilize.png|link=Status Effects#Immobilized|20px]]}}', label_color='#ffefd7', link='Status Effects#Immobilized'},
    invisible = {icon='{{Icon/White|[[File:Invisible.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    KnockBack = {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    KnockUp =  {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    MaxHealth = {icon='{{Icon/Green|[[File:Healing.png|link=Health|20px]]}}', label_color='#13f278', link='Health'},
    MeleeDamage = {icon='{{Icon/Brown|[[File:Melee damage.png|link=Melee Damage|20px]]}}', label_color='#ec981a', link='Melee Damage'},
    MoveSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    move_speed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    Pull =      {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    Pulling =  {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    Pulls =    {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    PureDamage = {icon='{{Icon/NoColor|[[File:Damage.png|link=Pure Damage|20px]]}}', label_color='#ffefd7', link='Pure Damage'},
    ReducedFireRate = {icon='{{Icon/Brown|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    Regen = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    revealed = {icon='{{Icon/White|[[File:Revealed.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    silence = {icon='{{Icon/White|[[File:Silence.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    Silence = {icon='{{Icon/Purple|[[File:Silence.png|link=Silence|20px]]}}', label_color='#bc8ee8', link='Silence'},
    Slow = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow|20px]]}}', label_color='#ffefd7', link='Movement_Slow'},
    slow = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow|20px]]}}', label_color='#ffefd7', link='Movement_Slow'},
    SlowResistance = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow#Movement_Slow_Resist|20px]]}}', label_color='#ffefd7', link='Movement_Slow#Movement_Slow_Resist'},
    Spirit = {icon='{{Icon/NoColor|[[File:Spirit_icon.png|link=Spirit Power|20px]]}}', label_color='#bc8ee8', link='Spirit Power'},
    SpiritDamage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    SpiritDPS = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    SpiritIcon = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', link='Spirit Damage'},
    SpiritResist = {icon='{{Icon/Purple|[[File:Spirit_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#bc8ee8', link='Damage_Resistance'},
    StatDesc_TechPower = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    Stun = {icon='{{Icon/White|[[File:Status_Stun.png|link=Stun|20px]]}}', link='Stun'},
    stun = {icon='{{Icon/White|[[File:Stun.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    tech_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
    tech_damage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', link='Spirit Damage'},
    WeaponDamage = {icon='{{Icon/NoColor|[[File:Bullet Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    Heals = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
}
local KEYBINDS = {
    MoveForward = "forward",
    AltCast = "alt_cast",
}
}


function get_lang_file(lang_code)
function get_lang_file(lang_code)
    if lang_file_cache[lang_code] ~= nil then
        return lang_file_cache[lang_code]
    end
   
     local file_name = string.format("Data:Lang_%s.json", lang_code)
     local file_name = string.format("Data:Lang_%s.json", lang_code)
     local success, data = pcall(mw.loadJsonData, file_name)
     local success, data = pcall(mw.loadJsonData, file_name)
     if success then
     if success then
        lang_file_cache[lang_code] = data
         return data
         return data
     else
     else
        lang_file_cache[lang_code] = false -- Cache failures to avoid repeating pcall
         return nil
         return nil
     end
     end
Line 53: Line 120:
local function process_newlines(text)
local function process_newlines(text)
     if not text or type(text) ~= "string" then return text end
     if not text or type(text) ~= "string" then return text end
    -- Replace "n with newline character
     return text:gsub('"n', '<br/>')
     return text:gsub('"n', '<br/>')
end
end


local function process_html_tags(text, frame)
local function process_html_tags(text, frame, lang_code)
     if not text or type(text) ~= "string" then return text end
     if not text or type(text) ~= "string" then return text end
      
      
    local replacements = {
    BonusFireRate = {icon='{{Icon/Brown|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    BonusMoveSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    BonusSprintSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Sprint speed|20px]]}}', label_color='#ffefd7', link='Sprint speed'},
    BonusSpiritDamage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    BonusWeaponDamage = {icon='{{Icon/NoColor|[[File:Bullet Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    bullet_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
    bullet_damage = {icon='{{Icon/NoColor|[[File:Bullet_damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    BulletResist = {icon='{{Icon/Brown|[[File:Bullet_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#ec981a', link='Damage_Resistance'},
    cast = {icon='{{Icon/White|[[File:Cast.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    CombatBarrier = {icon='{{Icon/White|[[File:Bullet Armor (item).png|link=Damage_Resistance|20px]]}}', label_color='#ffefd7', link='Barrier'},
    cooldown = {icon='{{Icon/White|[[File:AttributeIconTechDuration.png|link=Ability Cooldown|20px]]}}', label_color='#ffefd7', link='Ability Cooldown'},
    damage = {icon='{{Icon/NoColor|[[File:Damage heart.png|link=Damage|20px]]}}', label_color='#ec981a', link='Damage'},
    DamageAmp = {icon='{{Icon/White|[[File:Damage heart.png|link=Damage Amp|20px]]}}', label_color='#ffefd7', link='Damage Amp'},
    FireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    fire_rate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    Heal = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    Healing = {icon='{{Icon/Green|[[File:Healing.png|link=Healing|20px]]}}', label_color='#13f278', link='Healing'},
    Immobilize = {icon='{{Icon/White|[[File:Immobilize.png|link=Status Effects#Immobilized|20px]]}}', label_color='#ffefd7', link='Status Effects#Immobilized'},
    invisible = {icon='{{Icon/White|[[File:Invisible.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    KnockBack = {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    KnockUp =  {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    MaxHealth = {icon='{{Icon/Green|[[File:Healing.png|link=Health|20px]]}}', label_color='#13f278', link='Health'},
    MeleeDamage = {icon='{{Icon/Brown|[[File:Melee damage.png|link=Melee Damage|20px]]}}', label_color='#ec981a', link='Melee Damage'},
    MoveSpeed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    move_speed = {icon='{{Icon/Green|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    Pull =      {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    Pulling =  {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    Pulls =    {icon='{{Icon/NoColor|[[File:Displacement.png|link=|20px]]}}', label_color='#ffefd7', link='Status_Effects'},
    PureDamage = {icon='{{Icon/NoColor|[[File:Damage.png|link=Pure Damage|20px]]}}', label_color='#ffefd7', link='Pure Damage'},
    ReducedFireRate = {icon='{{Icon/Brown|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    Regen = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    revealed = {icon='{{Icon/White|[[File:Revealed.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    silence = {icon='{{Icon/White|[[File:Silence.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    Silence = {icon='{{Icon/Purple|[[File:Silence.png|link=Silence|20px]]}}', label_color='#bc8ee8', link='Silence'},
    Slow = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow|20px]]}}', label_color='#ffefd7', link='Movement_Slow'},
    slow = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow|20px]]}}', label_color='#ffefd7', link='Movement_Slow'},
    SlowResistance = {icon='{{Icon/White|[[File:MoveSlow.png|link=Movement_Slow#Movement_Slow_Resist|20px]]}}', label_color='#ffefd7', link='Movement_Slow#Movement_Slow_Resist'},
    Spirit = {icon='{{Icon/NoColor|[[File:Spirit_icon.png|link=Spirit Power|20px]]}}', label_color='#bc8ee8', link='Spirit Power'},
    SpiritDamage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    SpiritDPS = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    SpiritIcon = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', link='Spirit Damage'},
    SpiritResist = {icon='{{Icon/Purple|[[File:Spirit_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#bc8ee8', link='Damage_Resistance'},
    StatDesc_TechPower = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    Stun = {icon='{{Icon/White|[[File:Status_Stun.png|link=Stun|20px]]}}', link='Stun'},
    stun = {icon='{{Icon/White|[[File:Stun.png|link=|20px]]}}', label_color='#ffefd7', link=''},
    tech_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
    tech_damage = {icon='{{Icon/NoColor|[[File:Spirit damage.png|link=Spirit Damage|20px]]}}', link='Spirit Damage'},
    WeaponDamage = {icon='{{Icon/NoColor|[[File:Bullet Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    Heals = {icon='{{Icon/Green|[[File:Healing.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
}
 
     -- Process HTML spans
     -- Process HTML spans
     text = text:gsub('<span class="highlight">(.-)</span>', "<span style= font-weight:bold>%1</span>")
     text = text:gsub('<span class="highlight">(.-)</span>', "<span style= font-weight:bold>%1</span>")
Line 121: Line 135:
     text = text:gsub('</Panel>', '')
     text = text:gsub('</Panel>', '')
      
      
     -- Process panel tags
     -- Process panel tags (Optimized: Using internal Lua function call instead of frame:preprocess #invoke)
text = text:gsub('<Panel class=\"AbilityPropertyIcon prop_(.-)\">', function(key)
    text = text:gsub('<Panel class=\"AbilityPropertyIcon prop_(.-)\">', function(key)
    local replacement = replacements[key]
        local replacement = REPLACEMENTS[key]
    if replacement then
        if replacement then
        local label_color = replacement.label_color or 'inherit'
            local label_color = replacement.label_color or 'inherit'
        local icon = replacement.icon or ''
            local icon = replacement.icon or ''
        local link = replacement.link or ''
            local link = replacement.link or ''
        -- pre-process string, then check to omit link if the string is empty
           
        local translated = frame:preprocess('{{#invoke:Lang|get_string|InlineAttribute_' .. key .. '}}')
            local translated = p.get_string('InlineAttribute_' .. key, lang_code, '')
       
           
        if translated == '' then
            if translated == '' then
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s</span>', label_color, icon)
                return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s</span>', label_color, icon)
        else
            else
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',  
                return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',  
                label_color,  
                    label_color, icon, link, translated)
                icon,  
            end
                link,  
        else
                translated)
            return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing attribute definition - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
        end
        end
    else
    end)
        return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing attribute definition - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
       
    end
     -- Process citadel_inline_attribute tags (Optimized: Internal function call instead of frame:preprocess #invoke)
end)
    text = text:gsub("{g:citadel_inline_attribute:'(.-)'}", function(key)
   
        local replacement = REPLACEMENTS[key]
     -- Process citadel_inline_attribute tags
        if replacement then
text = text:gsub("{g:citadel_inline_attribute:'(.-)'}", function(key)
            local label_color = replacement.label_color or 'inherit'
    local replacement = replacements[key]
            local icon = replacement.icon or ''
    if replacement then
            local link = replacement.link or ''
        local label_color = replacement.label_color or 'inherit'
           
        local icon = replacement.icon or ''
            local translated = p.get_string('InlineAttribute_' .. key, lang_code, '')
        local link = replacement.link or ''
           
        -- pre-process string, then check to omit link if the string is empty
            if translated == '' then
        local translated = frame:preprocess('{{#invoke:Lang|get_string|InlineAttribute_' .. key .. '}}')
                return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s</span>', label_color, icon)
       
            else
        if translated == '' then
                return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',  
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s</span>', label_color, icon)
                    label_color, icon, link, translated)
        else
            end
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',  
        else
                label_color,  
            return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing attribute definition - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
                icon,  
        end
                link,  
    end)
                translated)
        end
    else
        return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing attribute definition - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
    end
end)
      
      
    -- List for keybinds with mismatched names. Replace with the correct name under "citadel_keybind_[key]" in Data:Lang
     -- Process citadel_binding/citadel_keybind tags (Optimized: Internal function call instead of frame:preprocess #invoke)
    keybinds = {
    text = text:gsub("{g:citadel_binding:'(.-)'}", function(key)
        MoveForward = "forward",
        AltCast = "alt_cast",
        }
   
     -- Process citadel_binding/citadel_keybind tags to replace with keybind strings/templates
text = text:gsub("{g:citadel_binding:'(.-)'}", function(key)
  -- convert key to lowercase
         local lower_key = key:lower()
         local lower_key = key:lower()
        -- look up key with Module:Lang. check if it has a replacement key first
         local replacement = KEYBINDS[key] or lower_key
         local replacement = keybinds[key] or lower_key
         local translated = p.get_string('citadel_keybind_' .. replacement, lang_code, '')
         local translated = frame:preprocess('{{#invoke:Lang|get_string|citadel_keybind_'.. replacement ..'}}')
       
  if translated ~= nil and translated ~= '' then
        if translated ~= nil and translated ~= '' then
        return "'''" .. translated .. "'''"
            return "'''" .. translated .. "'''"
         else
         else
             return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing keybind - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
             return '<span style="color:red;font-weight:bold;border-bottom:1px dotted red;" title="Missing keybind - Add \''..key..'\' to local replacements in [Module:Lang]">['..key..']</span>'
         end
         end
end)
    end)
   
     -- Force spaces around keybinds where the source data omits them
     -- Force spaces around keybinds where the source data omits them
     text = text:gsub("'''([a-zA-Z])", "''' %1")
     text = text:gsub("'''([a-zA-Z])", "''' %1")
Line 195: Line 196:
     text = text:gsub("'''%s+:", "''':")
     text = text:gsub("'''%s+:", "''':")


return frame:preprocess(text)
    return frame:preprocess(text)
end
end




function p.get_string(key, lang_code_override, fallback_str, remove_var_index, item_name)
function p.get_string(key, lang_code_override, fallback_str, remove_var_index, item_name)
    -- Get frame object (either passed directly or via first argument)
     local frame
     local frame
     if type(key) == "table" and key.args then
     if type(key) == "table" and key.args then
Line 215: Line 215:
     end
     end


    -- Determine lang_code if not overridden
     local lang_code = lang_code_override
     local lang_code = lang_code_override
     if (lang_code == '' or lang_code == nil) then
     if (lang_code == '' or lang_code == nil) then
Line 221: Line 220:
     end
     end


    -- Retrieve lang data
     local data = get_lang_file(lang_code)
     local data = get_lang_file(lang_code)
     if (data == nil) then
     if (data == nil) then
Line 227: Line 225:
     end
     end
      
      
    -- Localize
     local label = data[KEY_OVERRIDES[key] or key]
     local label = data[KEY_OVERRIDES[key] or key]


-- Some labels use key with "_postvalue_label" instead, so check this if previous search failed
    if label == nil and key ~= nil and key:sub(-6) == '_label' then
if label == nil and key ~= nil and key:sub(-6) == '_label' then
        local postvalue_key = key:sub(1, -7) .. '_postvalue_label'
    local postvalue_key = key:sub(1, -7) .. '_postvalue_label'
        label = data[postvalue_key]
    label = data[postvalue_key]
    end
end
   
     if (label == nil) then
     if (label == nil) then
        -- Apply fallback (without HTML processing for fallback)
         local fallback_tooltip = frame:expandTemplate{title = "MissingValveTranslationTooltip"}
         local fallback_tooltip = frame:expandTemplate{title = "MissingValveTranslationTooltip"}
         local fallback
         local fallback
Line 252: Line 247:
     end
     end
      
      
     -- Always retrieve the last parameter if string contains "|"
     -- Optimized: Using efficient string processing instead of converting to table array via mw.text.split
     if type(label) == "string" and label:find("|") then
     if type(label) == "string" and label:find("|") then
         local parts = mw.text.split(label, "|")
         label = label:gsub("^.*|", "")
        label = parts[#parts]
     end
     end
   
    
   -- Remove hashtags
    if type(label) == "string" and label:find("#") then
  if type(label) == "string" and label:find("#") then
         label = label:gsub("^.*#", "")
         local parts = mw.text.split(label, "#")
        label = parts[#parts]
     end
     end


    -- Apply remove_var
     if (remove_var_index ~= nil) then  
     if (remove_var_index ~= nil) then  
         label = util_module.remove_var(label, remove_var_index)
         label = util_module.remove_var(label, remove_var_index)
     end
     end
      
      
    -- Process variables if item_name is provided
     if item_name and item_name ~= '' then
     if item_name and item_name ~= '' then
         local item_data_module = require('Module:ItemData')
         local item_data_module = require('Module:ItemData')
         label = label:gsub("{s:([^}]+)}", function(variable_name)
         label = label:gsub("{s:([^}]+)}", function(variable_name)
            -- Get the value from ItemData
             local value = item_data_module.get_prop({args = {item_name, variable_name}})
             local value = item_data_module.get_prop({args = {item_name, variable_name}})
           
            -- If value exists, remove non-number symbols (but keep + and -)
             if value then
             if value then
                 value = value:gsub("[^0-9+-.]", "")
                 value = value:gsub("[^0-9+-.]", "")
                -- Return empty string if nothing left, otherwise return the filtered value
                 return value ~= "" and value or variable_name
                 return value ~= "" and value or variable_name
             else
             else
                 return variable_name -- Fallback to variable name if value not found
                 return variable_name
             end
             end
         end)
         end)
     end
     end
      
      
    -- Process HTML and return as "raw" HTML
     label = process_newlines(label)
     label = process_newlines(label)
     label = process_html_tags(label, frame)
     label = process_html_tags(label, frame, lang_code) -- Added lang_code parameter
     -- Create HTML object for safe output
 
    -- Note: mw.html.create() wraps text in a <div> block. If you just need raw text,
     -- you should drop this wrapper and return `label` directly to save extra processing.
     local html = mw.html.create()
     local html = mw.html.create()
     html:wikitext(label)
     html:wikitext(label)
Line 296: Line 283:
end
end


-- Search for a localized string using its English label
p.search_string = function(frame)
p.search_string = function(frame)
     local label = frame.args[1]
     local label = frame.args[1]
Line 308: Line 294:
-- search_string, but for internal use by other modules
-- search_string, but for internal use by other modules
p._search_string = function(label, lang_code_override)
p._search_string = function(label, lang_code_override)
     lang_code = lang_code_override
     local lang_code = lang_code_override -- Fixed missing local statement (was leaking globally)
     if (lang_code == '' or lang_code == nil) then
     if (lang_code == '' or lang_code == nil) then
         lang_code = p.get_lang_code()
         lang_code = p.get_lang_code()
     end
     end


    -- Load the language files
     local data_lang = get_lang_file(lang_code)
    local data_en = get_lang_file('en')  -- English data
     local data_lang = get_lang_file(lang_code) -- Target language data
 
     if (data_lang == nil) then
     if (data_lang == nil) then
         error("Lang code '%s' does not have a json file", lang_code)     
         error("Lang code '%s' does not have a json file", lang_code)     
     end
     end
      
      
     -- Search for the key in the English data
     -- Optimized: Lazy build the reverse key index cache.
     local key = nil
    -- This turns an O(N) loop into an O(1) table lookup map.
    for k, v in pairs(data_en) do
     if not en_reverse_cache then
        if v == label then
        en_reverse_cache = {}
            key = k -- Find the key corresponding to the label
        local data_en = get_lang_file('en')
             break
        if data_en then
            for k, v in pairs(data_en) do
                if en_reverse_cache[v] == nil then
                    en_reverse_cache[v] = k
                end
             end
         end
         end
     end
     end


     -- Default to input label if localized string is not found
     local key = en_reverse_cache[label]
     if (key == nil) then
 
        return label
     if (key == nil) or (data_lang[key] == nil) then
    end
    if (data_lang[key] == nil) then
         return label
         return label
     end
     end