Translate custom Resources in Godot
Tue Mar 19 2024
What's this about?
You might already be familiar with Godot's Resource
class. But if not, it's basically a data container. Quoting Godot's documentation:
Resource is the base class for all Godot-specific resource types, serving primarily as data containers. Since they inherit from RefCounted, resources are reference-counted and freed when no longer in use.
Basically, they are light and simple and are the go-to when you need to have some data, independent from the node tree. They can be both saved into a scene file directly or saved externally as a file.
Resources are great for any game that has multiples of similar "things," which aren't scenes. Like items in an RPG. They could have a name and a texture, and then we can store references to them anywhere we need—in the player inventory, in a loot table, or in an item scene, which uses that data to show the item in the game world.
Godot also has a great translation mechanism using gettext, which is a system made specifically for translations where the idea is that instead of coming up with a key for a string intended for translation, like LABEL_HEALTH
, you just use the primary language, e.g., English, as the key, so just "Health," which makes both development and translation much easier. Gettext also uses its own file formats. "POT" for a translation template (POT stands for "portable object template"), and the files containing translations: "PO," where the translations are stored in a plain text format, and "MO," where the translations are stored in a binary format, making gettext performant for large games and applications. These formats are supported by multiple applications to make creating and editing them easier, like Poedit.
Translating using gettext in Godot is quite easy: you open Project Settings → Localization → POT Generation and add your "tscn" files, if they contain text anywhere, e.g., in Label
, Button
, and similar nodes, and "gd" files, if they contain tr()
calls.
Then you click "Generate POT," save the POT file somewhere, and then use something like Poedit to create a translated PO file from it and subsequently add it to the Translations dialog in Project Settings → Localization → Translations, and, well, that's it.
The problem with this is that "tres" and "res" files, which are the default file formats for Resource
, aren't supported by this. So if you have resources like items and they have strings that should be translated, like a name
property, you have to add all of those names to the POT file manually, which is very clunky and makes translation templates hard to maintain.
Also, if you want to translate these fields, you have to feed them to the translation mechanism manually via calls to tr()
in the code. Using the following approach, these fields will be translated automatically.
What is the solution for this? In short, it's an EditorTranslationParserPlugin. It's a great solution a user called "vaartis" on GitHub came up. Basically, it tells Godot that "tres" files are parsable and allows you to control what parts of the Resource
should be added to the POT file instead of you having to do that manually.
This will work regardless, whether you translate via CSV or gettext!
Implementation
Data
First of all, we need something to work with. We will create a new Resource
, create a couple of this new type, and save them as files. Create a new script with the following contents:
class_name Item
extends Resource
@export var id: int
@export var name: String
@export_multiline var description: String
You can name the file however you like. Something like item.gd
.
So, now we told Godot that we want to add a Resource
type called Item
that has three properties. Two of those, name
and description
, we will want to translate.
Now we can use this data in other scripts, like @export var item: Item
, as well as create them as files in the FileSystem tab via the Create New → Resource dialog. Let's create three of them. I'll make something like an "Apple," "Pear," and "Orange," because I'm that creative, with some descriptions and random ids.
After creating the resources, when you click their files in the FileSystem tab, you can see the exported properties in the Inspector and edit them:
Plugin
We need to make an addon that will host our EditorTranslationParserPlugin
. To do that, go to Project Settings → Plugins and click "Create New Plugin." Just enter a name; everything else is optional. Then click "Create":
Godot will create an "addons" folder if it doesn't already exist and create a folder based on your plugin name and the boilerplate of the plugin structure inside of it.
Navigate inside of it and open "[your-plugin-name].gd", and replace it with the following contents:
@tool
extends EditorPlugin
var parser_plugin: EditorTranslationParserPlugin
func _enter_tree():
# Initialization of the plugin goes here.
parser_plugin = load("res://addons/yourpluginname/parser_plugin.gd").new()
add_translation_parser_plugin(parser_plugin)
func _exit_tree():
# Clean-up of the plugin goes here.
remove_translation_parser_plugin(parser_plugin)
Basically, we are telling our "addon" to load our EditorTranslationParserPlugin
, which we will create shortly when the addon loads, and then unload it when the addon unloads. Super simple. Now, in the same plugin folder, create a script called parser_plugin.gd
, and replace its contents with the following:
@tool
extends EditorTranslationParserPlugin
func _parse_file(path: String, msgids: Array[String], msgids_context_plural: Array[Array]) -> void:
var res: Resource = load(path)
if not res:
return
if res is Item:
var item: Item = res as Item
msgids.append(item.name)
msgids.append(item.description)
func _get_recognized_extensions() -> PackedStringArray:
return ["tres"]
So, here we are creating a plugin that extends EditorTranslationParserPlugin
, and we override two of its virtual methods: _parse_file
and _get_recognized_extensions
, the simplest of which, _get_recognized_extensions
, tells Godot that our plugin accepts files with the "tres" file extension.
That means that the path
that _parse_file
receives is guaranteed to have the "tres" extension; therefore, we are safe to assume that we can load it as a Resource
. Then we check if this Resource
is an instance of Item
, and if so, we just add its name
and description
to the msgid array msgids
for gettext. In case you need pluralization or context (e.g., the meaning of the word "bank" changes depending on its context), you can use the msgids_context_plural
array; you add an array to it where the first element is the singular msgid, the second element is the context, and the third is the plural form. And that's it! Select Project → Reload Current Project for the changes to take effect, and now we can translate our Items!
Go back to the Translations dialog in Project Settings → Localization → Translations and add the "tres" files representing your Item
s (the "tres" extension is not shown by default, so you have to select "All Files"):
Now click "Generate POT" and set a path for the POT, and click "Save." Now, if you open the generated POT file in a text editor or in Poedit, you will see that it contains both the name and description fields of our Item
files, and they are ready to be translated!
Conclusion
While Godot supports the "legacy" CSV method of translation, I highly recommend using gettext since it requires way less maintenance, like the need to remove unused keys, and has a well-supported ecosystem of software built around it.
Resource translation takes a minute to set up, but after that's done, you will save a lot of time in iteration!
Big thanks to @vaartis for sharing this approach!