gum-localization
$
npx mdskill add vchelaru/Gum/gum-localizationManage runtime text translations for Gum applications.
- Enables agents to load CSV or RESX translation files dynamically.
- Depends on ILocalizationService and SystemManagers for initialization.
- Selects language output by tracking the current language index.
- Returns translated strings directly through the Translate method.
SKILL.md
.github/skills/gum-localizationView on GitHub ↗
---
name: gum-localization
description: Reference guide for Gum's runtime localization system — ILocalizationService, CSV/RESX loading, Text vs TextNoTranslate paths, Forms control localization patterns, and gotchas.
---
# Gum 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` — three members:
- `CurrentLanguage` (int) — index into the translation arrays (0 = default/source language)
- `AddDatabase(Dictionary<string, string[]>, List<string>)` — loads translations; key = string ID, value = array where `[0]` is the ID and `[1..N]` are translations per language
- `Translate(string stringId)` — returns the translated string for `CurrentLanguage`
## LocalizationService (default implementation)
`GumCommon/Localization/LocalizationService.cs`
Translation logic in `TranslateForLanguage`:
1. If database is empty → return string as-is (no translation, no suffix)
2. If string ID is found → return `mStringDatabase[stringId][language]`
3. If string has no letters (numbers/punctuation/whitespace only) → return as-is (excluded from translation)
4. Otherwise → return `stringId + "(loc)"` — the "(loc)" suffix signals a missing translation key
## 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.
**RESX:** Two overloads:
- `AddResxDatabase(string baseResxFilePath)` — discovers satellite files by convention (e.g., `Strings.resx` + `Strings.es.resx`, `Strings.fr.resx`). Satellites are sorted alphabetically.
- `AddResxDatabase(IEnumerable<(string languageName, Stream stream)>)` — stream-based, for manual control over language order.
Both formats produce the same internal structure: `Dictionary<string, string[]>` where index 0 = string ID, 1+ = per-language translations.
## Translation Flow in CustomSetPropertyOnRenderable
`Gum/Wireframe/CustomSetPropertyOnRenderable.cs`, `TrySetPropertyOnText` method:
When `SetProperty` is called with property name `"Text"` or `"TextNoTranslate"`:
1. If the raw value contains `[` → treated as BBCode markup, applied directly (stored as `StoredMarkupText`)
2. If property is `"Text"` AND `LocalizationService != null` → `rawText = LocalizationService.Translate(rawText)`
3. If the *translated* result contains `[` → treated as BBCode (translation can produce BBCode)
4. If property is `"TextNoTranslate"` → no translation call, value used as-is
**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`:
- `Text` property (get/set) — calls `SetProperty("Text", value)` → goes through localization
- `SetTextNoTranslate(string?)` method — calls `SetProperty("TextNoTranslate", value)` → bypasses localization
`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 | `Text` | `SetTextNoTranslate()` |
| Label | `Text` | `SetTextNoTranslate()` |
| CheckBox | `Text` | `SetTextNoTranslate()` |
| RadioButton | `Text` | `SetTextNoTranslate()` |
| TextBox | `Text` | `SetTextNoTranslate()` |
| TextBoxBase | `Placeholder` | `SetPlaceholderNoTranslate()` |
| MenuItem | `Header` | `SetHeaderNoTranslate()` |
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
1. **"(loc)" suffix is intentional** — When a database is loaded but a string ID isn't found, `Translate()` appends "(loc)". This is a debugging feature, not a bug. Empty databases return strings unchanged (no suffix).
2. **Translation happens at assignment time, not read time** — The renderable stores only the final translated string. Changing `CurrentLanguage` after setting text does NOT retroactively update existing UI. You must re-assign `Text` to all controls.
3. **Null service = no localization** — If `LocalizationService` is null, all text passes through unchanged. This is the expected state when localization isn't needed.
4. **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.
5. **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 `CurrentLanguage = 0` returns the string ID.
6. **RESX satellite ordering** — Satellites are sorted alphabetically by file path, so `de` comes before `es` comes before `fr`. If you need a specific order, use the stream-based overload.
7. **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
- `GumCommon/Localization/ILocalizationService.cs` — interface
- `GumCommon/Localization/LocalizationService.cs` — default implementation
- `GumCommon/Localization/LocalizationServiceExtensions.cs` — CSV/RESX loaders
- `Gum/Wireframe/CustomSetPropertyOnRenderable.cs` — static `LocalizationService` property and translation logic in `TrySetPropertyOnText`
- `MonoGameGum/GueDeriving/TextRuntime.cs` — `Text` property and `SetTextNoTranslate` method
- `MonoGameGum/Forms/Controls/` — Forms control localization pattern
- `MonoGameGum.Tests/Forms/LocalizationTests.cs` — comprehensive test coverage
More from vchelaru/Gum
- bump-nuget-versionBump the NuGet package versions for all 12 Gum projects (11 libraries + GumCli). Queries NuGet to check if a version exists for today, then sets the new version to YYYY.M.D.V where V increments from the latest published version today (or starts at 1). Creates a release branch named ReleaseCode_YYYY_M_D_V, commits the changes, and pushes. Run this before triggering the nuget release workflow.
- gum-cliReference guide for GumCli — the headless command-line tool for Gum projects. Load this when working on gumcli commands (new, check, codegen, codegen-init), Gum.ProjectServices, HeadlessErrorChecker, ProjectLoader, HeadlessCodeGenerationService, CodeGenerationAutoSetupService, or the FormsTemplateCreator.
- gum-docs-writingReference guide for writing Gum documentation in GitBook markdown. Load when writing or editing docs/ files, adding pages to SUMMARY.md, using GitBook hints/figures, linking between pages, or adding images.
- gum-forms-behaviorsCovers Gum's behaviors system and the design-time → runtime Forms wrapping lifecycle. Load this when working on BehaviorSave, ElementBehaviorReference, StandardFormsBehaviorNames, FormsUtilities.RegisterFromFileFormRuntimeDefaults, DefaultFromFileXxxRuntime classes, or when investigating why Forms properties cannot be set at design time in the Gum tool.
- gum-forms-controlsReference guide for Forms controls — classes inheriting from FrameworkElement. Load this when working on Button, CheckBox, ListBox, ComboBox, TextBox, ScrollViewer, or any class in Gum.Forms.Controls (or FlatRedBall.Forms.Controls). Also load when working on FrameworkElement itself, the Visual/InteractiveGue relationship, state machines, DefaultVisuals, or ReactToVisualChanged.
- gum-forms-default-visualsReference guide for Forms DefaultVisuals — the code-only visual classes that back Forms controls. Load when working on ButtonVisual, any *Visual class in DefaultVisuals/, Styling, DefaultFormsTemplates registration, or building custom code-only Forms visuals.
- gum-forms-itemscontrolReference guide for ItemsControl and ListBox — the Items/ListBoxItems relationship, templates, InnerPanel sync, and gotchas. Load this when working on ItemsControl, ListBox, ListBoxItem, VisualTemplate, FrameworkElementTemplate, Items collection behavior, ListBoxItems desync, or adding/removing items from a list box.
- gum-layout>
- gum-layout-engine>
- gum-property-assignmentReference guide for how Gum applies variables and sets properties on renderables. Load this when working on ApplyState, SetProperty, SetVariablesRecursively, CustomSetPropertyOnRenderable, font loading, IsAllLayoutSuspended, or isFontDirty.