From godot-prompter
Implements localization (i18n/l10n) in Godot 4.3+ using TranslationServer, CSV/PO translation files, locale switching, RTL support, and pluralization for GDScript and C#.
npx claudepluginhub jame581/godotprompter --plugin godot-prompterThis skill uses the workspace's default tool permissions.
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Guides iOS localization with Xcode String Catalogs for translations, pluralization, RTL layouts, and SwiftUI LocalizedStringKey patterns.
Implement, review, or improve iOS/macOS app localization using String Catalogs (.xcstrings), generated symbols, LocalizedStringKey/Resource, pluralization, FormatStyle, RTL layouts, and Dynamic Type.
Share bugs, ideas, or general feedback.
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Related skills: godot-ui for Control nodes and theme management, save-load for persisting language settings, responsive-ui for layout adjustments per locale.
tr() — Godot's translation functionTranslation resourcesTranslationServer.set_locale()All Control nodes with text, tooltip_text, or placeholder_text properties are automatically translated if the value matches a translation key.
| Strategy | Example Key | Pros | Cons |
|---|---|---|---|
| Semantic keys | MENU_START_GAME | Clear intent, easy to find | Need a default language fallback |
| English-as-key | Start Game | Readable code, no mapping file for English | Keys break if English text changes |
Recommendation: Use semantic keys (
MENU_START_GAME) for production projects. Use English-as-key only for prototypes or solo projects.
The simplest format. First column is the key, subsequent columns are locale codes.
keys,en,cs,de,ja
MENU_START,Start Game,Začít hru,Spiel starten,ゲームスタート
MENU_OPTIONS,Options,Nastavení,Optionen,オプション
MENU_QUIT,Quit,Ukončit,Beenden,終了
PLAYER_HEALTH,Health: %d,Zdraví: %d,Gesundheit: %d,体力: %d
ITEM_COLLECTED,%s collected!,%s sebráno!,%s gesammelt!,%sを入手!
Save as translations.csv in your project. Godot auto-detects the format on import.
Import settings (Import dock):
Industry-standard format. Better for professional translation teams and tools like Poedit, Weblate, Crowdin.
Create a POT template (messages.pot):
msgid "MENU_START"
msgstr ""
msgid "MENU_OPTIONS"
msgstr ""
msgid "MENU_QUIT"
msgstr ""
msgid "PLAYER_HEALTH"
msgstr ""
Create locale files (e.g., cs.po for Czech):
msgid "MENU_START"
msgstr "Začít hru"
msgid "MENU_OPTIONS"
msgstr "Nastavení"
msgid "MENU_QUIT"
msgstr "Ukončit"
msgid "PLAYER_HEALTH"
msgstr "Zdraví: %d"
Project Settings → Localization → Translations → Add... → select your .csv or .po files.
Or register at runtime:
var translation := load("res://translations/cs.po") as Translation
TranslationServer.add_translation(translation)
var translation = GD.Load<Translation>("res://translations/cs.po");
TranslationServer.AddTranslation(translation);
# Basic translation
var label_text: String = tr("MENU_START") # "Start Game" or translated equivalent
# With format arguments
var health_text: String = tr("PLAYER_HEALTH") % current_health
# "Health: 85" or "Zdraví: 85"
# With string arguments
var collected_text: String = tr("ITEM_COLLECTED") % item_name
# "Sword collected!" or "Meč sebráno!"
# Pluralization (Godot 4.x)
var count := 5
var msg: String = tr_n("ONE_ENEMY", "MANY_ENEMIES", count)
# Requires PO files with plural forms
string labelText = Tr("MENU_START");
string healthText = string.Format(Tr("PLAYER_HEALTH"), currentHealth);
// Pluralization
string msg = TrN("ONE_ENEMY", "MANY_ENEMIES", count);
Label, Button, RichTextLabel, and other Control nodes automatically translate their text property if it matches a translation key. Set the text to the key:
Button.text = "MENU_START" → displays "Start Game" (en) or "Začít hru" (cs)
Tip: If you don't want automatic translation on a specific Control, set its
auto_translate_modetoDISABLED.
# Switch language
func set_language(locale_code: String) -> void:
TranslationServer.set_locale(locale_code)
# All Control nodes with translation keys update automatically
# Get current locale
var current: String = TranslationServer.get_locale() # e.g. "en", "cs", "de"
# Get available locales
var locales: PackedStringArray = TranslationServer.get_loaded_locales()
public void SetLanguage(string localeCode)
{
TranslationServer.SetLocale(localeCode);
}
string current = TranslationServer.GetLocale();
extends Control
@onready var language_button: OptionButton = %LanguageButton
var _locales: Array[Dictionary] = [
{"code": "en", "name": "English"},
{"code": "cs", "name": "Čeština"},
{"code": "de", "name": "Deutsch"},
{"code": "ja", "name": "日本語"},
]
func _ready() -> void:
for locale in _locales:
language_button.add_item(locale["name"])
# Set current selection
var current_locale: String = TranslationServer.get_locale()
for i in _locales.size():
if _locales[i]["code"] == current_locale:
language_button.selected = i
break
language_button.item_selected.connect(_on_language_selected)
func _on_language_selected(index: int) -> void:
TranslationServer.set_locale(_locales[index]["code"])
# Save preference — SettingsManager is a user-created autoload (see save-load skill)
SettingsManager.set_setting("general", "locale", _locales[index]["code"])
For Arabic, Hebrew, Persian, and other RTL languages.
# On any Control node
control.layout_direction = Control.LAYOUT_DIRECTION_RTL
# Or set globally in Project Settings:
# Internationalization → Rendering → Text Direction → RTL
| Property | Purpose |
|---|---|
layout_direction | LTR, RTL, LOCALE (auto from current locale), INHERITED |
text_direction | On Label/RichTextLabel: override text direction |
structured_text_type | Handle special structures (URLs, file paths, email) that shouldn't fully reverse |
# Force LTR for a number or URL inside RTL text
rich_text.text = "النتيجة: [ltr]100/200[/ltr]"
// LocaleAwarePanel.cs — flip layout direction whenever the locale changes.
using Godot;
public partial class LocaleAwarePanel : Control
{
public override void _Ready()
{
ApplyLayoutForLocale();
TranslationServer.Singleton.LocaleChanged += ApplyLayoutForLocale;
}
public override void _ExitTree()
{
// TranslationServer outlives every scene — without this unsubscribe,
// each panel instance leaks a delegate reference for the lifetime of the process.
TranslationServer.Singleton.LocaleChanged -= ApplyLayoutForLocale;
}
private void ApplyLayoutForLocale()
{
string locale = TranslationServer.Singleton.GetLocale();
bool isRtl = TextServerManager.GetPrimaryInterface().IsLocaleRightToLeft(locale);
LayoutDirection = isRtl
? Control.LayoutDirectionEnum.Rtl
: Control.LayoutDirectionEnum.Ltr;
}
}
// RichTextLabel mixed-direction example — same BBCode as GDScript, just assigned in C#.
public partial class ScoreLabel : RichTextLabel
{
public void SetArabicScore(int score, int max)
{
BbcodeEnabled = true;
Text = $"النتيجة: [ltr]{score}/{max}[/ltr]";
}
}
RTL scripts need fonts that support the relevant Unicode ranges. Godot's default font does not cover Arabic/Hebrew. Import a font like Noto Sans Arabic and assign it via Theme.
# Format numbers with locale-appropriate separators
var formatted: String = "%d" % 1234567
# Always outputs "1234567" — GDScript doesn't locale-format numbers
# For locale-aware number formatting, use a helper:
func format_number(value: int) -> String:
var s := str(value)
var result := ""
var count := 0
for i in range(s.length() - 1, -1, -1):
if count > 0 and count % 3 == 0:
result = "," + result # or "." for European locales
result = s[i] + result
count += 1
return result
Godot doesn't provide built-in locale-aware date formatting. Use Time.get_datetime_dict_from_system() and format manually per locale.
res://
├── translations/
│ ├── game.csv # Main game translations
│ ├── ui.csv # UI-specific translations
│ └── items.csv # Item names and descriptions
├── fonts/
│ ├── default_font.ttf # Latin, Cyrillic
│ └── cjk_font.ttf # Chinese, Japanese, Korean
└── themes/
└── default_theme.tres # Font assignments per locale if needed
# Category_Context_Description
MENU_MAIN_START # Main menu, start button
MENU_MAIN_QUIT # Main menu, quit button
HUD_HEALTH_LABEL # In-game HUD, health label
DIALOGUE_NPC_GREETING # NPC dialogue, greeting line
ITEM_SWORD_NAME # Inventory item name
ITEM_SWORD_DESC # Inventory item description
| Symptom | Cause | Fix |
|---|---|---|
| Translation key shows instead of text | Translation file not registered in Project Settings | Add to Project Settings → Localization → Translations |
| Text doesn't update on locale switch | Using string literals instead of tr() | Wrap all user-facing strings in tr() |
| Label shows key after scene change | Translation resource not loaded yet | Register translations in Project Settings (not at runtime) |
| RTL text renders LTR | layout_direction not set | Set to RTL or LOCALE on root Control |
| Font doesn't display characters | Missing Unicode range in font | Import a font that covers the target script (Noto Sans recommended) |
| Pluralization doesn't work with CSV | CSV doesn't support plural forms | Use PO format for languages with complex plural rules |
%s in translation shows literal %s | Using tr() result as key instead of formatting it | Use tr("KEY") % value, not tr("KEY" % value) |
Godot 4.5 adds a live locale preview to the editor. You can see how your UI looks in any configured locale — translated text, RTL layout, font changes — without running the game.
ja, cs, ar).tr() keys (untranslated keys show up as-is in non-English preview).Reset to the default locale by selecting the blank or
enentry in the Preview Language dropdown. The preview applies only in the editor and does not affect exported builds.
Godot 4.6 extends the CSV translation format with three optional header columns that enable features previously only available in PO files.
Note: Godot 4.6 is in beta; verify behavior on stable release.
| Column header | Purpose |
|---|---|
?context | Disambiguates keys with the same string but different meanings (e.g. "file" as a noun vs. "to file" as a verb) |
?plural | Provides the plural form of the string (for the source locale) |
?pluralrule | CLDR plural rule index for the source locale (0 = one, 1 = other, etc.) |
keys,?context,?plural,en,cs,de
ITEM_FILE,noun,,File,Soubor,Datei
ITEM_FILE,verb,,File,Uložit,Ablegen
ENEMY_COUNT,,{n} enemies,{n} enemy / {n} enemies,{n} nepřítel / {n} nepřátelé,{n} Feind / {n} Feinde
# Translate with context to disambiguate identical keys
var file_noun: String = tr("ITEM_FILE", "noun") # "File" (object)
var file_verb: String = tr("ITEM_FILE", "verb") # "File" (action)
# Without context — returns the first match for the key
var file_default: String = tr("ITEM_FILE")
// Translate with context
string fileNoun = Tr("ITEM_FILE", "noun");
string fileVerb = Tr("ITEM_FILE", "verb");
# Pluralize with tr_n() — works with CSV plural columns in 4.6+
var enemy_count := 3
var msg: String = tr_n("ENEMY_COUNT", "ENEMY_COUNT", enemy_count)
# Godot substitutes the correct plural form based on the current locale's rules
int enemyCount = 3;
string msg = TrN("ENEMY_COUNT", "ENEMY_COUNT", enemyCount);
When to use PO vs CSV: If you only need context and simple one/other plural rules, the new CSV columns cover most cases. For languages with three or more plural forms (Russian, Polish, Arabic), continue using PO format with full
msgstr[n]plural arrays.
tr() (or are set as translation keys on Control nodes)TranslationServer.set_locale()layout_direction set to RTL or LOCALE on root UI containers%s, %d) are applied AFTER tr(), not before?context column used when the same key has different meanings in different UI contexts (Godot 4.6+)?plural / ?pluralrule columns used for simple one/other plurals in CSV workflow; PO format used for languages with 3+ plural forms (Godot 4.6+)