The Complete Guide to Godot Game Localization

From TranslationServer to font fallbacks: localize your Godot game with CSV, PO files, GDScript, and automated AI translation.

1

TranslationServer Basics

Godot's built-in TranslationServer is the core of the localization system. It loads translation resources at startup and resolves keys through the tr() function. Every GDScript call to tr() goes through TranslationServer — no extra libraries needed.

TranslationServer basics
# TranslationServer is Godot's built-in localization system.
# It loads translations at startup and resolves keys via tr().

# Set the game's locale
TranslationServer.set_locale("ja")

# Get the current locale
var current = TranslationServer.get_locale()  # "ja"

# Translate a key — works everywhere in GDScript
var text = tr("MENU_START")  # "ゲームスタート"

# Translate with context (Godot 4.x)
var text = tr("OPEN", "verb")    # "Open" (action)
var text = tr("OPEN", "adj")     # "Open" (state)
TranslationServer supports CSV, PO (Gettext), and .translation (binary) formats. It auto-detects the system locale via OS.get_locale() and selects the matching translation resource. You can override the locale at any time with TranslationServer.set_locale().
2

CSV Translation Files

CSV is the simplest format for Godot translations. One file holds all languages in columns. The first column is the key, and each subsequent column is a locale. Godot auto-imports .csv files and generates .translation resources.

translations.csv
# translations.csv
# First column = key, subsequent columns = locale codes
keys,en,ja,de,es
MENU_START,Start Game,ゲームスタート,Spiel starten,Iniciar juego
MENU_SETTINGS,Settings,設定,Einstellungen,Configuración
MENU_QUIT,Quit,終了,Beenden,Salir
ITEM_SWORD,Sword,剣,Schwert,Espada
ITEM_SHIELD,Shield,盾,Schild,Escudo
DIALOG_GREETING,"Hello, adventurer!",冒険者よ、こんにちは!,"Hallo, Abenteurer!","¡Hola, aventurero!"
Importing CSV translations
# In Godot Editor:
# 1. Place your .csv file in the project (e.g., res://translations.csv)
# 2. Godot auto-imports it — creates .translation resources
# 3. Go to Project > Project Settings > Localization > Translations
# 4. Add the generated .translation files
Wrap values containing commas or newlines in double quotes. For values containing double quotes, escape them as "". Keep keys short and descriptive: MENU_START is better than menu_start_button_text_label.
3

PO / Gettext Translation Files

PO (Portable Object) files are the industry standard for software localization. Godot 4.x has native PO support with plurals, context disambiguation, and translator comments. Create one .po file per language in a locale/ directory.

locale/ja.po
# translations.po — Gettext format for Godot
# Place in res://locale/ja.po

msgid ""
msgstr ""
"Language: ja\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=1; plural=0;\n"

# Simple translation
msgid "MENU_START"
msgstr "ゲームスタート"

# Translation with context (disambiguates identical source strings)
msgctxt "verb"
msgid "OPEN"
msgstr "開く"

msgctxt "adj"
msgid "OPEN"
msgstr "開いている"

# Plural form
msgid "You collected %d coin."
msgid_plural "You collected %d coins."
msgstr[0] "%d枚のコインを集めました。"
Loading PO files
# Project Settings > Localization > Translations
# Add each .po file:
#   res://locale/en.po
#   res://locale/ja.po
#   res://locale/de.po
#   res://locale/es.po

# Or load programmatically:
func _ready():
    var translation = load("res://locale/ja.po")
    TranslationServer.add_translation(translation)
PO files support msgctxt for context disambiguation (e.g., 'OPEN' as a verb vs adjective), msgid_plural for plural forms, and translator comments (#. lines) to give translators context about where and how strings are used.
4

GDScript Translation Usage

Use tr() anywhere in GDScript to translate strings. Combine with GDScript's % operator for string formatting. For robust locale management, create an Autoload script that handles locale detection, persistence, and switching with signals.

Using tr() in scenes
extends Control

func _ready():
    # Simple key lookup
    $TitleLabel.text = tr("MENU_START")

    # With string formatting (positional)
    $GreetingLabel.text = tr("DIALOG_GREETING_NAME") % [player_name]

    # With multiple placeholders
    $StatusLabel.text = tr("PLAYER_STATUS") % [player_name, level, health]

    # Context-aware translation (Godot 4.x)
    $ActionButton.text = tr("OPEN", "verb")

    # Update UI when locale changes
    TranslationServer.set_locale("de")
    _update_ui()

func _update_ui():
    # Re-apply all translated strings
    $TitleLabel.text = tr("MENU_START")
    $GreetingLabel.text = tr("DIALOG_GREETING_NAME") % [player_name]
locale_manager.gd (Autoload)
# locale_manager.gd — Register as Autoload in Project Settings
extends Node

signal locale_changed(new_locale: String)

const SUPPORTED_LOCALES = ["en", "ja", "de", "es", "fr", "ko", "zh"]

func _ready():
    var system_locale = OS.get_locale_language()
    if system_locale in SUPPORTED_LOCALES:
        set_locale(system_locale)
    else:
        set_locale("en")

func set_locale(locale: String):
    TranslationServer.set_locale(locale)
    locale_changed.emit(locale)

func get_locale() -> String:
    return TranslationServer.get_locale()
Changing the locale with TranslationServer.set_locale() does not automatically update text that has already been rendered in your scenes. You must manually re-apply tr() to all visible labels, buttons, and text nodes after a locale change. Use signals from your locale manager to notify UI scenes to refresh.
5

Plurals and Placeholders

Godot handles plurals through PO file plural forms. Each language defines its own plural formula in the PO header. GDScript's % operator handles positional placeholders (%s for strings, %d for integers). For named placeholders, use String.replace().

Plural forms by language
# Godot uses Gettext PO plural rules.
# Each language defines its own plural formula.

# English (2 forms: one, other)
msgid "You collected %d coin."
msgid_plural "You collected %d coins."
msgstr[0] "You collected %d coin."
msgstr[1] "You collected %d coins."

# Japanese (1 form: other — no singular/plural distinction)
msgid "You collected %d coin."
msgid_plural "You collected %d coins."
msgstr[0] "%d枚のコインを集めました。"

# Russian (3 forms: one, few, many)
msgid "You collected %d coin."
msgid_plural "You collected %d coins."
msgstr[0] "Вы собрали %d монету."
msgstr[1] "Вы собрали %d монеты."
msgstr[2] "Вы собрали %d монет."
Placeholder formatting
# GDScript string formatting with tr()

# Positional placeholders with %
var msg = tr("SCORE_MSG") % [score]        # "Score: %d" → "Score: 1500"
var msg = tr("STATS") % [name, level, hp]  # "%s — Lv %d — HP: %d"

# Named placeholders (manual replacement)
var template = tr("WELCOME_BACK")
var msg = template.replace("{player}", name).replace("{days}", str(days))
Never hardcode plural logic like 'if count == 1'. Languages have wildly different plural rules: English has 2 forms, Russian has 3, Arabic has 6, and Japanese has 1. Let the PO plural system handle selection automatically. CSV files do not support plurals — use PO files for any content that needs plural forms.
6

Font Fallbacks for CJK, Arabic, and More

Your game's primary font likely doesn't include glyphs for Japanese, Korean, Chinese, Arabic, or Thai scripts. Godot 4.x supports font fallback chains — when a glyph is missing from the primary font, Godot checks fallback fonts in order. Without this, non-Latin text renders as empty squares.

Font fallback setup
# Godot 4.x supports font fallback chains.
# When a glyph is missing from the primary font, fallbacks are checked in order.

# In the Editor:
# 1. Create a LabelSettings or Theme resource
# 2. Set the primary font (e.g., Noto Sans for Latin)
# 3. Add fallback fonts: Noto Sans JP, KR, SC, Arabic

# Programmatically:
func setup_fonts():
    var font = FontFile.new()
    font.load_dynamic_font("res://fonts/NotoSans-Regular.ttf")

    var fallback_jp = FontFile.new()
    fallback_jp.load_dynamic_font("res://fonts/NotoSansJP-Regular.ttf")
    font.add_fallback(fallback_jp)

    $Label.add_theme_font_override("font", font)
RTL support
# Right-to-left (RTL) support for Arabic, Hebrew, etc.

# In the Editor:
# Select your Control node > Layout > Text Direction = RTL

# Programmatically:
func setup_rtl():
    var locale = TranslationServer.get_locale()
    var rtl_locales = ["ar", "he", "fa", "ur"]

    if locale.substr(0, 2) in rtl_locales:
        $Label.text_direction = Control.TEXT_DIRECTION_RTL
        $Container.layout_direction = Control.LAYOUT_DIRECTION_RTL
Use Google's Noto font family — it covers nearly all Unicode scripts. Add Noto Sans JP, Noto Sans KR, Noto Sans SC, and Noto Sans Arabic as fallbacks. For pixel art games, consider Noto Sans Mono or bitmap fonts that include CJK subsets. Keep total font size in mind — full CJK fonts can be 15-20MB each.
7

Scene and UI Localization

There are three approaches to localizing Godot scenes: translate in _ready() using tr(), use the Auto Translate property in the editor, or load entirely different scenes for languages that need different layouts (like RTL languages).

Scene localization approaches
# Approach 1: Translate in _ready() using tr()
extends Control

func _ready():
    $StartButton.text = tr("MENU_START")
    $SettingsButton.text = tr("MENU_SETTINGS")
    $QuitButton.text = tr("MENU_QUIT")

# Approach 2: Use auto-translate in the editor
# Set the Text property to the translation key (e.g., "MENU_START")
# and enable "Auto Translate" on the node.

# Approach 3: Locale-specific scenes for complex layouts
func load_localized_scene():
    var locale = TranslationServer.get_locale().substr(0, 2)
    var path = "res://ui/main_menu_%s.tscn" % locale
    if ResourceLoader.exists(path):
        add_child(load(path).instantiate())
    else:
        add_child(load("res://ui/main_menu_en.tscn").instantiate())
Auto Translate only works on the node's text property. If you set text dynamically in code after _ready(), auto-translate is overridden. For dynamically updated text, always use tr() explicitly in your code. Also note that auto-translate applies tr() to the literal text value — so the text property must contain the translation key, not the human-readable source string.
8

Smart Locale Fallback with LocaleChain

Godot's TranslationServer falls back directly to the project's default locale when a regional variant is missing. A pt-BR player with only pt-PT translations sees English instead of Portuguese. LocaleChain fixes this by merging translations from configurable fallback chains into TranslationServer at configure time.

LocaleChain plugin
# LocaleChain for Godot — smart locale fallback
# Install from Godot AssetLib or copy addons/locale_chain/ into your project

# Problem: Godot's TranslationServer falls back directly to the default locale.
# A pt-BR player with only pt-PT translations sees English, not Portuguese.

# Solution: One-line setup
func _ready():
    LocaleChain.configure()  # Uses built-in fallback chains

# Now pt-BR falls back to pt-PT → pt → default
# es-MX falls back to es-419 → es → default
# zh-Hant-HK falls back to zh-Hant-TW → zh-Hant → default

# Custom configuration:
func _ready():
    # Override specific chains
    LocaleChain.configure({"pt-BR": ["pt"]})

    # Full custom — only your chains
    LocaleChain.configure(
        {"pt-BR": ["pt-PT", "pt"], "es-MX": ["es-419", "es"]},
        false  # don't merge defaults
    )

    # Reset to original state
    LocaleChain.reset()
LocaleChain is a pure GDScript addon — no native extensions or engine modifications. Install it from the Godot AssetLib or copy the addons/locale_chain/ folder into your project. It works with CSV, PO, and .translation files.
9

Automate Game Translations

With your localization setup complete, translate your CSV or PO files using AI. Automate translation of game strings, UI text, item descriptions, and dialog — directly from your IDE or CI/CD pipeline.

Terminal
# Translate your Godot locale files with AI
# CSV files:
# In your IDE, ask your AI assistant:
> Translate translations.csv to Japanese, Korean, and German

# PO files:
> Translate locale/en.po to ja, ko, de

# Or use the CLI in CI/CD:
npx i18n-agent translate locale/en.po --lang ja,ko,de

# The tool preserves:
# - CSV column structure and delimiters
# - PO msgctxt, msgid_plural, and plural forms
# - Placeholder syntax (%s, %d, {name})
# - Comments and metadata headers
Translate incrementally. When you add new keys to your source file, translate just the diff rather than regenerating everything. This preserves any human-reviewed translations for narrative dialog or culturally sensitive content.

Automate Translation Quality

Catch missing keys and broken placeholders before they ship with i18n-validate. Test your UI with pseudo-translations using i18n-pseudo before real translations arrive.

Common Pitfalls

Translation Files Not Imported

Godot must import .csv and .po files before they are usable. If translations don't appear, check that your files are listed in Project Settings > Localization > Translations. For CSV, ensure Godot has generated .translation files in the .godot/imported/ directory.

CSV Values with Commas or Quotes Break Parsing

Values containing commas must be wrapped in double quotes. Values containing double quotes must escape them as "". A missing quote causes the entire row to parse incorrectly, often silently shifting all subsequent columns.

CJK or Arabic Text Shows Empty Squares

Your primary font doesn't include glyphs for these scripts. Add font fallbacks in your Theme or LabelSettings resource. Without fallbacks, missing glyphs render as empty rectangles. Use Noto Sans variants for comprehensive Unicode coverage.

Incorrect Plural Form Count in PO Header

If the nplurals value in your PO header doesn't match the actual number of msgstr entries, Godot may crash or show the wrong plural form. Always verify the Plural-Forms header matches the CLDR specification for each target language.

Auto Translate Overridden by Code

Setting a node's text property in GDScript after _ready() overrides the auto-translate result. Either use auto-translate exclusively (set text in editor, never in code) or use tr() exclusively in code. Mixing both leads to inconsistent behavior.

Recommended Project Structure

Project Structure
my_godot_game/
├── addons/
│   └── locale_chain/              # LocaleChain plugin (optional)
│       ├── fallback_map.gd
│       ├── locale_chain.gd
│       └── plugin.cfg
├── fonts/
│   ├── NotoSans-Regular.ttf       # Primary font (Latin)
│   ├── NotoSansJP-Regular.ttf     # Japanese fallback
│   ├── NotoSansKR-Regular.ttf     # Korean fallback
│   └── NotoSansArabic-Regular.ttf # Arabic fallback
├── locale/
│   ├── en.po                      # English (source)
│   ├── ja.po                      # Japanese
│   ├── de.po                      # German
│   ├── es.po                      # Spanish
│   └── ar.po                      # Arabic
├── translations.csv               # Alternative: CSV format
├── scenes/
│   └── ui/
│       ├── main_menu.tscn
│       └── settings_menu.tscn
├── scripts/
│   ├── locale_manager.gd          # Autoload for locale management
│   └── ui/
│       └── main_menu.gd
├── project.godot
└── export_presets.cfg

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Godot Localization FAQ