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:
- Define a mapping of
key, Japanese, English, Koreanentries in a translation file (e.g., CSV) - Register the translation file in Project Settings
- 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.
-
Create a CSV file:
- Create a
translations/folder in your project directory - Create
game_text.csv
- Create a
-
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
keysas a locale code. Always use valid locale codes likeja,en,koas column names. Names likecommentorcontextmay 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.
- Place the CSV file: Save it as
res://translations/game_text.csv - Verify auto-import: The editor recognizes the CSV and generates per-language
.translationresources automatically (e.g.,game_text.ja.translation,game_text.en.translation,game_text.ko.translation) - Register in Project Settings:
- Go to "Project" -> "Project Settings" -> "Localization" tab
- In the "Translations" section, click "Add"
- Add all generated
.translationfiles (ja,en, andko-- 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_modeproperty enabled by default. Setting a translation key directly as text can auto-translate it without explicitly callingtr(). Note thatauto_translate_modeis 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.
| Recommendation | Description |
|---|---|
| Use uppercase + underscores for keys | Keep a consistent format like START_GAME, LEVEL_COMPLETE |
| Use placeholders | Embed dynamic values with {key} |
| Verify font support | Ensure appropriate fonts are applied for each language (especially CJK characters) |
| Set a fallback language | Untranslated text should fall back to a primary language like English |
| Disable auto-translation | Turn off "Internationalization -> Editor Translation -> Use Editor Translation" in EditorSettings |
| Use CSV management tools | Manage translations with Excel or Google Sheets for team collaboration |
| Keep context info outside the CSV | Manage 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 likecontextornotes, 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