Module:Lang

From The Deadlock Wiki
Jump to navigation Jump to search

Overview

[edit source]

This module serves as the primary interface for retrieving and formatting localized game text from the Data:Lang_*.json files. It handles variable substitution by fetching data from Module:ItemData, automatically applies rich text formatting for game-specific keywords (adding icons, colors, and links), and provides robust fallback mechanisms for missing translations.

Functions

[edit source]

get_string

[edit source]

Localizes a given string key to the current language, i.e. Data:Lang_en.json for English. This function also automatically processes game-specific formatting tags (see "Automatic Text Formatting" section below).

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) - Specifies behavior when a key is not found in the target language.
    • Passing en causes it to return the English localization.
    • Passing any other string causes it to return that string.
    • Both of the above options have Template:MissingValveTranslationTooltip appended.
    • Passing dictionary causes it to return a translation via Module:Dictionary without the tooltip.
    • Use this often, as some keys are not yet localized in every language by the game. If parsing the fallback_str from Lua is computationally expensive, consider using the 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%.
  • item_name (OPTIONAL) - Required for strings containing {s:variable} placeholders. It will fetch the corresponding property from Module:ItemData to fill in the variable.

NOTE: When calling from wikitext via #invoke, parameters can be named. When calling from another Lua module, parameters must be passed in order (e.g., p.get_string(key, nil, 'en')).

Internal Logic & Fallbacks

[edit source]

The function includes several automatic behaviors to handle inconsistencies in the source data:

  • Key Overrides: It checks a hardcoded list of overrides for known edge cases. For example, if the key MoveSlowPercent_label is requested, the module will internally use the value for MovementSlow_label to handle this known inconsistency.
  • Post-value Label Fallback: If a key ending in _label is not found, it will automatically try again with a key ending in _postvalue_label.
  • String Parsing: If a retrieved string contains | or #, the function will automatically use only the text that comes after the final instance of that character.

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.

Increases the target's gravity for the duration of the stun.

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.

Increases the target's gravity for the duration of the stun.


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

Automatic Text Formatting

[edit source]

When a string is retrieved by get_string, it undergoes several automatic transformations to convert in-game formatting tags into rich wikitext.

  • Newlines: "n is converted to a line break (
    ).
  • HTML Spans: Tags like ... are converted into styled text (e.g., bolded and colored purple).
  • Game Attribute Icons: Special tags like <Panel class="AbilityPropertyIcon prop_cooldown"> or {g:citadel_inline_attribute:'SpiritDamage'} are replaced with formatted wikitext that includes an icon, a colored label, and a wiki link. If a keyword is not recognized, it will be highlighted in red.
  • Keybinds: Tags like {g:citadel_binding:'Ability1'} are replaced with their respective text string (e.g., Ability 1). A replacement can be added to the list in case of tags that don't match their string name (e.g. AltCast > alt_cast). The module also cleans up spacing around these replacements for better readability. (Note: If we want to add additional text to the string like "[Ability] button" this needs to be supported by localizations)

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. This function is much slower than get_string and should be avoided when the key is known. For use in other Lua modules, see _search_string.

Parameters

[edit source]
  • string - English string to search for.
  • lang_code_override (OPTIONAL) - Overrides the current language to a specific language code.

Examples

[edit source]

From wikitext:

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

Abrams

_search_string

[edit source]

This is the internal version of search_string, intended for use by other Lua modules. It provides the core search logic without the wikitext frame overhead or final text processing.

NOTE: This function returns the raw localized string. Unlike the invoked search_string, it does not process newlines ("n). The calling module is responsible for any further processing.

Parameters

[edit source]
  • label (string) - The English string to search for.
  • lang_code_override (string, optional) - The language code to translate to.

Example (from another Lua module)

[edit source]
local lang_module = require("Module:Lang")
-- Searches for the Spanish localization of the string "Abrams"
local localized_name = lang_module._search_string("Abrams", "es")

get_lang_code

[edit source]

Outputs the language subpage of the current page (e.g., "en", "es"). Defaults to "en" if the page is not a language subpage.

{{#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 = {
	BurnDurationBase_label = 'AfterburnDurationBase_label',
	BurnDurationBase_postfix = 'AfterburnDurationBase_postfix',
    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',
    MoveSlowDuration_postfix = 'EnemyMoveSlowDuration_postfix',
    StaminaRecoveryDisabledDuration_postfix = 'AbilityDuration_postfix',
    PostInvisBuffDuration_label = 'BuffDuration_label',
    PostInvisBuffDuration_postfix = 'BuffDuration_postfix',
    ProjectileFuse_label = 'BellLifetime_label',
    ProjectileFuse_postfix = 'BellLifetime_postfix',
    SelfBuffDuration_label = 'BuffDuration_label',
    SelfBuffDuration_postfix = 'BuffDuration_postfix',
    LifeStealPercentOnHit_label = 'LifestealPercentOnHit_label',
    LifeStealPercentOnHit_postfix = 'LifestealPercentOnHit_postfix',
    AuraFireRateBonus_label = 'FireRateBonus_label',
    AuraFireRateBonus_postfix = 'FireRateBonus_postfix',
    MoveSlowDuration_label = 'SlowDuration_label',
    MoveSlowDuration_postfix = 'SlowDuration_postfix',
	DashRange_label = 'TargetDashRadius_label',
	DashRange_postfix = 'TargetDashRadius_postfix',
	SpeedOnLandDuration_label = 'BuffDuration_label',
	SpeedOnLandDuration_postfix = 'BuffDuration_postfix',
	SummonCount_label = 'GhoulCount_label',
	TetherDuration_label = 'ImmobilizeTrap_CurseDuration_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', '<br/>')
end

local function process_html_tags(text, frame)
    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
    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="highlight_courage">(.-)</span>', '<span style="font-weight:bold; color:#ec981a">%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:Spirit damage.png|link=Spirit Damage|20px]]}}')
    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)
    
    -- List for keybinds with mismatched names. Replace with the correct name under "citadel_keybind_[key]" in Data:Lang
    keybinds = {
        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()
        -- look up key with Module:Lang. check if it has a replacement key first
        local replacement = keybinds[key] or lower_key
        local translated = frame:preprocess('{{#invoke:Lang|get_string|citadel_keybind_'.. replacement ..'}}')
	   	if translated ~= nil and translated ~= '' then
	        return "'''" .. translated .. "'''"
        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)
	
    -- Force spaces around keybinds where the source data omits them
    text = text:gsub("'''([a-zA-Z])", "''' %1")

    -- Clean up extra spaces between a keybind and following punctuation
    text = text:gsub("'''%s+:", "''':")

	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]

	-- 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
	    local postvalue_key = key:sub(1, -7) .. '_postvalue_label'
	    label = data[postvalue_key]
	end
	
    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
    
    -- Always retrieve the last parameter if string contains "|"
    if type(label) == "string" and label:find("|") then
        local parts = mw.text.split(label, "|")
        label = parts[#parts]
    end
    
   -- Remove hashtags
   if type(label) == "string" and label:find("#") then
        local parts = mw.text.split(label, "#")
        label = parts[#parts]
    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