Software localization
Game Localization in Godot

Godot is a free cross-platform game engine for creating 2D and 3D games. Since its inception, the open-source game engine has been empowering game developers around the world to create their own custom games. In fact, it is an alternative solution to Unity 3D, but with no strings attached.
🗒 Note » Want to learn how to best go about localizing a Unity game? Check out the following tutorial: Localizing Unity Games with the Official Localization Package.
In this tutorial, we will take a look at how to implement multilingual support for a custom game in Godot. To get it started, download the Godot game engine and make sure it is executable on your machine. To keep it simple, we will use one of the demo projects provided by the official repository—feel free to clone the project and open it using the project.godot file.
Translation files
When it comes to translation files, the most common approach is to write translations in a CSV format, delimited by a comma, semi-colon, or tab.
The file must have the following syntax (here an example of the comma use case):
keys,<lang1>,<lang2>,<langN> KEY1,string,string,string KEY2,string,string,string
At the same time, lang must be one of the valid locales supported by Godot. No restriction on keys, but it is recommended to use UPPERCASE to differentiate from the normal string. Besides that, the casing is quite important for keys. Take a look at the following examples—they will all be treated as different keys:
- KEYS1
- KEYS_1
- keys1
- Keys1
Delimiter options
If you are using a different delimiter, simply select the CSV file, and head over to "Import" to change the Delimiter options:
Remember to hit the "Reimport" button to complete the import. Godot treats CSV files as translations by default. It will check for changes to the CSV file and generate the corresponding compressed translation resource file in the same working directory. Let us say you have defined your CSV file as follows:
,en,es,ja KEY_HELLO,Hello!,Hola!,こんにちは KEY_PUSH,Push Me!,Aprétame!,押す
You should see the following translation files:
- text.en.translation
- text.es.translation
- text.ja.translation
An issue might arise if the translation text contains a comma, line break, or double quotes. In that case, simply enclose the string in double quotation marks. You also need to escape any double quotes inside a double quote. Have a look at the following example:
HELLO_WORLD,"Hello, world!","¡Hola mundo!","ハローワールド!"
Using gettext
On the other hand, Godot provides support for loading translation files written in the GNU gettext format (with PO as the acceptable format). The PO file format has a few advantages over the CSV one:
- Each locale has its own translation file
- It is much easier to edit multi-line text in gettext files
However, it does come with a couple of disadvantages as well:
- gettext is a lot more complex and can be challenging for those who are new to software localization
- Godot only uses PO files and not the compiled message object files (MO)
- Godot uses its own parser and does not support all the features available under GNU; one of the most prominent missing features in Godot is pluralization.
In this tutorial, we will use CSV as the translation file format.
If you are familiar with gettext and would like to give it a try, check the following documentation to create the base POT file and the corresponding PO file for each locale.
Loading translation files
By default, you need to add/load the translation files manually to the system. You can easily load translation files via Project > Project Settings. Next, under the Localization tab select "Translations". You should now see the following user interface:
Click on the "Add" button to import new translations and the bin icon to remove a specific translation (please note that deleting a translation affects only the translation system—it will not delete the translation (CSV) file itself; it just limits the number of supported locales in your system.
Translation server
By default, Godot has its own translation server that facilitates and manages all translations, including the addition or deletion of translation files. You can think of it as a built-in module that detects if the text in your controls matches any of the keys in your translation file. Once that is done, based on the existing locale, it will automatically translate the text for you. You can also use it to change the locale during runtime.
Auto-translation
This comes in handy as certain controls such as Buttons and Labels will automatically fetch translation if the corresponding Text value matches one of the translation keys for the current locale. For example, you can set KEY_HELLO as the text in a Label control for auto-translation (we have previously defined it as one of the keys in the translation file):
Likewise, you can repeat the same step for the Button control. Modify the key based on what you have set in your translation file:
You should get the following screen when you run the scene.
Disabling automatic translation
While Godot can translate automatically for you, saving you time and resources for actual game development, this may be problematic in some cases. For example, if you do not want to translate a player's name that matches one of your translation keys, you can disable the auto-translation feature by using the following script:
func _ready(): # assuming that the script node contains a Label node called NameLabel var label = get_node("NameLabel") label.set_message_translation(false) label.notification(NOTIFICATION_TRANSLATION_CHANGED)
Translating a text programmatically
Godot also provides a convenient function called tr()
which translates a message based on an input string that represents the translation key. This is extremely useful since it allows you to translate a text during runtime in the code:
# change the name of the boss boss.set_text(tr("LEVEL_5_BOSS")) # change the game status depending on the current status_index # if status_index is 1, it will use translation for key GAME_STATUS_1 status.set_text(tr("GAME_STATUS_" + str(status_index))) # interpolate and format the translated text with variable count # assuming POINTS is "You gained %d points!" # and count is 5 points.text = tr("POINTS") % [count] # You gained 5 points!
Pluralization
Although the auto-translation feature does not support pluralization, there are several ways to do it programmatically, especially for simple plurals. For example, you can add a new key and postfix it with a special word for pluralization:
POINTS,You gained %d point,Ganaste %d punto,%dポイントを得た POINTS_PLURAL,You gained %d points,Ganaste %d puntos,%dポイントを得た
Next, create a new function that checks the input value and returns the plural key if the latter is valid and the input value exceeds the desired threshold.
func get_plural_key(key, value, n=2): var plural_key = key + '_PLURAL' var text = tr(plural_key) if text != plural_key and value >= n: return plural_key return key
Call the function as follows:
var msg = tr(get_plural_key("POINTS", count)) % [count] # if count >= 2 # You have gained 3 points! # count < 2 # You have gained 1 point!
Changing the locale
You can easily change the locale of your game at runtime by calling the set_locale
function:
TranslationServer.set_locale('es')
For example, you can change the locale to Japanese when users click the Button control:
... func _on_ja_button_pressed(): TranslationServer.set_locale('ja')
Moreover, you can extend these capabilities to use the OptionButtons control and support multiple locales. Let us assume that you have the following items in the control:
You can easily obtain the selected index when users select an item—simply map it to the corresponding locale as follows:
var language_map = {0: "en", 1: "es", 2: "ja"} func _on_LanguageOption_item_selected(index): TranslationServer.set_locale(language_map[index])
Here is a screen example of the OptionButtons control with 3 locales:
Formatting strings
Auto-translation is limited to translating plain text across locales. If you have dynamic variables, you need to format your string programmatically.
Placeholder types
As mentioned earlier, you can format a string using the %
placeholder.
# integer var msg = "You have gained %d points!" % 5 # You have gained 5 points! # string var msg = "You have gained %s points!" % "five" # You have gained five points! # multiple placeholders var msg = "You have gained %s silvers and %s golds" % ["two", "five"] # You have gained two silvers and five golds
You can use it in conjunction with translation files and the tr()
function. Imagine that you have the following key values in your CSV file:
... POINTS,You gained %d point,Ganaste %d punto,%dポイントを得た
You can call it and format the string as follows:
var count = 5 var msg = tr("POINTS") % [count] # You have gained 5 points!
🗒 Note » Find the complete list of supported placeholder types here.
Placeholder modifiers
You can set certain modifiers as well to format the string:
# pad a string, 2 leading spaces for a total of 7 var msg = "%7d" % 12345 # " 12345" # pad with 0 instead of whitespace if integer stars with 0 var msg = "%07d" % 12345 # "0012345" # set to 3 precision using . var msg = "%10.3f" % 12345.6789 # " 12345.679" # dynamic padding using *, will fill it as %7.3f var msg = "%*.*f" % [7, 3, 8.8888] # " 8.889
Escape sequence
If you want to use the % character into a format string, you need to escape it with another % character:
var msg = "Health: %d%%" % 30 # Health 30%
Format function
Additionally, there is a format() function that supports both array and dictionary as input:
var msg = "Welcome to {company}!".format({"company": "Phrase"}) # Welcome to Phrase! # order is not important for dict var msg = "Hi, {name}. Welcome to {company}".format({"company": "Phrase", "name": "Bob"}) # Hi, Bob. Welcome to Phrase # using array instead of dictionary, order is important var msg = "Hi, {name}. Welcome to {company}".format(["Bob", "Phrase"]} # Hi, Bob. Welcome to Phrase
A few other localization aspects
Translating the project name
The app name is derived from the project name. If you want to set the project name in multiple locales, simply head over to "Project Settings" under Projects and add a new string property as follows:
The property must follow the following syntax:
application/config/name_<locale>
Finally, fill in the corresponding translation for the project name.
Localizing resources
Godot supports the localization of resources, allowing you to use an alternate version of assets, like images, audio files, etc, depending on the current locale. Imagine that you have the following Image with the path set to flag_uk.png.
You can easily localize it to use a different image by adding the corresponding assets in the "Remaps" tab under Project Settings:
Select a resource and click the "Add" button at the bottom right to include the corresponding assets for another locale. You can then select the desired locale for the newly added asset by clicking the dropdown list of locales supported by Godot.
Simply select the desired locale for the imported image manually. In our case, Spanish and Japanese are selected as part of the localized assets. In fact, you can localize resources for any locales even if there are no corresponding translation files in your project. Let us say that you have selected Arabic during runtime and the project doesn't have an Arabic asset—remap will not happen, and the asset will stay unchanged.
Likewise, the same can be done for an audio asset. If the base locale is English (en), the user interface of your game will be as follows:
Upon changing the locale to Spanish (es), Godot will translate the text and remap the assets as follows:
Formatting dates
When it comes to date formatting, Godot only provides a get date function where a dictionary of keys and the value inside the keys are in integer. This may be an issue if you want to localize dates in your game.
Nevertheless, there is a workaround for formatting dates:
- Set the date format for another locale.
- Fill in the translations for each month from January to December.
- Define a function to return the date string.
We will start by defining the date format and corresponding translations in our CSV file; here is an example for 3 locales (English, Spanish, and Japanese):
,en,es,ja ... MONTH_1,January,Enero,1 MONTH_2,February,Febrero,2 MONTH_3,March,Marzo,3 MONTH_4,April,Abril,4 MONTH_5,May,Mayo,5 MONTH_6,June,Junio,6 MONTH_7,July,Julio,7 MONTH_8,August,Agosto,8 MONTH_9,September,Septiembre,9 MONTH_10,October,Octubre,10 MONTH_11,November,Noviembre,11 MONTH_12,December,Diciembre,12 DATE_FORMAT,"{month} {day}, {year}","{day} {month} {year}","{year}年{month}月{day}日"
Next, add the following function in your script:
func get_today_date_string(): var today = OS.get_date() # keys available: day, dst, month, weekday, year var date_dict = {"month": tr("MONTH_" + str(today["month"])), "day": str(today["day"]), "year": str(today["year"])} return tr("DATE_FORMAT").format(date_dict)
Assume that you have a label control called DateLabel; simply call the function to change the text to the current date when users change the locale:
... func _on_LanguageOption_item_selected(index): TranslationServer.set_locale(language_map[index]) $DateLabel.text = get_today_date_string()
The English user interface should be as follows:
When users change the locale to Japanese (ja), they should see the following:
Conclusion
Well done, you're now equipped with the basics of localizing your custom game in Godot. If you want to take your localization process to the next level, consider signing up for Phrase, the most reliable software localization platform on the market. It comes with a 14-day free trial and will provide you with everything you need to:
- Build production-ready integrations with your development workflow,
- Invite as many users as you wish to collaborate on your projects,
- Edit and convert localization files online with more context for higher translation quality.
If