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

FeatureVanilla VSAlegacy Nested
File layoutlang/ru.json (single file)lang/ru/*.json (any depth)
OrganizationAll keys in one fileSplit by topic, NPC, quest, etc.
Hot-reloadNoYes (InvalidateCaches)
FallbackNoneShort locale (ru-RU → ru)
Key prefixingOptionalAutomatic (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.json

The 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-desc in loaded lang/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 albase and alstory as 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

PatternExamplePurpose
quest-{id}-namequest-foxthief-nameQuest title in journal
quest-{id}-descquest-foxthief-descQuest description text
quest-{id}-objective{N}quest-foxthief-objective0Objective N label
quest-{id}-hoverquest-foxthief-hoverCustom hover tooltip
npc-{id}-{line}npc-innkeeper-greetingDialogue line
ui-{feature}-{text}ui-herald-welcomeGeneric UI text
item-{code}-nameitem-gear-swordCustom 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.Get is 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, then LocalizationUtils.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

SymptomCauseFix
Key shows as raw textMissing translation in JSONAdd the key to the correct lang/{language}/ file
Wrong language shownLocale changed but cache is oldCall InvalidateCaches()
File not loadingPath has wrong domain or language folderCheck folder name matches Lang.CurrentLocale (e.g., en or en-us)
Keys from vanilla game missingStrict domain mode enabledEnsure key domain is in NestedLocalizationDomains or use GetSafe instead of GetSafeStrictDomains
JSON parse errorUTF-8 BOM or trailing commaRemove 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}- or npc-{id}- to avoid collisions.
  • Keep English as fallback — provide en/ translations even if your primary language is different.
  • Test keys — enable Debug: true in mod config to see missing key warnings in logs.
  • Avoid shadowing — don't use vanilla game keys like game:item-stone in your nested files unless you really mean to override them.

Press ESC to close