Developer Reference

SimTools Translation Tutorial

A practical guide to adding and maintaining localised strings across all 9 language files.

How the system works

SimTools uses a custom LanguageManager that reads INI-style .lang files from the Languages/ folder. When the app starts it loads the file matching the user's chosen language, falling back to en.lang if a key is missing. Every user-visible string should go through one of two methods:

// Static text
LanguageManager.Get("Section", "Key", "Fallback English text")

// Text with runtime values inserted
LanguageManager.Format("Section", "Key", variable1, variable2)

The third argument to Get() is the fallback — shown if the key is absent in the loaded lang file. Always fill it with the English text so the app never shows a blank string.

📄 The lang file format

Files live at Languages/*.lang. The format is simple INI:

[SectionName]
Key=The translated value goes here
AnotherKey=Another value

[AnotherSection]
SomeKey=Value

Rules:

Encoding newlines

LanguageManager.Get() automatically converts the literal two-character sequence \n into a real line break. Write \n wherever you want a new line in the displayed text:

[MySection]
MyMessage=First line.\nSecond line.\n\nAfter a blank line.

That produces:

First line.
Second line.

After a blank line.
⚠ Do not use actual newlines inside a value. Lang file values must be on a single line. Use the literal characters \n (backslash + n) — never press Enter inside a value.

{} Format placeholders

LanguageManager.Format() uses standard .NET composite format — {0}, {1}, {2}, etc., in the order the arguments are passed:

// C# side
LanguageManager.Format("Paths", "Generic", gameName)
; en.lang
[Paths]
Generic=The {0} game directory has not been configured.\nPlease set it in Settings.

{0} is replaced with gameName at runtime. For multiple arguments:

// C# — three runtime values
LanguageManager.Format("GameplayFixes", "Done", downloaded, skipped, failed)
; en.lang
[GameplayFixes]
Done=Done.\n\nDownloaded: {0}  |  Already up-to-date: {1}  |  Failed: {2}

🪜 Step-by-step: adding a new string

  1. Pick or create a section

    Group related strings under a logical section. Reuse existing sections where it makes sense:

    SectionWhat belongs there
    [Settings]SettingsWindow dialogs
    [Updates]Update check messages
    [Paths]"Path not configured" messages
    [Tweaks]Tweak / patch install confirmations
    [BugFixes]Bug fix install confirmations
    [Framework]Mod framework install dialogs
    [ModResources]Simitone, RPC, SaveCleaner, etc.
    [Music]Music player window messages
    [GameplayFixes]Gameplay Fixes window messages
    [AboutSimTools]About window, warnings

    For something that doesn't fit, just make up a new section name.

  2. Add the key to en.lang first

    Open Languages/en.lang, find the right section (or append a new one at the bottom), and add your key:

    [MySection]
    MyNewKey=This is the English text.\nIt can span lines using \n.
    MyFormattedKey=Hello, {0}! You have {1} items.
    ✔ Best practice Always add to en.lang first. It is the authoritative reference and the fallback for all other languages.
  3. Replace the hardcoded string in C#

    Before:

    MessageBox.Show("This is the English text.", "My Title", ...);

    After:

    MessageBox.Show(
        LanguageManager.Get("MySection", "MyNewKey", "This is the English text."),
        LanguageManager.Get("MySection", "MyNewKey_Title", "My Title"),
        ...);

    For a formatted string:

    // Before
    MessageBox.Show($"Hello, {name}! You have {count} items.", "Title", ...);
    
    // After
    MessageBox.Show(
        LanguageManager.Format("MySection", "MyFormattedKey", name, count),
        LanguageManager.Get("MySection", "MyFormattedKey_Title", "Title"),
        ...);
  4. Add translations to the other 8 lang files

    Open each of de.lang, es.lang, fr.lang, pt.lang, ru.lang, ar.lang, ja.lang, zh.lang and add the same key with the translated value. You can append a duplicate section block at the bottom of each file — the parser merges it automatically:

    [MySection]
    MyNewKey=Translated text here.\nNewline works the same way.
    ℹ Missing keys fall back gracefully. If a key is absent in a translated file, the C# fallback string (third argument to Get()) is shown instead. The app never breaks — but the user sees English, so fill every language file.

🏷 Naming conventions

Follow the patterns already used in the codebase:

PatternUse forExample
ActionNameMessage bodyAlderLake_Info1
ActionName_TitleMessageBox titleAlderLake_Title
ActionName_AskYes/No confirmation bodySuppress_Ask
ActionName_TitleYes/No confirmation titleSuppress_Title
Descriptive nounStatus / label textNoTrack, Fetching, Complete
Same name, {0} in valueFormatted stringsBadVersion, Done

📋 Quick reference cheat sheet

C# usage patterns

// Static string
string msg = LanguageManager.Get("Section", "Key", "English fallback");

// One runtime value
string msg = LanguageManager.Format("Section", "Key", someVariable);

// Multiple runtime values
string msg = LanguageManager.Format("Section", "Key", val1, val2, val3);

// Full MessageBox pattern
MessageBox.Show(
    LanguageManager.Get("Section", "MyMessage", "English text here."),
    LanguageManager.Get("Section", "MyMessage_Title", "English Title"),
    MessageBoxButton.OK,
    MessageBoxImage.Warning);

Lang file patterns

; Static string
[Section]
MyMessage=English text here.
MyMessage_Title=English Title

; Multiline string
LongMessage=Line one.\nLine two.\n\nAfter blank line.

; Formatted string (runtime values)
LongFormatted=Failed to load {0}.\n\nError details:\n{1}\n\nPlease try again.
LongFormatted_Title=Load Error

💡 Tips & reminders