Jump to content

Module:Lang: Difference between revisions

From The Deadlock Wiki
swapped argument order
Added replacement of keybind tags with keybind strings
 
(74 intermediate revisions by 5 users not shown)
Line 1: Line 1:
local p = {}
local p = {}
local util_module = require("Module:Utilities")
local lang_codes_set = mw.loadJsonData("Data:LangCodes.json")
local dictionary_module = require("Module:Dictionary")
-- Overrides applied to searches by key. Designed to handle edge cases where
-- the expected key does not have a localization entry
local KEY_OVERRIDES = {
    MoveSlowPercent_label = 'MovementSlow_label',
    BonusHealthRegen_label = 'HealthRegen_label',
    BarbedWireRadius_label = 'Radius_label',
    BarbedWireDamagePerMeter_label = 'DamagePerMeter_label',
    BuildUpDuration_label = 'BuildupDuration_label',
    TechArmorDamageReduction_label = 'TechArmorDamageReduction_Label',
    DamageAbsorb_label = 'DamageAbsorb_Label',
    InvisRegen_label = 'InvisRegen_Label',
    EvasionChance_label = 'EvasionChance_Label',
    DelayBetweenShots_label = 'DelayBetweenShots_Label',
}


function get_lang_file(lang_code)
function get_lang_file(lang_code)
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
Line 11: Line 29:
end
end


-- Get a localized string by the raw key
local function process_newlines(text)
p.get_string = function(frame)
    if not text or type(text) ~= "string" then return text end
local key = frame.args[1]
    -- Replace "n with newline character
local lang_code = frame.args[2]
    return text:gsub('"n', '\n')
end
 
local function process_html_tags(text, frame)
    if not text or type(text) ~= "string" then return text end
   
    local replacements = {
    SpiritIcon = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', link='Spirit Damage'},
    tech_damage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', link='Spirit Damage'},
    SpiritDamage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    StatDesc_TechPower = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    SpiritResist = {icon='{{Icon/Purple|[[File:Spirit_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#bc8ee8', link='Damage_Resistance'},
    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'},
    ReducedFireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    MeleeDamage = {icon='{{Icon/Brown|[[File:Melee damage.png|link=Melee Damage|20px]]}}', label_color='#ec981a', link='Melee Damage'},
    WeaponDamage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    damage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    bullet_damage = {icon='{{Icon/Brown|[[File: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'},
    CombatBarrier = {icon='{{Icon/White|[[File:Bullet_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#ffefd7', link='Barrier'},
    BonusWeaponDamage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
    MoveSpeed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    BonusSpiritDamage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    BonusFireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
    Heal = {icon='{{Icon/Green|[[File:Health_regen.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    BonusMoveSpeed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    move_speed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
    Spirit = {icon='{{Icon/NoColor|[[File:Spirit_icon.png|link=Spirit Power|20px]]}}', label_color='#bc8ee8', link='Spirit Power'},
    Stun = {icon='{{Icon/White|[[File:Status_Stun.png|link=Stun|20px]]}}', link='Stun'},
    SpiritDPS = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
    Healing = {icon='{{Icon/Green|[[File:Health_regen.png|link=Healing|20px]]}}', label_color='#13f278', link='Healing'},
    Regen = {icon='{{Icon/Green|[[File:Health_regen.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
    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'},
        Immobilize = {icon='{{Icon/White|[[File:Immobilize.png|link=Status Effects#Immobilized|20px]]}}', label_color='#ffefd7', link='Status Effects#Immobilized'},
        cooldown = {icon='{{Icon/White|[[File:AttributeIconTechDuration.png|link=Ability Cooldown|20px]]}}', label_color='#ffefd7', link='Ability Cooldown'},
        stun = {icon='{{Icon/White|[[File:Stun.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        invisible = {icon='{{Icon/White|[[File:Invisible.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        cast = {icon='{{Icon/White|[[File:Cast.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        silence = {icon='{{Icon/White|[[File:Silenced.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        revealed = {icon='{{Icon/White|[[File:Revealed.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        bullet_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
        tech_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
}
 
    -- Process HTML spans
    text = text:gsub('<span class="highlight">(.-)</span>', "<span style= font-weight:bold>%1</span>")
    text = text:gsub('<span class="highlight_spirit">(.-)</span>', '<span style="font-weight:bold; color:#bc8ee8">%1</span>')
    text = text:gsub('<span class="diminish">(.-)</span>', '<span style="font-style: italic;color:#C0C0C0">%1</span>')
    text = text:gsub('<span class=".-">(.-)</span>', '%1')
    text = text:gsub('<Panel class=\"AbilityPropertyIcon StatDesc_TechPower\">', '{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}')
    text = text:gsub('</Panel>', '')
   
    -- Process panel tags
text = text:gsub('<Panel class=\"AbilityPropertyIcon prop_(.-)\">', function(key)
    local replacement = replacements[key]
    if replacement then
        local label_color = replacement.label_color or 'inherit'
        local icon = replacement.icon 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 .. '}}')
       
        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)
        else
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',
                label_color,
                icon,
                link,
                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)
   
    -- Process citadel_inline_attribute tags
text = text:gsub("{g:citadel_inline_attribute:'(.-)'}", function(key)
    local replacement = replacements[key]
    if replacement then
        local label_color = replacement.label_color or 'inherit'
        local icon = replacement.icon 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 .. '}}')
       
        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)
        else
            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>',
                label_color,
                icon,
                link,
                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)
 
    KEYBINDS = {
    Attack = '{{Mouse|1}}',
    ADS = '{{Mouse|2}}',
    AltCast = '{{Mouse|3}}',
    Reload = 'R',
    Roll = 'Shift',
    Mantle = 'Space',
    Crouch = 'Ctrl',
    AbilityMelee = 'Melee',
    Ability1 = '1',
    Ability2 = '2',
    Ability3 = '3',
    Ability4 = '4',
    MoveDown = 'Down',
    MoveForward = 'Forward',
    }
   
    -- Process citadel_binding/citadel_keybind tags to replace with keybind strings/templates
text = text:gsub("{g:citadel_binding:'(.-)'}", function(key)
    local replacement = KEYBINDS[key]
    if replacement then
        return replacement
    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>'
    end
end)
local data = get_lang_file(lang_code)
text = text:gsub("{g:citadel_keybind:'(.-)'}", function(key)
if (data == nil) then
local replacement = KEYBINDS[key]
return string.format("Language code '%s' not found", lang_code)
    if replacement then
end
        return replacement
    else
local label = data[key]
        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>'
if (label == nil) then  
    end
return string.format("Key '%s' not found", key)  
end)
end
 
return frame:preprocess(text)
return label
end
 
 
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
    if type(key) == "table" and key.args then
        frame = key
        key = frame.args[1]
        lang_code_override = frame.args["lang_code_override"]
        fallback_str = frame.args["fallback_str"]
        remove_var_index = frame.args["remove_var_index"]
        item_name = frame.args["item_name"]
    else
        frame = mw.getCurrentFrame()
    end
 
    -- Determine lang_code if not overridden
    local lang_code = lang_code_override
    if (lang_code == '' or lang_code == nil) then
        lang_code = p.get_lang_code()
    end
 
    -- Retrieve lang data
    local data = get_lang_file(lang_code)
    if (data == nil) then
        return string.format("Lang code '%s' does not have a json file", lang_code)  
    end
   
    -- Localize
    local label = data[KEY_OVERRIDES[key] or key]
    if (label == nil) then
        -- Apply fallback (without HTML processing for fallback)
        local fallback_tooltip = frame:expandTemplate{title = "MissingValveTranslationTooltip"}
        local fallback
        if (fallback_str == 'en') then
            fallback = p.get_string(key, 'en', key .. fallback_tooltip, remove_var_index, item_name)
        elseif fallback_str == 'dictionary' then
            return dictionary_module.translate(key, lang_code_override)
        elseif fallback_str ~= nil then
            fallback = fallback_str
        else
            return ''
        end
        return fallback .. fallback_tooltip
    end
   
    -- Apply remove_var
    if (remove_var_index ~= nil) then
        label = util_module.remove_var(label, remove_var_index)
    end
   
    -- Process variables if item_name is provided
    if item_name and item_name ~= '' then
        local item_data_module = require('Module:ItemData')
        label = label:gsub("{s:([^}]+)}", function(variable_name)
            -- Get the value from ItemData
            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
                value = value:gsub("[^0-9+-]", "")
                -- Return empty string if nothing left, otherwise return the filtered value
                return value ~= "" and value or variable_name
            else
                return variable_name -- Fallback to variable name if value not found
            end
        end)
    end
   
    -- Process HTML and return as "raw" HTML
    label = process_newlines(label)
    label = process_html_tags(label, frame)
    -- Create HTML object for safe output
    local html = mw.html.create()
    html:wikitext(label)
    return tostring(html)
end
end


-- Search for a localized string using its English label
-- Search for a localized string using its English label
p.search_string = function(frame)
p.search_string = function(frame)
local key = frame.args[1]
    local label = frame.args[1]
local lang_code = frame.args[2]
    local lang_code_override = frame.args[2]
local data = get_lang_file(lang_code)
 
    local result = p._search_string(label, lang_code_override)
    result = process_newlines(result)
    return result
end
 
-- search_string, but for internal use by other modules
p._search_string = function(label, lang_code_override)
    lang_code = lang_code_override
    if (lang_code == '' or lang_code == nil) then
        lang_code = p.get_lang_code()
    end
 
    -- Load the language files
    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
        error("Lang code '%s' does not have a json file", lang_code)   
    end
   
    -- Search for the key in the English data
    local key = nil
    for k, v in pairs(data_en) do
        if v == label then
            key = k  -- Find the key corresponding to the label
            break
        end
    end
 
    -- Default to input label if localized string is not found
    if (key == nil) then
        return label
    end
    if (data_lang[key] == nil) then
        return label
    end
 
    return data_lang[key]
end
 
p.get_lang_code = function()
    local title = mw.title.getCurrentTitle()
    local lang_code = title.fullText:match(".*/(.*)$")
   
    if lang_code == nil or lang_codes_set[lang_code] == nil then
        return 'en'   
    end
       
    return lang_code
end
end


return p
return p

Latest revision as of 23:47, 4 July 2025

Overview[edit source]

overview

Functions[edit source]

get_string[edit source]

Localizes a given string to the current language, i.e. Data:Lang_en.json for english.

Note: If you need to get a string that uses a value referenced from another Data page, pass the item_name parameter to get_string.

Parameters[edit source]

  • key - Key string to localize
  • lang_code_override (OPTIONAL) - Overrides the current language to a specific language code
  • fallback_str (OPTIONAL) - Passing en causes it to return the english localization if it can't be localized to the current language. Passing any other string causes it to return that string if it can't be localized. Both have Template:MissingValveTranslationTooltip appended. Use this very often as some keys are not yet localized in every language by the game. Passing dictionary causes it to return the translation via Data:Dictionary if it can't be localized. If parsing the fallback_str from lua and is computationally expensive (i.e. add_space_before_cap), consider using fallback outside this function so it only computes when needed.
  • remove_var_index (OPTIONAL) - Removes %variables% from the resulting string. -1 also removes the character prefixing %variables%, while 1 removes the postfixed character, and 0 removes only the %variables%.

NOTE: Optional parameters are ideally named when not all parameters are provided, though named parameters can only be passed by invoke, and not internal lua calls.

Examples[edit source]

Invokes from wikitext:

{{#invoke:Lang|get_string|CitadelHeroStats_Weapon_Falloff}}

Falloff Range


{{#invoke:Lang|get_string|CitadelHeroStats_Weapon_Falloff|lang_code_override=es}}

Distancia de caída

Strings with Variables[edit source]

For strings containing {s:variable}, provide the item_name:

Example: {{#invoke:Lang|get_string|upgrade_target_stun_desc|item_name=Knockdown}} → Apply a Stun after 2s. Stun duration is increased against airborne targets.

Without item_name, variables won't be filled: {{#invoke:Lang|get_string|upgrade_target_stun_desc}} → Apply a Stun after {s:StunDelay}s. Stun duration is increased against airborne targets.


Examples for fallback_str[edit source]

{{#invoke:Lang|get_string|StatDesc_CritDamageBonusScale|lang_code_override=es}}

Escala de críticos adicional


{{#invoke:Lang|get_string|StatDesc_CritDamageBonusScale|lang_code_override=es|fallback_str=en}}

Escala de críticos adicional


{{#invoke:Lang|get_string|StatDesc_CritDamageBonusScale|lang_code_override=es|fallback_str=Crit Damage Bonus Scale}}

Escala de críticos adicional


{{#invoke:Lang|get_string|Tech Items|fallback_str=dictionary}}

Key 'Tech Items' is not in Data:Dictionary

Examples for remove_var_index[edit source]

{{#invoke:Lang|get_string|Citadel_HeroBuilds_DefaultHeroBuild}}

Default %hero_name% Build

TODO: Debug why is =0 still removing that extra space? Doesn't matter yet I suppose, no use cases for 0 yet {{#invoke:Lang|get_string|Citadel_HeroBuilds_DefaultHeroBuild|remove_var_index=0}}

Default Build


{{#invoke:Lang|get_string|Citadel_HeroBuilds_DefaultHeroBuild|remove_var_index=-1}}

Default Build


When calling by internal modules, the parameters cannot be named, and therefore have to be in order. Unused parameters before the last used parameter should be nil. Such as, .get_string('hero_atlas', nil, 'en')

search_string[edit source]

Searches for the unlocalized key corresponding to a given english string, then localizes it to the current language. NOTE: Use sparingly, always use get_string instead where plausible, as it has time complexity O(1) compared to search_string's O(10,000).

Parameters[edit source]

  • string - English string to search for

Examples[edit source]

From wikitext:

{{#invoke:Lang|search_string|Abrams}}

Abrams

get_lang_code[edit source]

Output the language subpage of the current page

{{#invoke:Lang|get_lang_code}}

en


local p = {}
local util_module = require("Module:Utilities")
local lang_codes_set = mw.loadJsonData("Data:LangCodes.json")
local dictionary_module = require("Module:Dictionary")

-- Overrides applied to searches by key. Designed to handle edge cases where
-- the expected key does not have a localization entry
local KEY_OVERRIDES = {
    MoveSlowPercent_label = 'MovementSlow_label',
    BonusHealthRegen_label = 'HealthRegen_label',
    BarbedWireRadius_label = 'Radius_label',
    BarbedWireDamagePerMeter_label = 'DamagePerMeter_label',
    BuildUpDuration_label = 'BuildupDuration_label',
    TechArmorDamageReduction_label = 'TechArmorDamageReduction_Label',
    DamageAbsorb_label = 'DamageAbsorb_Label',
    InvisRegen_label = 'InvisRegen_Label',
    EvasionChance_label = 'EvasionChance_Label',
    DelayBetweenShots_label = 'DelayBetweenShots_Label',
}

function get_lang_file(lang_code)
    local file_name = string.format("Data:Lang_%s.json", lang_code)
    local success, data = pcall(mw.loadJsonData, file_name)
    if success then
        return data
    else
        return nil
    end
end

local function process_newlines(text)
    if not text or type(text) ~= "string" then return text end
    -- Replace "n with newline character
    return text:gsub('"n', '\n')
end

local function process_html_tags(text, frame)
    if not text or type(text) ~= "string" then return text end
    
    local replacements = {
	    SpiritIcon = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', link='Spirit Damage'},
	    tech_damage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', link='Spirit Damage'},
	    SpiritDamage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
	    StatDesc_TechPower = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
	    SpiritResist = {icon='{{Icon/Purple|[[File:Spirit_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#bc8ee8', link='Damage_Resistance'},
	    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'},
	    ReducedFireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
	    MeleeDamage = {icon='{{Icon/Brown|[[File:Melee damage.png|link=Melee Damage|20px]]}}', label_color='#ec981a', link='Melee Damage'},
	    WeaponDamage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
	    damage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
	    bullet_damage = {icon='{{Icon/Brown|[[File: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'},
	    CombatBarrier = {icon='{{Icon/White|[[File:Bullet_Armor.png|link=Damage_Resistance|20px]]}}', label_color='#ffefd7', link='Barrier'},
	    BonusWeaponDamage = {icon='{{Icon/Brown|[[File:Damage.png|link=Weapon Damage|20px]]}}', label_color='#ec981a', link='Weapon Damage'},
	    MoveSpeed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
	    BonusSpiritDamage = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
	    BonusFireRate = {icon='{{Icon/White|[[File:Fire_Rate.png|link=Fire Rate|20px]]}}', label_color='#ffefd7', link='Fire Rate'},
	    Heal = {icon='{{Icon/Green|[[File:Health_regen.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
	    BonusMoveSpeed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
	    move_speed = {icon='{{Icon/White|[[File:Move_speed.png|link=Move speed|20px]]}}', label_color='#ffefd7', link='Move speed'},
	    Spirit = {icon='{{Icon/NoColor|[[File:Spirit_icon.png|link=Spirit Power|20px]]}}', label_color='#bc8ee8', link='Spirit Power'},
	    Stun = {icon='{{Icon/White|[[File:Status_Stun.png|link=Stun|20px]]}}', link='Stun'},
	    SpiritDPS = {icon='{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}', label_color='#bc8ee8', link='Spirit Damage'},
	    Healing = {icon='{{Icon/Green|[[File:Health_regen.png|link=Healing|20px]]}}', label_color='#13f278', link='Healing'},
	    Regen = {icon='{{Icon/Green|[[File:Health_regen.png|link=Health_Regen|20px]]}}', label_color='#13f278', link='Health_Regen'},
	    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'},
        Immobilize = {icon='{{Icon/White|[[File:Immobilize.png|link=Status Effects#Immobilized|20px]]}}', label_color='#ffefd7', link='Status Effects#Immobilized'},
        cooldown = {icon='{{Icon/White|[[File:AttributeIconTechDuration.png|link=Ability Cooldown|20px]]}}', label_color='#ffefd7', link='Ability Cooldown'},
        stun = {icon='{{Icon/White|[[File:Stun.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        invisible = {icon='{{Icon/White|[[File:Invisible.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        cast = {icon='{{Icon/White|[[File:Cast.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        silence = {icon='{{Icon/White|[[File:Silenced.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        revealed = {icon='{{Icon/White|[[File:Revealed.png|link=|20px]]}}', label_color='#ffefd7', link=''},
        bullet_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
        tech_armor_up = {icon='{{Icon/White|[[File:Barrier.png|link=Barrier|20px]]}}', label_color='#ffefd7', link='Barrier'},
	}
   
    -- Process HTML spans
    text = text:gsub('<span class="highlight">(.-)</span>', "<span style= font-weight:bold>%1</span>")
    text = text:gsub('<span class="highlight_spirit">(.-)</span>', '<span style="font-weight:bold; color:#bc8ee8">%1</span>')
    text = text:gsub('<span class="diminish">(.-)</span>', '<span style="font-style: italic;color:#C0C0C0">%1</span>')
    text = text:gsub('<span class=".-">(.-)</span>', '%1')
    text = text:gsub('<Panel class=\"AbilityPropertyIcon StatDesc_TechPower\">', '{{Icon/Purple|[[File:AttributeIconTechShieldHealth.png|link=Spirit Damage|12px]]}}')
    text = text:gsub('</Panel>', '')
    
    -- Process panel tags
	text = text:gsub('<Panel class=\"AbilityPropertyIcon prop_(.-)\">', function(key)
	    local replacement = replacements[key]
	    if replacement then
	        local label_color = replacement.label_color or 'inherit'
	        local icon = replacement.icon 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 .. '}}')
	        
	        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)
	        else
	            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>', 
	                label_color, 
	                icon, 
	                link, 
	                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)
	    
    -- Process citadel_inline_attribute tags
	text = text:gsub("{g:citadel_inline_attribute:'(.-)'}", function(key)
	    local replacement = replacements[key]
	    if replacement then
	        local label_color = replacement.label_color or 'inherit'
	        local icon = replacement.icon 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 .. '}}')
	        
	        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)
	        else
	            return string.format('<span class="no-blue-link" style="text-wrap:nowrap; font-weight:bold; color:%s;">%s [[%s|%s]]</span>', 
	                label_color, 
	                icon, 
	                link, 
	                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)

    KEYBINDS = {
	    Attack = '{{Mouse|1}}',
	    ADS = '{{Mouse|2}}',
	    AltCast = '{{Mouse|3}}',
	    Reload = 'R',
	    Roll = 'Shift',
	    Mantle = 'Space',
	    Crouch = 'Ctrl',
	    AbilityMelee = 'Melee',
	    Ability1 = '1',
	    Ability2 = '2',
	    Ability3 = '3',
	    Ability4 = '4',
	    MoveDown = 'Down',
	    MoveForward = 'Forward',
    }
    
    -- Process citadel_binding/citadel_keybind tags to replace with keybind strings/templates
	text = text:gsub("{g:citadel_binding:'(.-)'}", function(key)
	    local replacement = KEYBINDS[key]
	    if replacement then
	        return replacement
	    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>'
	    end
	end)
	
	text = text:gsub("{g:citadel_keybind:'(.-)'}", function(key)
		local replacement = KEYBINDS[key]
	    if replacement then
	        return replacement
	    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>'
	    end
	end)

	return frame:preprocess(text)
end


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
    if type(key) == "table" and key.args then
        frame = key
        key = frame.args[1]
        lang_code_override = frame.args["lang_code_override"]
        fallback_str = frame.args["fallback_str"]
        remove_var_index = frame.args["remove_var_index"]
        item_name = frame.args["item_name"]
    else
        frame = mw.getCurrentFrame()
    end

    -- Determine lang_code if not overridden
    local lang_code = lang_code_override
    if (lang_code == '' or lang_code == nil) then
        lang_code = p.get_lang_code()
    end

    -- Retrieve lang data
    local data = get_lang_file(lang_code)
    if (data == nil) then
        return string.format("Lang code '%s' does not have a json file", lang_code)    
    end
    
    -- Localize
    local label = data[KEY_OVERRIDES[key] or key]
    if (label == nil) then
        -- Apply fallback (without HTML processing for fallback)
        local fallback_tooltip = frame:expandTemplate{title = "MissingValveTranslationTooltip"}
        local fallback
        if (fallback_str == 'en') then
            fallback = p.get_string(key, 'en', key .. fallback_tooltip, remove_var_index, item_name)
        elseif fallback_str == 'dictionary' then
            return dictionary_module.translate(key, lang_code_override)
        elseif fallback_str ~= nil then
            fallback = fallback_str
        else
            return ''
        end
        return fallback .. fallback_tooltip
    end
    
    -- Apply remove_var
    if (remove_var_index ~= nil) then 
        label = util_module.remove_var(label, remove_var_index)
    end
    
    -- Process variables if item_name is provided
    if item_name and item_name ~= '' then
        local item_data_module = require('Module:ItemData')
        label = label:gsub("{s:([^}]+)}", function(variable_name)
            -- Get the value from ItemData
            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
                value = value:gsub("[^0-9+-]", "")
                -- Return empty string if nothing left, otherwise return the filtered value
                return value ~= "" and value or variable_name
            else
                return variable_name -- Fallback to variable name if value not found
            end
        end)
    end
    
    -- Process HTML and return as "raw" HTML
    label = process_newlines(label)
    label = process_html_tags(label, frame)
    -- Create HTML object for safe output
    local html = mw.html.create()
    html:wikitext(label)
    return tostring(html)
end

-- Search for a localized string using its English label
p.search_string = function(frame)
    local label = frame.args[1]
    local lang_code_override = frame.args[2]

    local result = p._search_string(label, lang_code_override)
    result = process_newlines(result)
    return result
end

-- search_string, but for internal use by other modules
p._search_string = function(label, lang_code_override)
    lang_code = lang_code_override
    if (lang_code == '' or lang_code == nil) then
        lang_code = p.get_lang_code()
    end

    -- Load the language files
    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
        error("Lang code '%s' does not have a json file", lang_code)    
    end
    
    -- Search for the key in the English data
    local key = nil
    for k, v in pairs(data_en) do
        if v == label then
            key = k  -- Find the key corresponding to the label
            break
        end
    end

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

    return data_lang[key]
end

p.get_lang_code = function()
    local title = mw.title.getCurrentTitle()
    local lang_code = title.fullText:match(".*/(.*)$")
    
    if lang_code == nil or lang_codes_set[lang_code] == nil then
        return 'en'    
    end
        
    return lang_code
end

return p