Skip to content

LocaleRegistry

QinShenYu edited this page Jun 10, 2026 · 3 revisions

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.


Quick Start

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.


How It Works

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.


Adding Entries — Single Key

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.


Adding Entries — Bulk

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);

Mod-Lang JSON Loader

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.


Interaction With Builder Shortcuts

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.


Querying & Diagnostics

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.


Notes & Limitations

  • ScavLib does not localize the vanilla notes / pdaNotes / pauseQuotes / character collections — those are list-shaped and pulled at random by Locale.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, not en).
  • If Locale.LoadLanguage() fails to find a JSON for the current currentLangName, the game initializes an empty Language and logs an error. ScavLib's fallback chain still works in that state: missing current-language entries fall through to EN and then to the bare key.

Clone this wiki locally