//rp.wtf

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.

Dropdown dialog for TextureRect's texture field, showing the various Resources it accepts

All of these are Resources. Some of them don't contain just image data, but also various properties.

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.

POT Generation dialog in Godot showing 4 added files

POT Generation dialog

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.

Translations dialog in Godot showing an added PO file containing a german translation.

Translations dialog in Godot showing an added PO file containing a german translation.

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:

Context menu path for creating a new script file in Godot.

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.

Create Resource dialog in Godot showing the new resource type Item as selected

Now we can create resources of the type Item

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:

Godot Node inspector for the custom Item resource

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's plugin creation screen

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 Items (the "tres" extension is not shown by default, so you have to select "All Files"):

POT Generation dialog in Godot showing 7 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!

The msgid table in Poedit showing 6 msgids.

The item resource data shows up in Poedit

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!