Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Localization
Runtime translation/localization helper for Valheim mods.
| Last updated | 4 days ago |
| Total downloads | 269 |
| Total rating | 0 |
| Categories | Language Utility AI Generated |
| Dependency string | Paxton0505-Localization-2.0.1 |
| Dependants | 0 other packages depend on this package |
This mod requires the following mods to function
ValheimModding-JsonDotNET
Shared version 13.0.3 of Json.NET from Newtonsoft, net45 package for use in Valheim mods. Maintained by the ValheimModding team.
Preferred version: 13.0.4denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2333ValheimModding-Jotunn
Jötunn (/ˈjɔːtʊn/, 'giant'), the Valheim Library was created with the goal of making the lives of mod developers easier. It enables you to create mods for Valheim using an abstracted API so you can focus on the actual content creation.
Preferred version: 2.29.0README
Localization
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.
What this mod does
- Intercepts final UI text assignments for
TMPro.TMP_Text.text,TMPro.TMP_Text.SetText(string, bool), andUnityEngine.UI.Text.text. - Applies
exactrules for full-string matches andtemplatesrules for placeholder-based matches. - Re-runs rewritten text through the rule engine so an English baseline alias can normalize non-English hardcoded text before the active language translation is applied.
- Strips supported rich-text tags from intercepted input before matching and compares exact rules with
OrdinalIgnoreCase. - Loads rule files from
BepInEx/config/com.paxton0505.localization/<LanguageCode>/*.json. - Uses
English/*.jsonas an optional shared baseline. - Reloads rules when the active game language changes.
- Supports a top-level
translation_enabledruntime toggle in the localconfig.yaml. - Includes an in-game devtools overlay for live trace, browser, performance, and playground workflows on clients, enabled by default with
Ctrl + F1. - Supports session-only ServerSync overlays when both server and client run the mod.
- Keeps server-synced rules in memory only and does not write them into the client's local JSON files.
What this mod does not do
- It does not machine-translate text for you.
- It does not modify the source files of other mods.
- It does not save server overlays to disk after the session ends.
- On a dedicated server it does not run local UI translation patches, because there is no local UI to patch.
Before / After


Install
- Install the mod from Thunderstore. The declared dependencies are installed automatically:
BepInExPack_Valheim,Jotunn, andJsonDotNET. - If you install manually, place
Localization.dllinBepInEx/plugins. - Launch the game once so
config.yaml, the plugin-localization files, and the language folders are created. - Create one or more rule files under
BepInEx/config/com.paxton0505.localization/<LanguageCode>/. - If you want server-provided overlays, install the mod on both server and clients.
Localization.dll already bundles the ServerSync implementation used by this mod. You do not need to add a separate ServerSync DLL for this plugin.
Folder layout
Example structure:
BepInEx/
config/
com.paxton0505.localization/
config.yaml
Localization-English.json
Localization-Chinese_Trad.json
English/
Common.json
Chinese_Trad/
MyMod.json
Englishis optional and can be used as a shared baseline.- The active language folder should match the game's language name, for example
Chinese_Trad. - Files inside each language folder are loaded in alphabetical order by file name.
Localization-*.jsonfiles are for the plugin's own overlay, status text, and fallback log strings.
Optional runtime settings
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 leastwhole_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.
Built-in devtools overlay
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).
Pluginexposes live toggles for translation, debug mode, whole-string longest-match settings, trace-feed capacity, and the overlay shortcut.Performanceshows loaded-rule counts, cache sizes, hit rates, timings, and lets you reset metrics.Tracecaptures unique intercepted strings, lets you pause/search the feed, and shows per-stage translation analysis for the selected entry.Browsergroups loaded local and server rules by source, language, and file name.Playgroundlets 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.
Rule file format
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>"
}
]
}
Rule behavior
exactrules 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.templatesmatch 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)}}.- Before matching, the plugin strips supported rich-text tags from the intercepted input.
- Exact rules compare against normalized input with
OrdinalIgnoreCase. - Whole-string longest-match exact replacement collects matches across the whole normalized string, prefers the longest non-overlapping exact matches first, applies them, and then repeats until no exact matches remain.
- Template literal segments also compare against normalized input case-insensitively.
- If a rule rewrites the full string into another matchable string, the plugin evaluates the rewritten result again until it stabilizes.
- Template literal matching is exact on punctuation, spacing, and line breaks after tag stripping.
- Constrained placeholders only work in the template
matchstring. Thereplacestring still uses{{$name}}and{{localize($name)}}. intandfloataccept optional leading+or-signs by default.floatcurrently uses.as the decimal separator.- Numeric predicate syntax uses
where=with&&clauses such as>0,>=0,<10,<=10,==5, or!=0. - Use
\ninside JSON strings for line breaks. - The displayed output comes from the rule
replacevalue. Old tags from the intercepted source text are not re-applied automatically. - If no rule matches, the original text is left unchanged.
Example:
{
"templates": [
{
"match": "Missing {{$count:int(where=>0)}} x {{$itemName:words(count=1..3)}}",
"replace": "缺少 {{$count}} 個 {{localize($itemName)}}"
}
]
}
localize() inside templates
You can call {{localize($name)}} inside a template replacement.
When this happens, the plugin resolves the value in this order:
- Normalized exact rules loaded by this plugin, including chained exact aliases.
- Valheim's own localization table.
- The rewritten result is checked against the plugin's normalized exact rules again.
- The original value if nothing matches.
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.
Chained normalization through English
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.
Load order and precedence
When a non-English language is active, the layers are applied from baseline to highest override in this order:
- Local
Englishfiles. - Synced
Englishfiles from the server. - Local active-language files.
- Synced active-language files from the server.
If the active language is English, only the English layers are used.
Client and dedicated server behavior
Client
- Applies runtime translation to intercepted UI text.
- Reloads compiled rules when the game language changes.
- Applies synced server snapshots immediately when ServerSync values change.
Dedicated server
- Runs in sync-only mode.
- Initializes ServerSync and watches the local config folder.
- Does not run the local translation runtime or UI Harmony patches.
- Publishes local rule changes to connected clients when it is the ServerSync source of truth.
Hot reload
- Local config changes are watched under
BepInEx/config/com.paxton0505.localization. - Local translation JSON changes reload the compiled rules for the current language and clear translation caches while translation is enabled.
- Root-level
Localization-*.jsonchanges also reload the plugin's own overlay/status strings and rebuild the overlay if it is open. config.yamlchanges are also watched. Changingtranslation_enabled,debug, whole-string longest-match settings, trace-feed capacity, or the devtools toggle settings takes effect without restarting the game.- On the source-of-truth side, repeated file events are debounced for about 500ms before a snapshot is published.
- On the receiving side, synced rule snapshots are applied when they arrive; that path is not debounced by the file watcher.
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).
Troubleshooting
- If a translation does not appear, first verify that the folder name matches the current in-game language.
- If a translation does not appear at all, verify
translation_enabled: truein the localconfig.yamlfor that client. - If you need to inspect the raw intercepted string before translation, set
debug: trueinBepInEx/config/com.paxton0505.localization/config.yamland then review the client log. - If the devtools overlay does not open, verify
devtools_enabled: trueand confirmdevtools_toggle_shortcutmatches the key combination you expect. - If a template does not match, compare the rendered text after removing supported rich-text tags, then check spacing, punctuation, and line breaks.
- If you expect a server overlay, make sure both server and client have this mod installed.
- Invalid JSON files or invalid template rules are logged and skipped.
- The client log will also report how many exact rules and templates were loaded for the active language.
Good use cases
- Translating hardcoded UI strings from other mods.
- Sharing a server-side translation overlay with clients for the current session.
- Reusing exact-rule translations inside templates with
localize().
Custom Localization
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.