


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.translation_enabled runtime toggle in the local config.yaml.Ctrl + F1.

BepInExPack_Valheim, Jotunn, and JsonDotNET.Localization.dll in BepInEx/plugins.config.yaml, the plugin-localization files, and the language folders are created.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/
config.yaml
Localization-English.json
Localization-Chinese_Trad.json
English/
Common.json
Chinese_Trad/
MyMod.json
English is optional and can be used as a shared baseline.Chinese_Trad.Localization-*.json files are for the plugin's own overlay, status text, and fallback log strings.The plugin creates BepInEx/config/com.paxton0505.localization/config.yaml on startup if it is missing.
translation_enabled: true
debug: false
whole_string_longest_match_mode: per_word
whole_string_longest_match_min_length: 5
trace_feed_max_entries: 1024
devtools_enabled: true
devtools_toggle_shortcut: LeftControl + F1
Set translation_enabled: false to bypass the entire translation pipeline. While it is false, intercepted UI text is left unchanged and the runtime skips rule reloads until translation is enabled again.
Set debug: true to enable runtime debug mode.
whole_string_longest_match_mode controls whole-string longest-match exact replacement:
per_word: only replace spans that start and end on word boundaries.min_length: only replace spans whose visible letter/digit count is at least whole_string_longest_match_min_length.off: keeps the legacy unrestricted greedy behavior.When enabled, the log includes intercepted UI strings before translation, cache hits and miss-cache hits, the winning match stage, placeholder-resolution details, and per-file rule-load diagnostics.
trace_feed_max_entries controls how many unique entries the Trace page keeps before it evicts the oldest ones.
devtools_enabled turns the in-game overlay on or off.
devtools_toggle_shortcut controls the overlay hotkey. The config file uses raw key names such as LeftControl + F1, while the in-game UI shows the same shortcut as Ctrl + F1.
translation_enabled, debug, whole_string_longest_match_*, trace_feed_max_entries, and devtools_* all come from the local config.yaml. ServerSync does not override runtime settings.
On client builds, Localization includes an in-game devtools overlay that is enabled by default and opens with Ctrl + F1 in the UI (LeftControl + F1 in config.yaml).
Plugin exposes live toggles for translation, debug mode, whole-string longest-match settings, trace-feed capacity, and the overlay shortcut.Performance shows loaded-rule counts, cache sizes, hit rates, timings, and lets you reset metrics.Trace captures unique intercepted strings, lets you pause/search the feed, and shows per-stage translation analysis for the selected entry.Browser groups loaded local and server rules by source, language, and file name.Playground lets you draft exact/template rules, preview them against saved and loaded rules, and copy bare JSON.The overlay and plugin-owned labels are localized through the root-level Localization-*.json files, which hot-reload independently of the translation rule folders.
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}}, {{$count}}, or constrained placeholders like {{$count:int}}, {{$weight:float(int=1..3,frac=0..2)}}, {{$itemName:words(count=1..3)}}, and {{$mode:oneof(off|on|auto)}}.OrdinalIgnoreCase.match string. The replace string still uses {{$name}} and {{localize($name)}}.int and float accept optional leading + or - signs by default. float currently uses . as the decimal separator.where= with && clauses such as >0, >=0, <10, <=10, ==5, or !=0.\n inside JSON strings for line breaks.replace value. Old tags from the intercepted source text are not re-applied automatically.Example:
{
"templates": [
{
"match": "Missing {{$count:int(where=>0)}} x {{$itemName:words(count=1..3)}}",
"replace": "缺少 {{$count}} 個 {{localize($itemName)}}"
}
]
}
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.Localization-*.json changes also reload the plugin's own overlay/status strings and rebuild the overlay if it is open.config.yaml changes are also watched. Changing translation_enabled, debug, whole-string longest-match settings, trace-feed capacity, or the devtools toggle settings takes effect without restarting the game.Dedicated servers log 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).
translation_enabled: true in the local config.yaml for that client.debug: true in BepInEx/config/com.paxton0505.localization/config.yaml and then review the client log.devtools_enabled: true and confirm devtools_toggle_shortcut matches the key combination you expect.localize().If you want commissioned localization for a single mod or a full mod pack, for a private or public server, in any in-game language, contact me via Discord to discuss scope, availability, and details.
This workflow is AI-assisted, so current coverage is typically around 90% to 95%, not a guaranteed 100% translation pass.