-
Notifications
You must be signed in to change notification settings - Fork 0
LocaleRegistry
Namespace: ScavLib.locale
Supplies translations for custom IDs without forking the game's
Lang/*.json files. LocaleRegistry patches Locale.GetItem,
Locale.GetBuilding, Locale.GetMoodle, and Locale.GetOther via Harmony
Postfixes and preserves the vanilla fallback chain:
current language → EN → bare id
If a key has no entry in the current language, ScavLib falls back to the
EN entry. If neither exists, the vanilla method's original return value
(usually the bare key) is left untouched.
using ScavLib.locale;
private void Awake()
{
LocaleRegistry.AddItem("mymod_salve", "EN", "Herbal Salve");
LocaleRegistry.AddItem("mymod_salve", "zh-CN", "草药膏");
LocaleRegistry.AddOther("mymod_salve_desc", "EN",
"A pungent paste that soothes burns and shallow cuts.");
LocaleRegistry.AddOther("mymod_salve_desc", "zh-CN",
"气味刺鼻的药膏,能舒缓烧伤与浅切口");
}After registration, Locale.GetItem("mymod_salve") returns the
language-appropriate name; in any language without an entry, it returns
the English fallback.
The game's locale system loads a single Language object per session and
exposes four lookup methods, each backed by a dictionary on
Locale.currentLang:
| Vanilla method | Backing dictionary | Type code |
|---|---|---|
Locale.GetItem(str) |
currentLang.main |
0 |
Locale.GetBuilding(str) |
currentLang.buildings |
1 |
Locale.GetMoodle(str) |
currentLang.moodles |
2 |
Locale.GetOther(str) |
currentLang.other |
3 |
A miss returns the bare key. ScavLib's Postfix runs after the vanilla
method, and only when the returned value equals the input key (i.e. the
vanilla lookup missed). At that point ScavLib consults its own table for
the current language; if that misses too, it falls back to EN; if that
misses, the bare key is returned unchanged.
The vanilla Locale.ChangeLanguage(str) call reloads the active scene.
ScavLib's table is a static dictionary that survives the reload, so
language switches are seamless for custom keys.
LocaleRegistry.AddItem(string key, string lang, string text);
LocaleRegistry.AddBuilding(string key, string lang, string text);
LocaleRegistry.AddMoodle(string key, string lang, string text);
LocaleRegistry.AddOther(string key, string lang, string text);| Parameter | Description |
|---|---|
key |
The ID the game will look up (item ID, building ID, moodle ID, or any custom string for Other). |
lang |
Language code matching the filename in Lang/, without the .json suffix — e.g. "EN", "zh-CN", "FR". Case-sensitive. |
text |
The translated string. |
Re-adding the same (key, lang) pair overwrites the previous value
silently — locale entries are intentionally last-writer-wins so a parent
mod can override a child's translation if both ship one.
The EN entry doubles as the fallback. Always register at least an
EN entry for every custom key; players running unsupported languages
will get the English string rather than the bare ID.
LocaleRegistry.AddItems (string lang, IDictionary<string, string> entries);
LocaleRegistry.AddBuildings(string lang, IDictionary<string, string> entries);
LocaleRegistry.AddMoodles (string lang, IDictionary<string, string> entries);
LocaleRegistry.AddOthers (string lang, IDictionary<string, string> entries);Convenient when shipping a JSON / Dictionary per language:
var enItems = new Dictionary<string, string>
{
["mymod_salve"] = "Herbal Salve",
["mymod_tonic"] = "Bitter Tonic",
["mymod_distill"] = "Distillation Apparatus",
};
LocaleRegistry.AddItems("EN", enItems);For mods shipping multiple languages, ScavLib provides a simple loader that reads the same JSON shape the game uses, scoped to your mod:
LocaleRegistry.LoadFromDirectory(string directoryPath);The loader scans directoryPath for *.json files. Each file's name
(without extension) is the language code; the file body is a JSON object
with up to four sections:
{
"main": {
"mymod_salve": "Herbal Salve",
"mymod_tonic": "Bitter Tonic"
},
"buildings": { },
"moodles": { },
"other": {
"mymod_salve_desc": "A pungent paste that soothes burns and shallow cuts."
}
}Typical layout next to your DLL:
BepInEx/plugins/
└── MyMod/
├── MyMod.dll
└── lang/
├── EN.json
└── zh-CN.json
private void Awake()
{
string dir = Path.Combine(Path.GetDirectoryName(Info.Location), "lang");
LocaleRegistry.LoadFromDirectory(dir);
}Malformed or missing files log a warning and are skipped; loading is best-effort and never throws into BepInEx's plugin loader.
CustomItemBuilder.DisplayName(...) / Description(...) and
CustomLiquidBuilder.DisplayName(...) are thin wrappers around this
system. Calling
CustomItemBuilder.Create("mymod_salve")
.DisplayName("Herbal Salve")
.Description("A pungent paste ...")
.Register();is equivalent to:
LocaleRegistry.AddItem ("mymod_salve", "EN", "Herbal Salve");
LocaleRegistry.AddOther("mymod_salve_desc", "EN", "A pungent paste ...");When you need more than EN (and the one-off lang overloads on the
Builders are not enough), drop down to LocaleRegistry directly or use
LoadFromDirectory.
LocaleRegistry.TryGet(int type, string lang, string key, out string value);
LocaleRegistry.GetOwner(string key); // last mod to write this key
LocaleRegistry.GetRegisteredLanguages(); // IReadOnlyCollection<string>type matches the vanilla type code (0=item, 1=building, 2=moodle,
3=other).
scavlib check reports the total number of registered locale entries per
mod and lists conflicts where two mods have written the same
(type, key, lang) triple.
- ScavLib does not localize the vanilla
notes/pdaNotes/pauseQuotes/charactercollections — those are list-shaped and pulled at random byLocale.GetNote,Locale.GetPdaNote, etc. Adding list-shaped overrides would change the game's RNG distribution and is out of scope. - Language codes are case-sensitive and must match the JSON filename used
by the game (
EN, noten). - If
Locale.LoadLanguage()fails to find a JSON for the currentcurrentLangName, the game initializes an emptyLanguageand logs an error. ScavLib's fallback chain still works in that state: missing current-language entries fall through toENand then to the bare key.