【Godot】Implementing Localization with TranslationServer

Created: 2026-02-08

Learn how to use Godot's TranslationServer for localization. Covers creating CSV translation files, dynamic language switching, and using placeholders.

Overview

Tested with: Godot 4.3+

Reaching players worldwide requires localization (i18n: internationalization). If you need to "display UI text in Japanese, English, and Korean" or "automatically switch based on player language settings," Godot's TranslationServer provides the solution.

Localization might sound complex, but Godot makes it surprisingly simple. There are essentially three steps -- prepare translation data in a CSV, register it in the project, and call tr() in your code. That's all it takes to support any number of languages without changing your code.

This article covers practical implementation from managing translations with CSV files to dynamic language switching and leveraging placeholders, using Japanese, English, and Korean as a three-language example.

How TranslationServer Works

Let's start with the big picture. TranslationServer maps translation keys to language-specific text. Instead of hardcoding strings like "Start Game" or "ゲームスタート" directly in your code, you use common keys like START_GAME and retrieve the appropriate text at runtime based on the current language.

Why is this approach necessary? If you hardcode text in one language, supporting another means modifying your code. As you add 3, 5, or more languages, the number of changes becomes unmanageable and error-prone. Translation keys let you add languages without touching the code at all.

Basic flow:

  1. Define a mapping of key, Japanese, English, Korean entries in a translation file (e.g., CSV)
  2. Register the translation file in Project Settings
  3. Call tr("key") in your code to get the text for the current language

Example:

# When the current language is Japanese
tr("START_GAME")  # -> "ゲームスタート"

# When the current language is English
tr("START_GAME")  # -> "Start Game"

# When the current language is Korean
tr("START_GAME")  # -> "게임 시작"

The calling code is identical in every case -- only the returned text changes based on the language setting.

Creating a CSV Translation File

Now that you understand how TranslationServer works, let's prepare the actual translation data. CSV format is recommended for managing translation data. It's simple and can be edited with Excel or Google Sheets, which means translators who aren't programmers can easily contribute -- a huge advantage for team workflows.

  1. Create a CSV file:

    • Create a translations/ folder in your project directory
    • Create game_text.csv
  2. CSV structure:

keys,ja,en,ko
START_GAME,ゲームスタート,Start Game,게임 시작
SETTINGS,設定,Settings,설정
QUIT,終了,Quit,종료
HEALTH,体力,Health,체력
SCORE,スコア,Score,점수
LEVEL_COMPLETE,ステージクリア!,Level Complete!,스테이지 클리어!

Rules:

  • Row 1 (header): keys (translation key column) followed by locale codes (ja, en, ko) as column names
  • Row 2+: key name, Japanese text, English text, Korean text, ...
  • Key naming convention: uppercase with underscores (START_GAME, LEVEL_COMPLETE)
  • To add a new language, simply add a column in the header and fill in the translations

tips: Godot 4's CSV importer treats every column name except keys as a locale code. Always use valid locale codes like ja, en, ko as column names. Names like comment or context may be misinterpreted as locales.

Registering in Project Settings

With your CSV file ready, let's get the Godot editor to recognize it. In Godot 4, CSV files placed under res:// are automatically imported. No manual file conversion is needed.

  1. Place the CSV file: Save it as res://translations/game_text.csv
  2. Verify auto-import: The editor recognizes the CSV and generates per-language .translation resources automatically (e.g., game_text.ja.translation, game_text.en.translation, game_text.ko.translation)
  3. Register in Project Settings:
    • Go to "Project" -> "Project Settings" -> "Localization" tab
    • In the "Translations" section, click "Add"
    • Add all generated .translation files (ja, en, and ko -- three files)

Once registered, the tr() function is ready to use in your code.

Displaying Translated Text with tr()

Once the registration is done, all you need to do is call tr() in your code. This is where the magic of localization happens -- you can add new languages without changing a single line of code.

Basic Usage

extends Control

func _ready():
    # Set translated text on labels
    $TitleLabel.text = tr("START_GAME")
    $SettingsButton.text = tr("SETTINGS")
    $QuitButton.text = tr("QUIT")

You pass the key name from the keys column of your CSV to tr(). If the current locale is ja, Japanese text is returned; en returns English; ko returns Korean. If a matching key isn't found, the key name itself is returned as-is.

tips: In Godot 4, UI nodes (Label, Button, etc.) have an auto_translate_mode property enabled by default. Setting a translation key directly as text can auto-translate it without explicitly calling tr(). Note that auto_translate_mode is a property of Node (moved from Control in Godot 4).

Using Placeholders

You'll often need to embed dynamic values in translated text, like "Score: 1500." In this case, define placeholders using {key} syntax in your CSV, then substitute values with GDScript's format() method.

CSV file:

keys,ja,en,ko
PLAYER_SCORE,スコア: {score},Score: {score},점수: {score}
HEALTH_STATUS,体力: {current}/{max},Health: {current}/{max},체력: {current}/{max}

GDScript:

func update_score_label(score: int):
    $ScoreLabel.text = tr("PLAYER_SCORE").format({"score": score})
    # ja: "スコア: 1500"
    # en: "Score: 1500"
    # ko: "점수: 1500"

func update_health_label(current: int, max_health: int):
    $HealthLabel.text = tr("HEALTH_STATUS").format({
        "current": current,
        "max": max_health
    })
    # ja: "体力: 75/100"
    # en: "Health: 75/100"
    # ko: "체력: 75/100"

Placeholder names ({score}, {current}, etc.) are shared across all languages. Translators leave the {score} part as-is and only translate the surrounding text.

Dynamic Language Switching

Now that translated text is displaying correctly, players will expect to be able to change the language from an options menu. You can change the language at any time during runtime using TranslationServer.set_locale(). After switching, you'll need to update any currently displayed text.

extends Control

func _ready():
    # Set the initial language to Japanese
    TranslationServer.set_locale("ja")
    update_ui()

func _on_language_option_selected(index: int):
    # Switch language
    match index:
        0: TranslationServer.set_locale("ja")  # Japanese
        1: TranslationServer.set_locale("en")  # English
        2: TranslationServer.set_locale("ko")  # Korean

    # Refresh all UI
    update_ui()

func update_ui():
    $TitleLabel.text = tr("START_GAME")
    $SettingsButton.text = tr("SETTINGS")
    $QuitButton.text = tr("QUIT")

Getting the current language:

var current_locale = TranslationServer.get_locale()
print(current_locale)  # "ja", "en", or "ko"

Auto-Detecting the OS Language

In addition to manual switching, auto-detecting the player's language on first launch makes for an even more polished experience. Godot provides OS.get_locale_language() to retrieve the OS language.

In the following example, if the OS language matches a supported language, it's used automatically. Otherwise, English is set as the default fallback.

extends Node

func _ready():
    # Get the OS language
    var os_locale = OS.get_locale_language()  # "ja", "en", "ko", "fr", etc.

    # Check if the language is supported
    var supported_locales = ["ja", "en", "ko"]
    if os_locale in supported_locales:
        TranslationServer.set_locale(os_locale)
    else:
        # Fall back to English for unsupported languages
        TranslationServer.set_locale("en")

    print("Language set to: ", TranslationServer.get_locale())

OS.get_locale() vs. OS.get_locale_language():

  • OS.get_locale() returns a format like "ja_JP" (includes region)
  • OS.get_locale_language() returns just the language code "ja"

Since TranslationServer's locale setting works with language codes alone, OS.get_locale_language() is the simpler choice.

Receiving Language Change Notifications

If your scene has many UI nodes, calling update_ui() on each one individually after a language change gets tedious fast. Use _notification() to detect locale changes -- when the language changes, NOTIFICATION_TRANSLATION_CHANGED is sent to all nodes, allowing each to handle its own update independently.

func _notification(what: int) -> void:
    if what == NOTIFICATION_TRANSLATION_CHANGED:
        # Called automatically when language changes
        _update_dynamic_texts()

func _update_dynamic_texts():
    $ScoreLabel.text = tr("PLAYER_SCORE").format({"score": current_score})

With this approach, only one place in your code calls TranslationServer.set_locale(), and each UI node handles its own text refresh. This keeps the code clean and maintainable.

Plural Forms (tr_n)

Some languages, like English, change wording based on quantity -- "1 item" vs. "5 items." You can handle this with tr_n(), which automatically switches between singular and plural.

# Requires PO files (.po) for plural form support (not supported in CSV)
var text = tr_n("ONE_ITEM", "MANY_ITEMS", item_count)
# item_count=1: "1 item"
# item_count=5: "5 items"

tips: tr_n() plural forms require PO (Gettext) files. CSV files cannot define plural forms, so consider using PO files if you need plural support. Note that languages like Japanese and Korean don't distinguish between singular and plural, so this feature primarily applies to European languages like English and French.

Best Practices

Here are key guidelines to keep in mind when integrating localization into your project. Establishing these rules before translation work ramps up will save significant rework later.

RecommendationDescription
Use uppercase + underscores for keysKeep a consistent format like START_GAME, LEVEL_COMPLETE
Use placeholdersEmbed dynamic values with {key}
Verify font supportEnsure appropriate fonts are applied for each language (especially CJK characters)
Set a fallback languageUntranslated text should fall back to a primary language like English
Disable auto-translationTurn off "Internationalization -> Editor Translation -> Use Editor Translation" in EditorSettings
Use CSV management toolsManage translations with Excel or Google Sheets for team collaboration
Keep context info outside the CSVManage translator notes in spreadsheet comments or separate sheets

Implementing a Fallback Language

When a translation key has no entry for the current language, Godot returns the key name as-is. You can take advantage of this by using English text as the key name itself, so untranslated languages naturally display English.

# Strategy 1: Use English as the key name (natural fallback)
# CSV: keys,ja,ko
#      Start Game,ゲームスタート,게임 시작
# -> For unsupported languages, "Start Game" is displayed as-is

# Strategy 2: Implement fallback in code
func get_text(key: String) -> String:
    var text = tr(key)
    if text == key:
        # Translation not found (key returned as-is)
        # Retrieve English version
        var current = TranslationServer.get_locale()
        TranslationServer.set_locale("en")
        text = tr(key)
        TranslationServer.set_locale(current)
    return text

tips: Only use valid locale codes (ja, en, ko, etc.) as CSV column names. If you add columns like context or notes, Godot 4's CSV importer may interpret them as locales. Manage translator notes in Google Sheets comments or a separate sheet instead.

Summary

  • TranslationServer maps translation keys to language-specific text
  • Create translation files in CSV format and register them in Project Settings
  • The tr() function returns the appropriate text for the current language based on the key
  • Placeholders ({key}) let you embed dynamic values in translated text
  • Dynamic language switching is done with TranslationServer.set_locale()
  • OS language auto-detection uses OS.get_locale_language()
  • Only use locale codes as CSV column names; manage context info outside the CSV
  • Set up a fallback language to handle untranslated text gracefully

Further Reading