Gum gum-localization
Reference guide for Gum's localization system — ILocalizationService, CSV/RESX loading in both the tool and runtime, Text vs TextNoTranslate paths, Forms control localization patterns, and gotchas.
git clone https://github.com/vchelaru/Gum
T=$(mktemp -d) && git clone --depth=1 https://github.com/vchelaru/Gum "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/gum-localization" ~/.claude/skills/vchelaru-gum-gum-localization && rm -rf "$T"
.claude/skills/gum-localization/SKILL.mdGum Localization
Architecture Overview
Localization is opt-in via a nullable static property. When set, text assigned through the
"Text" property name is translated; text assigned through "TextNoTranslate" bypasses translation entirely.
Entry point:
CustomSetPropertyOnRenderable.LocalizationService (static, nullable ILocalizationService?)
Default initialization:
SystemManagers lazily creates a LocalizationService instance using ??=, so assigning your own service before initialization preserves it.
Access at runtime:
GumService.Default.LocalizationService forwards to the static property above.
ILocalizationService
GumCommon/Localization/ILocalizationService.cs — five members:
(int) — index into the translation arrays (0 = default/source language)CurrentLanguage
(Languages
) — language names populated after loading; empty until a database is loadedIReadOnlyList<string>
— loads translations; key = string ID, value = array whereAddDatabase(Dictionary<string, string[]>, List<string>)
is the ID and[0]
are translations per language[1..N]
— resets the database and Languages listClear()
— returns the translated string forTranslate(string stringId)CurrentLanguage
LocalizationService (default implementation)
GumCommon/Localization/LocalizationService.cs
Translation logic in
TranslateForLanguage:
- If database is empty → return string as-is (no translation, no suffix)
- If string ID is found → return
mStringDatabase[stringId][language] - If string has no letters (numbers/punctuation/whitespace only) → return as-is (excluded from translation)
- Otherwise → return
— the "(loc)" suffix signals a missing translation keystringId + "(loc)"
Loading Data — LocalizationServiceExtensions
GumCommon/Localization/LocalizationServiceExtensions.cs — extension methods on ILocalizationService:
CSV:
AddCsvDatabase(Stream) — uses CsvHelper. First column = string ID, subsequent columns = translations. First row = language headers. Languages list populated from header row.
RESX: Four overloads — single or multi, path-based or stream-based. All accept an optional
Action<string> onWarning callback (used on cross-file key collisions; runtime never logs on its own).
— single base file, auto-discovers satellites (AddResxDatabase(string baseResxFilePath)
+Strings.resx
,Strings.es.resx
). Base labeledStrings.fr.resx
; satellites use their culture code."Default"
— multi-file. Merges keys across all base files. Language set is the union; missing keys fall back to the string ID. Collision policy: last-write-wins;AddResxDatabase(IEnumerable<string> baseResxFilePaths, Action<string> onWarning = null)
fires once per colliding key and names all prior sources.onWarning
— single-file stream variant for mobile/web.AddResxDatabase(IEnumerable<(string languageName, Stream stream)>)
— multi-group stream variant with explicit group names used in collision warnings.AddResxDatabase(IEnumerable<(string? groupName, IEnumerable<(string languageName, Stream stream)>)> fileGroups, Action<string> onWarning = null)
All formats produce the same internal structure:
Dictionary<string, string[]> where index 0 = string ID, 1+ = per-language translations.
Gum Tool Localization Support
The tool stores
LocalizationFiles — a List<string> of project-relative paths — on GumProjectSave. A legacy single-string LocalizationFile property is kept as a back-compat serialization shim (reads/writes index 0) so .gumx files written by the new tool can still be partially loaded by older tool versions. See gum-project-versioning skill for why no version bump was needed.
Policy in
:FileCommands.LoadLocalizationFile()
- 0 paths → no-op.
- 1 RESX or multiple RESX → routed through the multi-file
overload.AddResxDatabase(IEnumerable<string>, onWarning)
is wired toonWarning
so collisions appear in the Output tab.IOutputManager.AddOutput - 1 CSV → single-file CSV path.
- Mixed CSV+RESX or multiple CSVs →
and skip (no multi-CSV overload by design;AddError
replaces rather than merges).AddDatabase
UI:
ProjectPropertiesViewModel exposes LocalizationFiles with PreferredDisplayer = typeof(MultiFileDisplay) — a list editor with Add/Remove/Up/Down buttons that composes FilePickingLogic.
Runtime auto-load:
GumService.InitializeInternal applies the same policy and exposes collision warnings on GumService.Default.LastLoadResult.Warnings (no Output tab available in games).
File watching:
FileChangeReactionLogic.IsLocalizationFileThatShouldTriggerReload(changedFile, IEnumerable<FilePath> baseFiles) returns true if the changed file matches any base path in the list OR any base's satellite ({BaseName}.*.resx in the same directory). A single-file overload is preserved as the inner loop body.
Language dropdown: After loading,
ILocalizationService.Languages is populated. ProjectPropertiesViewModel.LanguageName (string) replaces the raw LanguageIndex int in the UI. The plugin syncs LanguageName ↔ LanguageIndex via IFileCommands.LocalizationLoaded event (fired at the end of every LoadLocalizationFile() call).
Variable grid refresh:
LoadLocalizationFile() calls _guiCommands.RefreshVariables() at the end, so the Text property displayer updates from plain textbox to localization combo box without requiring re-selection.
Translation Flow in CustomSetPropertyOnRenderable
Gum/Wireframe/CustomSetPropertyOnRenderable.cs, TrySetPropertyOnText method:
When
SetProperty is called with property name "Text" or "TextNoTranslate":
- If the raw value contains
→ treated as BBCode markup, applied directly (stored as[
)StoredMarkupText - If property is
AND"Text"
→LocalizationService != nullrawText = LocalizationService.Translate(rawText) - If the translated result contains
→ treated as BBCode (translation can produce BBCode)[ - If property is
→ no translation call, value used as-is"TextNoTranslate"
Key detail: BBCode in the original string is checked first (step 1). If there's no BBCode in the original, translation runs, then BBCode is checked again on the result (step 3). This means a translated value can contain BBCode markup even if the string ID didn't.
TextRuntime
MonoGameGum/GueDeriving/TextRuntime.cs:
property (get/set) — callsText
→ goes through localizationSetProperty("Text", value)
method — callsSetTextNoTranslate(string?)
→ bypasses localizationSetProperty("TextNoTranslate", value)
SetTextNoTranslate is a method, not a property, because the underlying renderable only stores the final string — there's no way to distinguish translated from untranslated text after assignment, so a getter would be misleading.
Forms Controls Pattern
All Forms controls with displayable text follow the same pattern:
| Control | Localized property | No-translate method |
|---|---|---|
| Button | | |
| Label | | |
| CheckBox | | |
| RadioButton | | |
| TextBox | | |
| TextBoxBase | | |
| MenuItem | | |
Internally, all no-translate methods call
SetProperty("TextNoTranslate", value) on the underlying text component.
Data-Driven Controls — Intentionally No Localization
ComboBox —
Text property sets coreTextObject.RawText directly (bypasses SetProperty entirely). This is because ComboBox text comes from SelectedItem.ToString(), which is data-driven.
ListBoxItem —
UpdateToObject(object o) sets coreText.RawText = o?.ToString() directly. Same reason: items come from a data collection.
To localize data-driven controls, pre-translate values before adding them to the
Items collection.
TextBox and PasswordBox — User Input
TextBox internally uses
SetTextNoTranslate for all user-initiated editing: typing (HandleCharEntered), pasting, and deleting. This prevents accidental translation of user-typed content.
PasswordBox uses
TextNoTranslate for mask characters (e.g., "●●●●") since those should never be translated.
Gotchas
-
"(loc)" suffix is intentional — When a database is loaded but a string ID isn't found,
appends "(loc)". This is a debugging feature, not a bug. Empty databases return strings unchanged (no suffix).Translate() -
Translation happens at assignment time, not read time — The renderable stores only the final translated string. Changing
after setting text does NOT retroactively update existing UI. You must re-assignCurrentLanguage
to all controls.Text -
Null service = no localization — If
is null, all text passes through unchanged. This is the expected state when localization isn't needed.LocalizationService -
BBCode interaction — If the original string contains
, BBCode is parsed before translation (and translation is skipped for that value). If the original has no BBCode but the translated result does, BBCode is parsed on the translated result. Be careful: a string ID with[
in it won't be translated.[ -
CurrentLanguage is a raw array index — No bounds checking. Index 0 in the translation array is the string ID itself (not a translation). Actual translations start at index 1. Setting
returns the string ID.CurrentLanguage = 0 -
RESX satellite ordering and naming — Satellites are sorted alphabetically by file path, so
comes beforede
comes beforees
. The base file is always first and labeledfr
. If you need a specific order or names, use the stream-based overload."Default" -
ShouldExcludeFromTranslation — Strings with no letters (pure numbers, punctuation, whitespace, or empty) are silently excluded from translation and returned as-is, with no "(loc)" suffix. This prevents false positives on numeric display values.
Key Files
— interface (GumCommon/Localization/ILocalizationService.cs
,CurrentLanguage
,Languages
,AddDatabase
,Clear
)Translate
— default implementationGumCommon/Localization/LocalizationService.cs
— CSV/RESX loadersGumCommon/Localization/LocalizationServiceExtensions.cs
— staticGum/Wireframe/CustomSetPropertyOnRenderable.cs
property and translation logic inLocalizationServiceTrySetPropertyOnText
—Gum/Commands/FileCommands.cs
(CSV/RESX branch,LoadLocalizationFile()
event)LocalizationLoaded
—Gum/Commands/IFileCommands.cs
event declarationLocalizationLoaded
—Gum/Managers/FileChangeReactionLogic.cs
(list + satellite matching)IsLocalizationFileThatShouldTriggerReload()
— Language dropdown +Gum/Plugins/InternalPlugins/ProjectPropertiesWindowPlugin/
list editor UILocalizationFiles
—WpfDataUi/Controls/MultiFileDisplay.xaml(.cs)
control forIDataUi
file-path lists; composesList<string>FilePickingLogic
— shared file-dialog/relative-path plumbing (pattern likeWpfDataUi/Controls/FilePickingLogic.cs
)TextBoxDisplayLogic
— runtime auto-load ofMonoGameGum/GumService.cs.gumx
; collision warnings surface onLocalizationFilesGumLoadResult.Warnings
—MonoGameGum/GueDeriving/TextRuntime.cs
property andText
methodSetTextNoTranslate
— Forms control localization patternMonoGameGum/Forms/Controls/
— CSV/RESX loader testsMonoGameGum.Tests/Localization/LocalizationServiceExtensionsTests.cs
—MonoGameGum.Tests/Localization/LocalizationServiceLanguagesTests.cs
interface contract testsILocalizationService.Languages
— satellite matching testsTool/Tests/GumToolUnitTests/Managers/FileChangeReactionLogicTests.cs