


Localization is a BepInEx plugin for Valheim that translates hardcoded mod text at the final UI display layer.
TMPro.TMP_Text and UnityEngine.UI.Text.BepInEx/config/com.paxton0505.localization/<Language>/*.json.English/*.json files are merged into the fallback baseline.Chinese_Trad/*.json and overlays it on top of the English baseline.config/
com.paxton0505.localization/
English/
core.json
ui.json
Chinese_Trad/
core.json
ui.json
If the game language is Chinese_Trad, the runtime merges:
config/com.paxton0505.localization/English/*.jsonconfig/com.paxton0505.localization/Chinese_Trad/*.jsonThe active language rules override English rules with the same match key.
dotnet build -c Release
bin/Release/net48/Localization.dll into your Valheim BepInEx plugins folder.Example:
BepInEx/
plugins/
Localization.dll
BepInEx/
config/
com.paxton0505.localization/
English/
Chinese_Trad/
English and under the language folders you want to support.Create a file such as BepInEx/config/com.paxton0505.localization/English/core.json:
{
"exact": [
{
"match": "PackHorse",
"replace": "PackHorse"
}
],
"templates": [
{
"match": "Player {{$playername}} joined server",
"replace": "Player {{$playername}} joined server"
}
]
}
English is the fallback baseline. It should contain the source strings you want the runtime to recognize.
If the game language is Traditional Chinese, create a file such as BepInEx/config/com.paxton0505.localization/Chinese_Trad/core.json:
{
"exact": [
{
"match": "PackHorse",
"replace": "負重"
}
],
"templates": [
{
"match": "Player {{$playername}} joined server",
"replace": "{{$playername}} 進入了伺服器"
}
]
}
If you want a captured template parameter to be resolved through the game's own localization system, use {{localize($name)}} in the replacement string instead of {{$name}}.
Example:
{
"templates": [
{
"match": "Found {{$amount}} items matching '{{$item}}' in nearby containers.",
"replace": "已在附近的容器中找到 {{$amount}} 個 {{localize($item)}}"
}
]
}
When the game language is Chinese_Trad, the runtime loads all files from English/*.json, then all files from Chinese_Trad/*.json, and overlays the second set on top of the first.
You can split rules by topic.
Example:
config/
com.paxton0505.localization/
English/
items.json
chat.json
Chinese_Trad/
items.json
chat.json
All *.json files in the same language folder are merged automatically.
If two files in the same language folder define the same exact match text or the same template match text, the plugin logs an error and ignores the later file entry.
This means file ordering is deterministic, but duplicates are not used as an override mechanism inside the same language folder.
The plugin reloads rules when the game writes a new language value into PlayerPrefs.
This plugin only changes strings that reach the final UI text layer through TMPro.TMP_Text or UnityEngine.UI.Text.
That is why it can catch many hardcoded mod strings, but it also means:
Each JSON file can contain exact-match rules and template rules.
{
"exact": [
{
"match": "PackHorse",
"replace": "負重"
}
],
"templates": [
{
"match": "Player {{$playername}} joined server",
"replace": "{{$playername}} 進入了伺服器"
}
]
}
match must be a full exact string match.replace is the translated result.{{$name}} tokens for captured parameters.{{localize($name)}} tokens when the captured parameter should be resolved through the game's own localization or item-name lookup._, and must start with a letter or _.{{$name}} and function tokens {{localize($name)}}.localize(...) currently supports exactly one placeholder argument.{{...}} is reserved syntax inside template rules. If the source text itself literally contains {{$item}} or another token-shaped fragment, do not model it as a template rule.
Use an exact rule instead.
Example: a mod literally shows Use {{$item}} to continue on screen, and you only want to translate the sentence, not treat {{$item}} as a captured parameter.
{
"exact": [
{
"match": "Use {{$item}} to continue",
"replace": "使用 {{$item}} 繼續"
}
]
}
That avoids a syntax collision because exact rules do not parse token syntax.
If the text is actually dynamic, keep the variable part only where it is really needed.
Good:
{
"templates": [
{
"match": "Found {{$amount}} items matching '{{$item}}' in nearby containers.",
"replace": "已在附近的容器中找到 {{$amount}} 個 {{localize($item)}}"
}
]
}
This keeps {{amount}} and {{item}} as explicit parameters, while the surrounding sentence stays fixed and unambiguous.
PlayerPrefs("language") update path instead of per-frame or timed polling.This plugin uses Harmony because hardcoded mod text usually bypasses normal localization systems.
If a mod directly does one of these:
tmpText.text = "PackHorse"tmpText.SetText("Player Bob joined server")uiText.text = "Some hardcoded text"there is no clean configuration hook or official API callback to intercept that text before display.
Harmony solves that by patching the final text assignment methods at runtime.
harmony.PatchAll();harmony.PatchAll(); scans this plugin assembly for classes marked with HarmonyPatch and applies all of them during startup.
In this project that is important for two reasons:
TextInterceptionPatches, not inside PluginMain.In practice, PatchAll() gives you one startup line that says: load every patch declared in this plugin assembly.
That is simpler and safer than manually calling Harmony patch methods one by one.
For hardcoded mod text, avoiding Harmony usually means you miss the real interception point.
The whole point of this plugin is to catch text at the bottom layer right before it is shown. For that, runtime method patching is the practical tool:
src/PluginMain.cs: plugin bootstrap and Harmony initialization.src/TranslationRuntime.cs: active language tracking, caches, and translation entry point.src/TranslationRuleLoader.cs: folder-based JSON loading and merge logic.src/TextInterceptionPatches.cs: final UI text interception hooks.src/CompiledTemplateRule.cs: safe placeholder-template matcher.Requirements:
Localization.csprojLocalization.csprojBuild command:
dotnet build -c Release
The resulting plugin assembly is written to bin/Release/net48/Localization.dll.