Agent Skillsvchelaru/Gum › gum-localization

gum-localization

GitHub

Gum引擎本地化技能,支持ILocalizationService接口、CSV/RESX数据加载及运行时语言切换。处理Text与TextNoTranslate路径,实现UI元素自动重翻译及缺失标记功能。

.claude/skills/gum-localization/SKILL.md vchelaru/Gum

Trigger Scenarios

需要为Gum UI添加多语言支持 加载或切换CSV/RESX翻译文件 配置运行时语言切换逻辑

Install

npx skills add vchelaru/Gum --skill gum-localization -g -y
More Options

Non-standard path

npx skills add https://github.com/vchelaru/Gum/tree/main/.claude/skills/gum-localization -g -y

Use without installing

npx skills use vchelaru/Gum@gum-localization

指定 Agent (Claude Code)

npx skills add vchelaru/Gum --skill gum-localization -a claude-code -g -y

安装 repo 全部 skill

npx skills add vchelaru/Gum --all -g -y

预览 repo 内 skill

npx skills add vchelaru/Gum --list

SKILL.md

Frontmatter
{
    "name": "gum-localization",
    "description": "Gum's localization — ILocalizationService, CSV\/RESX loading (tool + runtime), Text vs TextNoTranslate paths, Forms control localization."
}

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.

Runtime language switching: ILocalizationService.CurrentLanguageChanged fires when CurrentLanguage is reassigned to a different value. GumService subscribes and walks Root/PopupRoot/ModalRoot re-translating every text-bearing element that was assigned via the localized path. Manual entry point: GumService.Default.RefreshLocalization(). Per-element entry: GraphicalUiElement.RefreshLocalization() recurses into Children. The per-element re-translate is delegated through GraphicalUiElement.RefreshLocalizationOnElementAction (wired by GumService since GumRuntime cannot reference CustomSetPropertyOnRenderable). Originating string IDs live in a static ConditionalWeakTable<GraphicalUiElement, string> on CustomSetPropertyOnRenderable, populated whenever TrySetPropertyOnText runs the localization path and cleared by SetTextNoTranslate.

ILocalizationService

GumCommon/Localization/ILocalizationService.cs — six members:

  • CurrentLanguage (int) — index into the translation arrays (0 = default/source language)
  • Languages (IReadOnlyList<string>) — language names populated after loading; empty until a database is loaded
  • CurrentLanguageChanged (event Action?) — fires when CurrentLanguage is reassigned to a different value; subscribed by GumService to drive automatic re-translation of live visuals
  • 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
  • Clear() — resets the database and Languages list
  • 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. 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).

  • AddResxDatabase(string baseResxFilePath) — single base file, auto-discovers satellites (Strings.resx + Strings.es.resx, Strings.fr.resx). Base labeled "Default"; satellites use their culture code.
  • AddResxDatabase(IEnumerable<string> baseResxFilePaths, Action<string> onWarning = null)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; onWarning fires once per colliding key and names all prior sources.
  • AddResxDatabase(IEnumerable<(string languageName, Stream stream)>) — single-file stream variant for mobile/web.
  • AddResxDatabase(IEnumerable<(string? groupName, IEnumerable<(string languageName, Stream stream)>)> fileGroups, Action<string> onWarning = null) — multi-group stream variant with explicit group names used in collision warnings.

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 AddResxDatabase(IEnumerable<string>, onWarning) overload. onWarning is wired to IOutputManager.AddOutput so collisions appear in the Output tab.
  • 1 CSV → single-file CSV path.
  • Mixed CSV+RESX or multiple CSVs → AddError and skip (no multi-CSV overload by design; AddDatabase replaces rather than merges).

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 LanguageNameLanguageIndex 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":

  1. If the raw value contains [ → treated as BBCode markup, applied directly (stored as StoredMarkupText)
  2. If property is "Text" AND LocalizationService != nullrawText = 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

ComboBoxText property sets coreTextObject.RawText directly (bypasses SetProperty entirely). This is because ComboBox text comes from SelectedItem.ToString(), which is data-driven.

ListBoxItemUpdateToObject(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. Language selection is always index-driven. CurrentLanguage (int) is the only way to select a language; Languages/LanguageName (tool VM) is a display-string wrapper around that index, not a separate string-based selection mechanism.

  2. "(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).

  3. Translation happens at assignment time, not read time — The renderable stores only the final translated string. Live UI is kept in sync by a separate path: CustomSetPropertyOnRenderable records the original raw value in a ConditionalWeakTable<GraphicalUiElement, string> whenever the localized Text path runs, and GumService subscribes to ILocalizationService.CurrentLanguageChanged to walk the live tree and re-call SetProperty("Text", storedKey) on every tracked element. SetTextNoTranslate clears the entry, so user input and explicit literals survive language switches. Programmatic dynamic strings assigned via the localized Text property still get re-translated on language change and will pick up the (loc) suffix — use SetTextNoTranslate for those. Bound Text is overwritten by refresh; the design assumes bindings and runtime language switching aren't combined.

  4. Null service = no localization — If LocalizationService is null, all text passes through unchanged. This is the expected state when localization isn't needed.

  5. 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.

  6. 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.

  7. RESX satellite ordering and naming — Satellites are sorted alphabetically by file path, so de comes before es comes before fr. The base file is always first and labeled "Default". If you need a specific order or names, use the stream-based overload.

  8. 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 (CurrentLanguage, Languages, AddDatabase, Clear, Translate)
  • GumCommon/Localization/LocalizationService.cs — default implementation
  • GumCommon/Localization/LocalizationServiceExtensions.cs — CSV/RESX loaders
  • Gum/Wireframe/CustomSetPropertyOnRenderable.cs — static LocalizationService property (with LocalizationServiceChanged event), _localizationKeys ConditionalWeakTable, TryGetLocalizationKey, and translation logic in TrySetPropertyOnText
  • Gum/Commands/FileCommands.csLoadLocalizationFile() (CSV/RESX branch, LocalizationLoaded event)
  • Gum/Commands/IFileCommands.csLocalizationLoaded event declaration
  • Gum/Managers/FileChangeReactionLogic.csIsLocalizationFileThatShouldTriggerReload() (list + satellite matching)
  • Gum/Plugins/InternalPlugins/ProjectPropertiesWindowPlugin/ — Language dropdown + LocalizationFiles list editor UI
  • WpfDataUi/Controls/MultiFileDisplay.xaml(.cs)IDataUi control for List<string> file-path lists; composes FilePickingLogic
  • WpfDataUi/Controls/FilePickingLogic.cs — shared file-dialog/relative-path plumbing (pattern like TextBoxDisplayLogic)
  • MonoGameGum/GumService.cs — runtime auto-load of .gumx LocalizationFiles; collision warnings surface on GumLoadResult.Warnings; RefreshLocalization() walks the three roots; constructor wires the RefreshLocalizationOnElementAction delegate and subscribes to LocalizationServiceChanged
  • GumRuntime/GraphicalUiElement.csRefreshLocalization() recursion + RefreshLocalizationOnElementAction static delegate hook
  • MonoGameGum.Tests/Localization/RefreshLocalizationTests.cs — runtime language-switch tests (Forms controls, BBCode-from-translation, TextNoTranslate survival, popup/modal roots)
  • MonoGameGum/GueDeriving/TextRuntime.csText property and SetTextNoTranslate method
  • MonoGameGum/Forms/Controls/ — Forms control localization pattern
  • MonoGameGum.Tests/Localization/LocalizationServiceExtensionsTests.cs — CSV/RESX loader tests
  • MonoGameGum.Tests/Localization/LocalizationServiceLanguagesTests.csILocalizationService.Languages interface contract tests
  • Tool/Tests/GumToolUnitTests/Managers/FileChangeReactionLogicTests.cs — satellite matching tests

Version History

  • c93866f Current 2026-08-20 09:20

Same Skill Collection

.claude/skills/gum-cross-platform-unification/SKILL.md
.claude/skills/gum-issue-creation/SKILL.md
.claude/skills/gum-monthly-release/SKILL.md
.claude/skills/gum-runtime-binding/SKILL.md
.claude/skills/gum-runtime-syntax-version/SKILL.md
.claude/skills/gum-tool-selection/SKILL.md
.claude/skills/refactoring-direction/SKILL.md
.claude/skills/tdd/SKILL.md

Metadata

Files
0
Version
c93866f
Hash
44d8a12b
Indexed
2026-08-20 09:20

- 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-26 00:16
浙ICP备14020137号-1 $방문자$