Game Localization in Godot

A step-by-step guide to implementing multilingual support for your custom games 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:

Changing the Delimiter options of our CSV file in the import menu | Phrase

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:

Adding a new translation file | Phrase

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):

Setting the KEY_HELLO as the text in a Label control for auto-translation | Phrase

Likewise, you can repeat the same step for the Button control. Modify the key based on what you have set in your translation file:

Setting the KEY_HELLO as the text in Button control for auto-translation | Phrase

You should get the following screen when you run the scene.

Running the scene | Phrase

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:

OptionButtons control with multiple supported locales | Phrase

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:

OptionButtons control with 3 locales | Phrase

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:

Adding a new string property as follows to "Project Settings" under Projects | Phrase

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.

English locale image | Phrase

You can easily localize it to use a different image by adding the corresponding assets in the “Remaps” tab under Project Settings:

Localized locale images | Phrase

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.

Selecting the right image for our locale | Phrase

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:

English locale image | Phrase

Upon changing the locale to Spanish (es), Godot will translate the text and remap the assets as follows:

Spanish locale image | Phrase

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:

English date formats | Phrase

When users change the locale to Japanese (ja), they should see the following:

Japanese date formats | Phrase

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

Comments