Agent Skills › vchelaru/Gum › gum-localization

gum-localization

GitHub

Gum UI框架的本地化技能,支持CSV/RESX加载、运行时语言切换及文本翻译。通过ILocalizationService接口管理多语言资源,提供自动重翻译机制,并优化了CSV解析规则与警告处理,确保UI文本的正确国际化展示。

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

Trigger Scenarios

需要实现或配置应用程序的多语言支持 运行时动态切换语言以更新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.

Load policy lives in GumCommon/Localization/ProjectLocalizationLoader.cs, shared by the tool (FileCommands.LoadLocalizationFile()), gumcli (HeadlessLocalizationLoader) and the runtime GumService. Each host passes only a ProjectLocalizationLoadOptions (bundle provider, where skips and warnings go); CSV always goes through AddCsvDatabase, so tool and game parse a file the same way.

  • 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: the MonoGame and Skia GumService call the same ProjectLocalizationLoader.Load; collision warnings surface 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":

  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. 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
  • Tools/Gum.Presentation/Commands/FileCommands.cs — LoadLocalizationFile() (CSV/RESX branch, LocalizationLoaded event)
  • Tools/Gum.Presentation/Commands/IFileCommands.cs — LocalizationLoaded event declaration
  • Tools/Gum.Presentation/Managers/FileChangeReactionLogic.cs — IsLocalizationFileThatShouldTriggerReload() (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)
  • GumCommon/Localization/ProjectLocalizationLoader.cs — load policy for .gumx LocalizationFiles, called by the tool's FileCommands, gumcli's HeadlessLocalizationLoader, MonoGameGum/GumService.cs and Runtimes/SkiaGum/GumServiceSkiaBase.cs
  • MonoGameGum/GumService.cs — RefreshLocalization() walks the three roots; constructor wires the RefreshLocalizationOnElementAction delegate and subscribes to LocalizationServiceChanged
  • GumRuntime/GraphicalUiElement.cs — RefreshLocalization() 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.cs — Text property and SetTextNoTranslate method
  • MonoGameGum/Forms/Controls/ — Forms control localization pattern
  • MonoGameGum.Tests/Localization/LocalizationServiceExtensionsTests.cs — CSV/RESX loader tests
  • MonoGameGum.Tests/Localization/LocalizationServiceLanguagesTests.cs — ILocalizationService.Languages interface contract tests
  • Tests/Gum.Presentation.Tests/FileChangeReactionLogicTests.cs — satellite matching tests

Version History

  • 7fc2261 Current 2026-09-28 09:48

    统一使用运行时AddCsvDatabase方法解析CSV,移除独立工具解析器;增强CSV解析兼容性(修剪引号外空格、保留未引用单元格内引号、跳过注释行);新增重复String ID警告功能。

  • 78a2f53 2026-09-22 22:52
  • c93866f 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
7fc2261
Hash
49320592
Indexed
2026-08-20 09:20

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-28 22:09
浙ICP备14020137号-1