


Localization is a runtime translation helper for Valheim mods. It intercepts text right before it is displayed and replaces it with values from JSON rule files.
This mod is meant to help translate hardcoded mod UI text. It does not generate translations by itself; you provide the translation rules.
TMPro.TMP_Text.text, TMPro.TMP_Text.SetText(string, bool), and UnityEngine.UI.Text.text.exact rules for full-string matches and templates rules for placeholder-based matches.OrdinalIgnoreCase.BepInEx/config/com.paxton0505.localization/<LanguageCode>/*.json.English/*.json as an optional shared baseline.

BepInExPack_Valheim, Jotunn, and JsonDotNET.Localization.dll in BepInEx/plugins.BepInEx/config/com.paxton0505.localization/<LanguageCode>/.Localization.dll already bundles the ServerSync implementation used by this mod. You do not need to add a separate ServerSync DLL for this plugin.
Example structure:
BepInEx/
config/
com.paxton0505.localization/
English/
Common.json
Chinese_Trad/
MyMod.json
English is optional and can be used as a shared baseline.Chinese_Trad.Each JSON file contains two top-level arrays: exact and templates.
{
"exact": [
{
"match": "Item is favorited and won't be stored",
"replace": "物品已被收藏,不會被存放"
},
{
"match": "Slot is favorited and won't be stored",
"replace": "格位已被收藏,不會被存放"
}
],
"templates": [
{
"match": "Stored {{$total}} items from your inventory into nearby containers",
"replace": "已將 {{$total}} 個物品從你的背包存放到附近容器中"
},
{
"match": "<color=red>{{$itemName}} is not in nearby containers</color>",
"replace": "<color=red>附近容器中沒有 {{localize($itemName)}}</color>"
}
]
}
exact rules first try a full-string lookup, then fall back to whole-string longest-match exact replacement when the full string and templates do not match.templates match full displayed strings with placeholders such as {{$itemName}} or {{$count}}.OrdinalIgnoreCase.\n inside JSON strings for line breaks.replace value. Old tags from the intercepted source text are not re-applied automatically.localize() inside templatesYou can call {{localize($name)}} inside a template replacement.
When this happens, the plugin resolves the value in this order:
This is useful when a template contains an item name or label that should be translated through your existing exact rules.
Captured placeholder values come from the same tag-stripped input used for matching. Normalized exact and template matches re-apply only the outer tags immediately wrapping the matched visible span. Whole-string longest-match exact replacement keeps only the outermost tags of the full intercepted string.
The runtime can safely apply more than one translation pass to the same intercepted string.
This means you can use the optional English layer as a normalization step for hardcoded text from another language, then let the active language layer translate that normalized English text into the final target language.
Example with Chinese_Trad active:
BepInEx/config/com.paxton0505.localization/English/core.json
{
"exact": [
{
"match": "Packesel",
"replace": "Pack horse"
}
],
"templates": []
}
BepInEx/config/com.paxton0505.localization/Chinese_Trad/core.json
{
"exact": [
{
"match": "Pack horse",
"replace": "馱馬"
}
],
"templates": []
}
The displayed string can now flow as Packesel -> Pack horse -> 馱馬 in one UI interception pass.
Looped mappings are cut off safely, so the chain stops if rules start cycling.
If the intercepted text differs from a rule only by casing or by supported rich-text tags, normalized matching can still bridge it. For example, <color=orange>PACKHORSE</color> can bridge into PackHorse, then continue into the active-language translation while preserving the outer color tag around the translated result.
When a non-English language is active, the layers are applied from baseline to highest override in this order:
English files.English files from the server.If the active language is English, only the English layers are used.
BepInEx/config/com.paxton0505.localization.From v1.1.0, the dedicated server logs when hot reload is active and when a new snapshot is published. Example messages:
ServerSync source-of-truth enabled; local config hot reload is active.
Published local translation snapshot after debounced local config change: 2 language folder(s), 12 file(s).
localize().