Localization
Alegacy Quest Framework uses a custom nested file localization system that extends Vintage Story's default lang/*.json flat file approach. This allows you to organize translations into subfolders, making large quest packs manageable.
Quick Comparison
| Feature | Vanilla VS | Alegacy Nested |
|---|---|---|
| File layout | lang/ru.json (single file) | lang/ru/*.json (any depth) |
| Organization | All keys in one file | Split by topic, NPC, quest, etc. |
| Hot-reload | No | Yes (InvalidateCaches) |
| Fallback | None | Short locale (ru-RU → ru) |
| Key prefixing | Optional | Automatic (domain:key) |
Folder Structure
Instead of dumping everything into one file, split translations by topic:
assets/
albase/
lang/
en/ ← language folder
core.json ← shared UI text
entities/
creatures.json
items/
gear.json
rewards.json
npc/
innkeeper.json
bosshunter.json
quests/
bosshunt/
intro.json
ashenlurker.json
innkeeper/
firstimpression.json
foxthief.json
reputation/
innkeeper.json
ui/
herald.json
quiz.jsonThe framework recursively scans lang/{language}/ and merges every .json file it finds into a single in-memory dictionary.
How It Works
Loading Flow
graph TD
A[Game starts or locale changes] --> B[LoadNestedLanguageFiles]
B --> C{Scan each mod domain}
C --> D[Try flat file: lang/ru.json]
C --> E[Try nested: lang/ru/*.json]
C --> F[Try recursive: GetLocations]
D --> G{Any files found?}
E --> G
F --> G
G -->|No| H[Fallback to short locale: ru]
G -->|Yes| I[Parse JSON & merge keys]
I --> J[Cache: domain:key and key]
J --> K[Ready for GetSafe / GetFromNested]Key Resolution Priority
When you call LocalizationUtils.GetSafe("albase:quest-foxthief-desc"):
- Nested cache — checks
albase:quest-foxthief-descin loadedlang/ru/files. - Vanilla
Lang.Get— falls back to Vintage Story's native localization. - Return raw key — if nothing found (so you see what key is missing).
graph LR
A[GetSafe langKey] --> B{Nested cache hit?}
B -->|Yes| C[Return cached value]
B -->|No| D[Try Lang.Get]
D -->|Found| E[Return vanilla translation]
D -->|Not found| F[Return raw langKey]Configuration
Set your domains in the mod config (modconfig/alegacyvsquest.json):
{
"NestedLocalizationDomains": [
"alegacyvsquest",
"albase",
"alstory",
"yourmod"
]
}- Only domains listed here are scanned for nested files.
- The framework automatically scans all loaded mod domains plus
albaseandalstoryas a safety net.
Translation File Format
Each .json file is a flat key→value map. Keys starting with _ are ignored (use them for comments).
{
"_note": "Innkeeper quest translations",
"quest-foxthief-name": "The Fox Thief",
"quest-foxthief-desc": "Someone keeps stealing mugs from the tavern. Find the culprit.",
"quest-foxthief-objective0": "Talk to the innkeeper",
"quest-foxthief-objective1": "Search the cellar",
"quest-foxthief-objective2": "Catch the fox",
"npc-innkeeper-greeting": "Welcome, traveler! Care for a drink?",
"npc-innkeeper-hint": "I heard noises downstairs at night..."
}Key Naming Conventions
| Pattern | Example | Purpose |
|---|---|---|
quest-{id}-name | quest-foxthief-name | Quest title in journal |
quest-{id}-desc | quest-foxthief-desc | Quest description text |
quest-{id}-objective{N} | quest-foxthief-objective0 | Objective N label |
quest-{id}-hover | quest-foxthief-hover | Custom hover tooltip |
npc-{id}-{line} | npc-innkeeper-greeting | Dialogue line |
ui-{feature}-{text} | ui-herald-welcome | Generic UI text |
item-{code}-name | item-gear-sword | Custom item display name |
API Methods
GetSafe(string langKey)
The most common method. Checks nested files first, then vanilla.
string title = LocalizationUtils.GetSafe("albase:quest-foxthief-name");
// Returns "The Fox Thief" if found, otherwise the raw key.GetSafe(string langKey, params object[] args)
With string formatting support:
string msg = LocalizationUtils.GetSafe("albase:quest-kill-count", 5, 10);
// JSON: "quest-kill-count": "Killed {0} of {1}"
// Result: "Killed 5 of 10"GetFromNested(string langKey)
Only checks the custom nested cache (no vanilla fallback). Returns null if missing.
string custom = LocalizationUtils.GetFromNested("albase:quest-foxthief-name");GetSafeStrictDomains(string langKey)
Used internally for strict separation:
- If the key's domain is in
NestedLocalizationDomains→ only nested files are searched. - Otherwise → only vanilla
Lang.Getis used.
This prevents accidental shadowing of vanilla game keys.
ResolveKeyOrText(string value)
Handy for fields that accept either a raw string or a localization key:
// If value contains ':', treat as lang key; otherwise return as-is.
string text = LocalizationUtils.ResolveKeyOrText("albase:quest-foxthief-desc");
string raw = LocalizationUtils.ResolveKeyOrText("Hello world!");Mob Display Names
For creatures, the framework uses MobLocalizationUtils.GetMobDisplayName(code) with a 4-step priority:
graph TD
A[GetMobDisplayName] --> B{mobdisplaynames.json exact?}
B -->|Yes| C[Return override]
B -->|No| D{Game localization exact?}
D -->|Yes| E[Return game name]
D -->|No| F{mobdisplaynames.json prefix?}
F -->|Yes| G[Return override]
F -->|No| H{Game wildcard localization?}
H -->|Yes| I[Return wildcard name]
H -->|No| J[Pretty-print code]Override file location: config/mobdisplaynames.json inside any domain.
{
"albase:creature-foxthief": "Mug-Stealing Fox",
"creature-foxthief": "Fox"
}Quest Progress Text
QuestProgressTextUtil automatically builds live quest text:
- Resolves item names via
GetItemDisplayName(checks item registry, block registry, action items, thenLocalizationUtils.GetSafe). - Injects current progress values into objective templates.
- Adds stage info for multi-stage quests.
Example quest description in JSON:
{
"quest-foxthief-desc": "Catch the thief who stole {0} mugs."
}At runtime, {0} is replaced with the live count from the active quest.
Hot Reload
If you need to reload translations without restarting the server:
LocalizationUtils.InvalidateCaches();
LocalizationUtils.LoadNestedLanguageFiles(api);This clears all caches and forces a full re-scan on the next access.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Key shows as raw text | Missing translation in JSON | Add the key to the correct lang/{language}/ file |
| Wrong language shown | Locale changed but cache is old | Call InvalidateCaches() |
| File not loading | Path has wrong domain or language folder | Check folder name matches Lang.CurrentLocale (e.g., en or en-us) |
| Keys from vanilla game missing | Strict domain mode enabled | Ensure key domain is in NestedLocalizationDomains or use GetSafe instead of GetSafeStrictDomains |
| JSON parse error | UTF-8 BOM or trailing comma | Remove BOM (save as UTF-8 without BOM), check JSON validity |
Best Practices
- Use subfolders — split by feature (
npc/,quests/,ui/) rather than one giant file. - Name keys consistently — always prefix with
quest-{id}-ornpc-{id}-to avoid collisions. - Keep English as fallback — provide
en/translations even if your primary language is different. - Test keys — enable
Debug: truein mod config to see missing key warnings in logs. - Avoid shadowing — don't use vanilla game keys like
game:item-stonein your nested files unless you really mean to override them.