-
Notifications
You must be signed in to change notification settings - Fork 0
i18n
Namespace: ScavLib.i18n
i18n translation is still in the testing phase and may have issues such as being unable to translate or only translating certain parts. If you encounter this situation, you can open an issue in the DLL repository.
ScavLib's localization layer overlays mod-supplied translations on top of
the game's Locale system. Every custom string flows through the vanilla
Locale.GetItem / GetBuilding / GetMoodle / GetOther lookups, so the
game's fallback chain (current language → EN → bare id) is preserved.
The system is JSON-driven: ship one or more JSON files in your mod's
lang/ folder and call LocaleManager.AutoRegister once during Awake().
There is also a programmatic API for ad-hoc strings, which is what
CustomItemBuilder.DisplayName(...) / .Description(...) use under the
hood.
Replaces the older
ScavLib.locale.LocaleRegistry. See Migrating from LocaleRegistry at the bottom of this page for a side-by-side conversion table.
BepInEx/plugins/
YourMod.dll
lang/
EN.json
zh-CN.json
using ScavLib.i18n;
using System.IO;
private void Awake()
{
string pluginDir = Path.GetDirectoryName(Info.Location);
LocaleManager.AutoRegister("YourMod", pluginDir);
}AutoRegister loads EN.json first, then overlays the file matching the
active game language. With the ScavLib runtime hooks in place, switching
language at runtime via Locale.ChangeLanguage(...) automatically
re-syncs every registered custom item's fullName from the JSON — no
manual refresh required.
CustomItemBuilder.Create("yourmod_salve", "YourMod", ItemTemplate.Bandage)
.Category("medical")
.Register(out _);With EN.json containing "items": { "yourmod_salve": "Herbal Salve" },
the item's display name resolves correctly through Locale.GetItem from
that point on.
LocaleManager.AutoRegister uses the game's active language code, read
from Locale.currentLangName (falling back to
PlayerPrefs.GetString("locale", "EN")). Resolution order:
-
EN.json— always loaded if present, used as the base layer. -
<currentLang>.json— overlays on top of EN. - If
<currentLang>contains a region (e.g.zh-CN) and no exact match is found, ScavLib tries the short code (zh.json).
File name matching is case-insensitive, so EN.json, en.json, and
En.json all resolve to the EN layer.
You can call LocaleManager.GetGameLanguageCode() to inspect the resolved
code yourself.
A localization file is a single JSON object whose top-level keys map to
the corresponding Locale.currentLang dictionaries. Every section is
optional — include only what you need.
{
"items": {
"yourmod_salve": "Herbal Salve",
"yourmod_pistol": "Service Pistol"
},
"buildings": {
"yourmod_workbench": "Field Workbench"
},
"moodles": {
"yourmod_focused": "Focused"
},
"other": {
"yourmod_salve_desc": "A pungent paste that soothes burns and shallow cuts.",
"yourmod_ui.confirm": "Confirm",
"yourmod_ui.cancel": "Cancel"
},
"character": [
{
"greeting": ["Hello, friend.", "Took you long enough."],
"farewell": "See you out there."
}
],
"pauseQuotes": [
"Breathe.",
"Press on."
]
}| Top-level key | Target dictionary | Notes |
|---|---|---|
items |
Locale.currentLang.main |
Item display names. Reachable via Locale.GetItem(id). |
buildings |
Locale.currentLang.buildings |
Building names. |
moodles |
Locale.currentLang.moodles |
Moodle names. |
other |
Locale.currentLang.other |
Free-form strings. Reachable via Locale.GetOther(key). |
character |
Locale.currentLang.character |
Per-character dialog. Each entry is an object whose values may be a single string or an array of strings (see below). |
pauseQuotes |
Locale.currentLang.pauseQuotes |
List of pause-screen quotes. Existing quotes are preserved; new entries are appended (deduped). |
Nested objects under other are flattened into dotted keys. Both shapes
below produce the same lookup key yourmod_ui.confirm:
"other": {
"yourmod_ui.confirm": "Confirm"
}"other": {
"yourmod_ui": {
"confirm": "Confirm"
}
}Top-level objects whose key is not one of the reserved sections
(items, buildings, moodles, other, character, notes,
pauseQuotes, pdaNotes) are also flattened into other automatically:
{
"yourmod_status": {
"online": "Online",
"offline": "Offline"
}
}becomes yourmod_status.online / yourmod_status.offline in other.
Each entry in the character array corresponds to one slot in
Locale.currentLang.character. Values can be either a single string or an
array of strings:
"character": [
{
"greeting": ["Hello.", "Howdy."],
"farewell": "Take care."
}
]A bare string is wrapped into a one-element list, matching the runtime's
expected Dictionary<string, List<string>> shape.
pauseQuotes is treated as a list of strings; existing entries are
preserved and only new ones are appended. Duplicates (by exact string
match) are skipped.
You can register strings directly without shipping a JSON file. This is
what CustomItemBuilder.DisplayName(...) / .Description(...) use
internally.
LocaleManager.RegisterItem(
id: "yourmod_salve",
names: new Dictionary<string, string>
{
{ "EN", "Herbal Salve" },
{ "zh-CN", "草药膏" }
},
descs: new Dictionary<string, string>
{
{ "EN", "Soothes burns and shallow cuts." },
{ "zh-CN", "舒缓烧伤与浅创。" }
});
LocaleManager.RegisterString(
key: "yourmod_ui.confirm",
translations: new Dictionary<string, string>
{
{ "EN", "Confirm" },
{ "zh-CN", "确认" }
});| Method | Description |
|---|---|
LocaleManager.RegisterItem(id, names, descs = null) |
Register a custom item's display name and (optional) description. Description is stored under the key <id>dsc in other. |
LocaleManager.RegisterString(key, translations) |
Register an arbitrary other-table string (UI labels, system messages, etc.). |
LocaleManager.AutoRegister(modId, modFolder) |
Load <modFolder>/lang/EN.json plus the active-language file. |
LocaleManager.GetGameLanguageCode() |
The active game language code (Locale.currentLangName with a PlayerPrefs fallback). |
LocaleManager.RefreshAllUI() |
Re-pull every active UILocalizer text from Locale.GetOther(...). Useful after manually mutating Locale.currentLang.other. |
LocaleManager.RegisteredItemIds |
HashSet<string> of every item id that has gone through RegisterItem (or CustomItemBuilder.DisplayName). |
RegisterItem and RegisterString accept an IReadOnlyDictionary<string, string> keyed by language code. Resolution at runtime tries the active code, then EN, then English, then the first available value — pragmatic enough to never leave a custom string fully blank.
other-table entries can come from three places:
- JSON files loaded via
AutoRegister. - Strings registered via
RegisterString. - Manually written entries already in
Locale.currentLang.other.
When both layers define the same key, the JSON layer wins during the
Locale.LoadLanguage Postfix sweep, because manual injection runs first
and JSON injection runs last. If you need the programmatic value to win,
write it after the language load — e.g. inside an
IModLifecycle.OnEnabled() that runs after the first LoadLanguage.
ScavLib hooks three game entry points (in LocalePatches):
| Game method | What ScavLib does |
|---|---|
Locale.LoadLanguage (Postfix) |
Run the full JSON + manual injection; re-pull ItemInfo.fullName for every registered custom item from Locale.GetItem(id). |
Locale.ChangeLanguage (Postfix) |
Re-pull ItemInfo.fullName for every registered custom item. |
Item.SetupItems (Postfix) |
Re-pull ItemInfo.fullName for every registered custom item. |
Net effect: switching language in-game updates custom items immediately, and reloading items (e.g. across run boundaries) keeps display names in sync.
TranslationTemplateGenerator.ExportTemplate(path) writes a JSON template
listing every item id ScavLib has seen so far, with TODO: … placeholders
for the names. If the file already exists, the generator merges your
existing translations in so progress is preserved:
- Keys you already translated → kept verbatim.
- Keys present in the running game but missing from the file → added with
TODO: <id> Name. - Keys present in the file but no longer registered → kept (treated as a union, not a prune).
using ScavLib.i18n;
// In a debug command, menu button, or one-off Awake():
TranslationTemplateGenerator.ExportTemplate(
Path.Combine(BepInEx.Paths.ConfigPath, "yourmod_lang_template.json"));A typical workflow:
- Run the game once with your mod loaded so all items register.
- Trigger
ExportTemplateto write a template. - Translate the placeholders, save the file as
EN.json/zh-CN.jsonetc. insidelang/. - Re-running the generator on the same path keeps your translations and only fills in newly added ids.
LocaleManager.PerformInjection() is what LocalePatches calls during
Locale.LoadLanguage Postfix. You normally never need to call it
yourself, but it can be invoked manually if you mutate
JsonLocaleCache / programmatic registrations after the language has
already loaded and want them to take effect immediately.
After the call, LocaleManager.RefreshAllUI() re-pulls every active
UILocalizer text so on-screen labels reflect the new strings.
The old ScavLib.locale.LocaleRegistry API has been removed. The most
mechanical migration is to keep your strings in code and switch to
LocaleManager:
Old (ScavLib.locale) |
New (ScavLib.i18n) |
|---|---|
LocaleRegistry.AddItem(id, "EN", text) |
LocaleManager.RegisterItem(id, new Dictionary<string,string>{{"EN", text}}) |
LocaleRegistry.AddItem(id, "zh", text) |
Same RegisterItem call with "zh" (or "zh-CN") added to the dictionary. |
LocaleRegistry.AddOther(key, "EN", text) |
LocaleManager.RegisterString(key, new Dictionary<string,string>{{"EN", text}}) |
LocaleRegistry.AddItems("EN", dict) |
Either call RegisterItem per id, or move the entire payload into lang/EN.json under "items". |
LocaleRegistry.AddOthers("EN", dict) |
Same — call RegisterString per key, or move into lang/EN.json under "other". |
For any non-trivial amount of text, prefer the JSON path. Translators
do not need to touch C# code, and the TranslationTemplateGenerator keeps
the file synchronised with your registered ids over time.
CustomItemBuilder.DisplayName(...) and .Description(...) are unchanged
in shape — they used to forward to LocaleRegistry, they now forward to
LocaleManager. No call-site change is required.
- The JSON files are read at
AutoRegistertime. Hot-reloading a JSON file while the game is running is not supported; callAutoRegisteragain or restart the game. -
LocaleManager.RegisteredItemIdsonly tracks ids that went throughRegisterItemorCustomItemBuilder.DisplayName. JSON-only ids are applied toLocalecorrectly but do not show up inRegisteredItemIds—TranslationTemplateGeneratortherefore lists ids that have at least one programmatic display-name registration. If you want a JSON-only id in the template output, callRegisterItem(id, names: new Dictionary<string,string>())once at startup with an empty dictionary. -
pauseQuotesis appended on every load. If you want to replace vanilla quotes wholesale, do it afterLocale.LoadLanguageby writing toLocale.currentLang.pauseQuotesdirectly.