Internationalizing the game and UI
Automatic translation, computed text, resource remaps, fonts, RTL and startup checks.
An internationalized game separates message lookup, player language selection, text rendering and localized assets. A correct translated string does not prove that the font, layout or resource is correct. Check each layer independently.
Prepare the project
- Import and register RU/EN catalogs using translation setup.
- Keep stable keys for UI text. Use context when one source word has different meanings; preserve placeholders in every translation.
- Choose only among languages the game ships. Apply a saved player preference after loading settings; otherwise select from the OS locale with an explicit fallback. The locale guide supplies a complete RU/EN resolver.
- Give the player a language selector. TranslationServer.SetLocale changes the current language, but does not save the choice.
Use CSV or PO as the source of translations. Do not replace catalog-based translation with a separate if/else for every label.
Automatic text, calculated text and player text
| Kind | Approach |
|---|---|
| Static Label/Button caption | Store the translation key in Text; enable automatic translation |
| Count, formatted result or generated sentence | Look up the complete template, choose plural form if needed, format it, and refresh on a translation-change notification |
| Player name, chat, external text | Disable automatic translation; display the supplied text |
A Label retains its source key in Text while its internal translated text changes. Do not read Text expecting the displayed translation. Disabled automatic translation can be inherited from an ancestor; Always can override that inheritance for an individual Entity.
A complete localized HUD
Create a VBoxContainer and attach LocalizedHud.cs below. Register tables containing PLAY_KEY and plural keys ITEM/ITEMS. PLAY_KEY should translate to Play/Играть; use the CSV plural table for ITEM/ITEMS.
using Reckfell;
public partial class LocalizedHud : VBoxContainer
{
public Label PlayLabel { get; private set; } = null!;
public Label PlayerLabel { get; private set; } = null!;
public Label CountLabel { get; private set; } = null!;
public int ItemCount { get; set; } = 5;
public override void _Ready()
{
PlayLabel = new Label { Text = "PLAY_KEY" };
PlayerLabel = new Label
{
AutoTranslateMode = AutoTranslateModeEnum.Disabled,
Text = "PLAY_KEY"
};
CountLabel = new Label { AutoTranslateMode = AutoTranslateModeEnum.Disabled };
AddChild(PlayLabel);
AddChild(PlayerLabel);
AddChild(CountLabel);
RefreshCount();
}
public override void _Notification(int what)
{
if (what == NotificationTranslationChanged && CountLabel != null)
RefreshCount();
}
public void RefreshCount()
{
string form = TranslationServer.TranslatePlural("ITEM", "ITEMS", ItemCount).ToString();
CountLabel.Text = string.Format(form, ItemCount);
}
public void SetGameLocale(string locale)
{
TranslationServer.SetLocale(locale);
}
}Call SetGameLocale("en"), then SetGameLocale("ru"). The first line changes from Play to Играть; the second remains literally PLAY_KEY, simulating a player name identical to a key; the counter changes from 5 items to 5 предметов. After changing ItemCount, call RefreshCount. Replace the diagnostic player-name value with your actual player text.
TranslatePlural selects a template; string.Format inserts the count. Do not concatenate separately translated fragments: translators may need a different word order. The complete message and its placeholders belong in the catalog.
Context must reach the lookup
For an automatic Control, set TranslationContext before assigning Text. For computed text, pass context directly to TranslationServer.Translate. Changing TranslationContext alone currently does not recalculate an existing Label's translated buffer. To refresh after a context change, call label.PropagateNotification((int)Entity.NotificationTranslationChanged), where label is your existing Label. Reassigning the same Text value alone does not force a refresh.
Localized resources
In Project Settings → Localization → Remaps, select an original resource, add alternatives and assign their locales. Continue loading the original path in game code. Loading an alternative path directly bypasses selection for the original.
This project.reckfell setting demonstrates remapping with Gradient resources; merge it into the existing internationalization section while the editor is closed:
[internationalization]
locale/translation_remaps={"asset://Locale/palette.rdata": PackedStringArray("asset://Locale/palette-en.rdata:en", "asset://Locale/palette-ru.rdata:ru")}Create all three Gradient resources first. Load asset://Locale/palette.rdata. Changing locale reloads a loaded, remapped resource; the cached object can retain its identity while its contents change. For a locale with no matching alternative, the original resource is used. The text fallback locale does not automatically supply a resource alternative: provide an appropriate original.
Use the editor's remap workflow for localized images or voices, then test their actual consumers. A resource reload is not proof that an audio player restarts playback or that every cached copy in your code updates. Keep translated asset dimensions, timing and references compatible with the consumer.
Fonts and layout
Use the current Font/FontFile resources and Font.Fallbacks, not a DynamicFont resource. Set reusable typography through a Theme, for example its DefaultFont, rather than replacing every Control separately. Provide fonts covering the actual text, including player text that can differ from the selected UI language. Adding a fallback resource does not prove glyph coverage; test the required characters and respect each font's license.
Label.Language affects text shaping; it does not change TranslationServer's locale or choose another catalog. TextDirection controls text direction, while Control.LayoutDirection controls UI layout direction. These are different settings. Paths and other structured strings can require StructuredTextBidiOverride rather than ordinary paragraph ordering.
For RTL, test real mixed Arabic/Hebrew, numbers and Latin strings, focus order, icons and each relevant container. Explicit RTL layout is inherited by children unless overridden. It does not mirror the game's entire coordinate system or replace translation selection. Do not assume every directional illustration should be flipped.
Let containers grow and labels wrap where appropriate; test the smallest supported window. Use pseudolocalization to stress length, noting its C# placeholder and plural limitations. Accented Latin text is not a substitute for CJK or RTL tests.
TranslationServer.FormatNumber/ParseNumber convert supported numbering-system characters; they are not a complete currency, unit or cultural-formatting policy. Keep number formatting separate from catalog selection. For languages needing dictionary-based line breaking, review internationalization/locale/include_text_server_data and validate an exported build with the required TextServer support; do not infer export behavior from the editor.
Test startup and release configuration
internationalization/locale/test forces a startup locale. Clear it before release when the game should select the player's language. A command-line language can override the test setting:
& "C:\path\Reckfell Engine.console.exe" --path "C:\path\Game" --language ruReplace both paths with your installed executable and project. Code that calls SetLocale afterward can still change the language again.
For a localized application title, application/config/name_localized stores locale-to-title entries. Check the actual target platform's exported app metadata: a translated in-game Label does not verify the launcher/store title.
Acceptance and diagnosis
| Failure | First check |
|---|---|
| Key is visible | Registration, exact key/context, selected locale and fallback |
| Static caption updates, counter does not | Notification handler and template reformatting |
| Player name unexpectedly translates | AutoTranslateMode on that element |
| Text changes but the localized asset does not | Original load path, remap alternatives and the consumer's cached state |
| Correct string has boxes, clipping or wrong direction | Font coverage, shaping and layout separately |
| Locale resets after restart | Saved preference and startup test setting |
Native tests exercise this exact HUD, Label text buffers, translation inheritance, context refresh, locale versus Language, loaded Gradient remaps and command-line startup. They also check explicit RTL flags and the FontFile fallback API. They do not certify rendered glyphs, mirrored layouts, overflow, image/audio playback, preference persistence, editor interaction or platform exports. Those remain separate acceptance tasks.