Use when implementing localization (i18n/l10n) — TranslationServer, CSV/PO translation files, locale switching, RTL support, and pluralization in Godot 4.3+
Scanned 6/3/2026
Install to Claude Code
npx -y skills add jame581/GodotPrompter --skill localization --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Localization?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jame581-localization)More formats (shields.io, HTML) on the badges page.
---
name: localization
description: Use when implementing localization (i18n/l10n) — TranslationServer, CSV/PO translation files, locale switching, RTL support, and pluralization in Godot 4.3+
---
# Localization in Godot 4.3+
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.
---
## 1. Core Concepts
### How Godot Localization Works
1. **Wrap all user-facing strings** in `tr()` — Godot's translation function
2. **Create translation files** (CSV or PO) mapping keys to translated strings
3. **Import translation files** as `Translation` resources
4. **Switch locale at runtime** via `TranslationServer.set_locale()`
All `Control` nodes with `text`, `tooltip_text`, or `placeholder_text` properties are automatically translated if the value matches a translation key.
### Translation Key Strategies
| 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.
---
## 2. Translation Files
### CSV Format
The simplest format. First column is the key, subsequent columns are locale codes.
```csv
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):
- **Delimiter**: Comma (default) or Tab
- **Translations** section: enable/disable individual locales
### PO Format (Gettext)
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"
```
### Registering Translations
**Project Settings → Localization → Translations → Add...** → select your `.csv` or `.po` files.
Or register at runtime:
```gdscript
var translation := load("res://translations/cs.po") as Translation
TranslationServer.add_translation(translation)
```
```csharp
var translation = GD.Load<Translation>("res://translations/cs.po");
TranslationServer.AddTranslation(translation);
```
---
## 3. Using tr() in Code
### GDScript
```gdscript
# 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
```
### C#
```csharp
string labelText = Tr("MENU_START");
string healthText = string.Format(Tr("PLAYER_HEALTH"), currentHealth);
// Pluralization
string msg = TrN("ONE_ENEMY", "MANY_ENEMIES", count);
```
### Automatic Control Translation
`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_mode` to `DISABLED`.
---
## 4. Switching Locale at Runtime
### GDScript
```gdscript
# 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()
```
### C#
```csharp
public void SetLanguage(string localeCode)
{
TranslationServer.SetLocale(localeCode);
}
string current = TranslationServer.GetLocale();
```
### Language Selection Menu
```gdscript
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"])
```
---
## 5. Right-to-Left (RTL) Support
For Arabic, Hebrew, Persian, and other RTL languages.
### Enabling RTL
```gdscript
# On any Control node
control.layout_direction = Control.LAYOUT_DIRECTION_RTL
# Or set globally in Project Settings:
# Internationalization → Rendering → Text Direction → RTL
```
### Per-Control Settings
| 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 |
### RichTextLabel BBCode for Mixed Direction
```gdscript
# Force LTR for a number or URL inside RTL text
rich_text.text = "النتيجة: [ltr]100/200[/ltr]"
```
### C# parity
```csharp
// 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]";
}
}
```
### Font Requirements
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.
---
## 6. Locale-Aware Formatting
### Numbers
```gdscript
# 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
```
### Dates and Times
Godot doesn't provide built-in locale-aware date formatting. Use `Time.get_datetime_dict_from_system()` and format manually per locale.
### C#
```csharp
using Godot;
using System.Globalization;
public partial class LocaleFormatter : Node
{
public string FormatNumber(double value)
{
var culture = CultureInfo.GetCultureInfo(TranslationServer.GetLocale().Replace("_", "-"));
return value.ToString("N", culture);
}
// FormatCurrency and FormatDate follow the same pattern — same culture lookup,
// ToString("C", culture) and ToString("d", culture) respectively.
}
```
---
## 7. Project Organization
### Recommended File Structure
```
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
```
### Translation Keys Convention
```
# 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
```
---
## 8. Common Pitfalls
| 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)` |
---
## 9. Editor Locale Preview (Godot 4.5+)
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.
### How to Use
1. Open **Project → Project Settings → Internationalization**.
2. Find the **Preview Language** dropdown.
3. Select a locale from the list of registered translations (e.g., `ja`, `cs`, `ar`).
4. The editor viewport updates immediately to reflect the selected locale.
### Benefits
- Identify layout issues caused by longer translated text without entering Play mode.
- Verify RTL layout direction for Arabic, Hebrew, and Persian.
- Confirm that all Control nodes with text properties are properly wrapped in `tr()` keys (untranslated keys show up as-is in non-English preview).
- Faster translation QA — iterate directly in the editor.
> Reset to the default locale by selecting the blank or `en` entry in the **Preview Language** dropdown. The preview applies only in the editor and does not affect exported builds.
---
## 10. CSV Plural and Context Support (Godot 4.6+)
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.
### New CSV Columns
| 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.) |
### Example CSV with Context and Plural
```csv
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
```
### Using Context in Code
```gdscript
# 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")
```
```csharp
// Translate with context
string fileNoun = Tr("ITEM_FILE", "noun");
string fileVerb = Tr("ITEM_FILE", "verb");
```
### Using Plural in Code
```gdscript
# 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
```
```csharp
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.
---
## 11. Implementation Checklist
- [ ] All user-facing strings use `tr()` (or are set as translation keys on Control nodes)
- [ ] Translation files (CSV or PO) are registered in Project Settings → Localization → Translations
- [ ] Language can be switched at runtime via `TranslationServer.set_locale()`
- [ ] Language preference is saved and restored on game launch
- [ ] Fonts cover all target language character sets (Latin, CJK, Arabic, etc.)
- [ ] RTL languages have `layout_direction` set to `RTL` or `LOCALE` on root UI containers
- [ ] Format strings (`%s`, `%d`) are applied AFTER `tr()`, not before
- [ ] Translation keys follow a consistent naming convention
- [ ] UI layout adapts to longer/shorter text in different languages (no hardcoded widths)
- [ ] PO format is used for languages with complex plural rules
- [ ] Editor Locale Preview (Project Settings → Internationalization → Preview Language) is used for translation QA instead of test runs (Godot 4.5+)
- [ ] CSV `?context` column used when the same key has different meanings in different UI contexts (Godot 4.6+)
- [ ] CSV `?plural` / `?pluralrule` columns used for simple one/other plurals in CSV workflow; PO format used for languages with 3+ plural forms (Godot 4.6+)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!