⚙ 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:
- Section headers:
[SectionName]— exact match, case-sensitive. - Keys:
KeyName=Value— no spaces around=. - Blank lines between sections are fine and encouraged for readability.
- The parser merges duplicate section headers — if
[Settings]appears twice the keys from both blocks are combined. This means you can safely append a new[Settings]block at the bottom of a file rather than hunting for the existing one. - File encoding: UTF-8 without BOM on all 9 files.
↵ 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.
\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
-
Pick or create a section
Group related strings under a logical section. Reuse existing sections where it makes sense:
Section What 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.
-
Add the key to
en.langfirstOpen
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 toen.langfirst. It is the authoritative reference and the fallback for all other languages. -
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"), ...); -
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.langand 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 toGet()) 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:
| Pattern | Use for | Example |
|---|---|---|
ActionName | Message body | AlderLake_Info1 |
ActionName_Title | MessageBox title | AlderLake_Title |
ActionName_Ask | Yes/No confirmation body | Suppress_Ask |
ActionName_Title | Yes/No confirmation title | Suppress_Title |
Descriptive noun | Status / label text | NoTrack, Fetching, Complete |
Same name, {0} in value | Formatted strings | BadVersion, 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
- Always add to
en.langfirst — it is the authoritative reference and fallback for all other languages. - The fallback argument matters — if you forget to add a key to a translated file, the C# fallback string shows instead of a blank, so always make it the real English text.
- Duplicate section headers are safe — append a new
[SectionName]block at the bottom of any file; the parser merges all keys under the same name. - Never use actual newlines inside a value — use the two-character sequence
\ninstead. - Don't leave orphan keys — if you remove a string from C# code, remove its lang file entries too.
- Test with a non-English language — set the app to German or Arabic and walk through the feature to confirm nothing shows in English that shouldn't.
- UTF-8 without BOM — save all
.langfiles with UTF-8 encoding and no byte-order mark. Most editors (VS Code, Notepad++) have this as an explicit save option.