Localization

Introduction

Gum supports localization using CSV and RESX files. Localization can be performed automatically by linking a localization file in your Gum project, or it can be done by hand in code-only projects. This document explains how to use the LocalizationManager to perform localization.

Localization in Gum Projects (Using the Gum UI Tool)

If you are using the Gum UI Tool to create your project, you can add and test localization in the tool itself. For information on how to set up localization in the Gum UI tool, see the Localization page.

Once you have a project set up with localization, the only code change needed is to specify the language index. In the Gum UI tool, you select a language by name from a dropdown. At runtime in code, you set CurrentLanguage as an integer index: index 0 is the string ID column, index 1 is the first language column, index 2 is the second, and so on. If CurrentLanguage is left at its default value of 0, your game will display the raw string IDs.

For example, the following is a screenshot from the Gum UI tool:

Screen displaying string IDs

At runtime the string IDs are displayed by default:

Screen displaying string IDs

We can select our string IDs before creating our screen:

Screen with localization

Localization and Font Ranges

Localized games may need an extended font range. If using the Gum tool, see the Project Properties page for information on font ranges.

Localization in a Code-Only Project

Code-only projects can use the LocalizationManager to enable localization. The steps for localization are:

  1. Create a localization CSV or RESX files

  2. Add these files to your project in such a way as to obtain a stream to them

  3. Call the appropriate method for loading these files

  4. Set the language index

  5. Assign Text to a string ID

How you open the stream depends on your platform. XNA-like platforms (MonoGame, KNI, FNA) bundle content files into the app package and require TitleContainer.OpenStream to read them — this is the only approach that works on iOS, Android, consoles, and web. Non-XNA platforms (raylib, SkiaSharp, and plain desktop apps) can read content directly from the filesystem with File.OpenRead. The examples below show both.

Code Example: Loading from CSV

This example uses a CSV file with the following contents:

Label and Buttons displaying localized UI

Code Example: Loading from RESX

RESX files are the standard .NET resource format and integrate well with tools like Visual Studio's resource editor. Gum loads a base RESX file and automatically discovers satellite files (named by culture code) alongside it.

This example uses a base file Strings.resx with English entries:

A satellite Strings.es.resx sits next to it with the Spanish translations (same keys, translated values).

The path-based overload uses Directory.GetFiles to auto-discover satellites, which does not work with bundled content. Open the base file and each satellite as streams via TitleContainer and pass them as (languageName, stream) pairs:

Satellite discovery uses the file naming convention BaseName.<culture>.resx (for example Strings.es.resx, Strings.fr.resx). The base file is labeled "Default"; each satellite is labeled with its culture code.

Multiple RESX Files

Larger projects often split localized strings across several base files — for example, one per feature area. This is the layout used by tools like ResXResourceManager, where strings are organized into files such as Strings.resx, Buttons.resx, and Errors.resx, each with its own satellites:

Pass the collection of base files (or groups of streams) to AddResxDatabase. Gum merges keys across all files into a single database:

Build one group per base file. Each group is a collection of (languageName, stream) pairs and may optionally be named — the group name appears in collision-warning messages:

The set of languages is the union of cultures across all base files. If Strings.resx has an es satellite but Buttons.resx does not, keys from Buttons.resx fall back to their string ID when es is selected.

Switching Language at Runtime

Gum re-translates already-instantiated visuals when you change CurrentLanguage. You do not need to recreate screens or rebuild controls — assigning a new value triggers the refresh automatically.

Automatic runtime language switching requires the June 2026 Gum release or newer. On earlier versions, changing CurrentLanguage only affected text assigned after the change — already-displayed text kept its old translation, and the only way to refresh was to recreate the screen.

The refresh walks Root, PopupRoot, and ModalRoot and re-applies the original string ID assigned to each control's Text, Header, or Placeholder. State-driven text (e.g. a Highlighted state that sets Text to a different string ID) refreshes correctly because the most recently assigned string ID is what gets re-translated.

If you need to refresh manually — for example, after building visuals that aren't attached to one of the standard roots — call RefreshLocalization directly:

Behavior of Untranslated Text on Refresh

Text assigned via SetTextNoTranslate (or SetHeaderNoTranslate / SetPlaceholderNoTranslate) is not touched by refresh. This is what makes user input in a TextBox survive language switches — TextBox routes typing, pasting, and deleting through the no-translate path internally.

Programmatic dynamic strings should also use the no-translate API. For example:

If you assign a dynamic string through the localized Text property while a LocalizationService is active, Gum will treat it as a string ID. On the first assignment it gets the (loc) missing-key suffix; on every subsequent language switch it will be re-translated and pick up the suffix again.

Data Bindings

If a control's Text is data-bound, refreshing the language will overwrite the bound value with a re-translated string ID. Refresh while bindings are active is not supported — prefer SetTextNoTranslate for bound text, or unbind before switching.

Forms Control Localization

Forms controls localize text automatically when a LocalizationService is active. When you assign a string ID to a control's Text property (or Header for MenuItem, Placeholder for TextBox), Gum translates it at assignment time. Each control that supports localization also provides a no-translate method for setting literal text that should not be translated.

Localization by Control

The following table shows how each Forms control handles localization:

Control
Localized Property
No-Translate Method
Notes

Button

Text

SetTextNoTranslate()

Label

Text

SetTextNoTranslate()

CheckBox

Text

SetTextNoTranslate()

RadioButton

Text

SetTextNoTranslate()

TextBox

Text

SetTextNoTranslate()

Setting Text in code localizes. User-typed text does not localize. See below.

TextBoxBase

Placeholder

SetPlaceholderNoTranslate()

Placeholder text localizes when set in code.

MenuItem

Header

SetHeaderNoTranslate()

PasswordBox

Mask characters are never localized.

ComboBox

Text comes from selected item. Pre-translate items before adding.

ListBoxItem

Text comes from data items. Pre-translate items before adding.

ScrollBar

No text property.

Slider

No text property.

ToggleButton

No text property.

TextBox Localization Behavior

TextBox has special behavior because it handles both programmatic text and user-typed input:

  • Setting Text in code applies localization — use this for initial values that should be translated.

  • Text entered by the user through typing, pasting, or deleting is never localized. TextBox internally uses SetTextNoTranslate for all user-initiated edits.

  • The Placeholder property (from TextBoxBase) is localized when set in code.

For example, if you set a TextBox's Text to a string ID, it displays the translated text. Once the user begins editing, their input is used as-is without translation.

Data-Driven Controls

ComboBox and ListBoxItem intentionally bypass localization. Their displayed text comes from data objects (via ToString()), so translating would attempt to look up the data value as a string ID.

To localize items in a ComboBox or ListBox, translate the values before adding them to the Items collection:

Using SetTextNoTranslate

Every localization-aware control provides a method for setting text without translation. This is useful when displaying dynamic values that should not be treated as string IDs:

SetTextNoTranslate is a method rather than a property because the underlying text component only stores the final string. A TextNoTranslate property getter would be misleading since there is no way to distinguish translated from untranslated text after assignment.

Last updated

Was this helpful?