In my previous post on this topic, I demonstrated how to localise a game in Godot 4.4 using one or more CSV files. Now I’m going to focus on the second method of localisation, which uses gettext.
- Advantages and disadvantages of gettext
- Installing gettext
- Project setup
- Generating the translation template
- Translating the game
- Importing the translations
- Adding new translations
- Updating our locale files
- Translating resources
- Plugins to the rescue!
- Translation keys from json files
- gettext localisation summary
Once again, the Godot Documentation has a good tutorial on how to set up localisation with gettext, but it doesn’t explain how to set up automatic registration of translation keys from resources or other data sources, such as json files.
I’ve uploaded the completed version of this tutorial to github. There’s a separate branch for C# and GDScript.
Advantages and disadvantages of gettext
There are several advantages compared to using a CSV file.
- gettext is a standard format for translation services. It can be edited with any text editor, although this is trickier than just editing a CSV, or using a more specialised editor, such as poedit
- There are multiple online translation services that use gettext editors, so it’s easier to collaborate online with other people. I’ve not tried to use any of these services, but some examples are Transifex and Weblate
- gettext works better with version control, e.g. Git, when compared to CSV files
- It is easier to edit multiline strings with gettext than in a CSV file
- Godot can quickly generate and regenerate the template file. This adds new keys from any scene, and removes keys which are no longer present. You can also setup parsers to add keys from resources and json files. This removes a lot of the potential for human error, and improves scalability
However, there are a couple of disadvantages too.
- CSV files are easier to grasp if you’re new to localisation when compared to gettext
- Anyone who maintains translation files generated by gettext will need to install gettext tools on their system. This isn’t particularly difficult, but may be annoying.
Installing gettext
To update your template file, you will need the gettext tools. Where you get them depends on your operating system.
- Windows – download an installer from here
- MacOS – you can install via homebrew
- Linux – on most distros, you can install gettext via your package manager (for me, it was already installed)
Project setup
I’m going to use the same project that I set up and duplicated in my previous post.
All this is is a new project with a Main scene and a script attached to the parent node. I write my game code in C#, but I’ve also had a go at GDScript. The GDScript versions might not be particularly well-written though.
C# Main.cs
public partial class Main : Node
{
public override void _Ready()
{
// this grabs the language to use
// from Godot's internationalization test settings
var testLanguage = ProjectSettings
.GetSetting("internationalization/locale/test")
.ToString();
if (testLanguage != string.Empty)
{
// use the test language
TranslationServer.SetLocale(testLanguage);
}
else
{
// use the language from the OS
TranslationServer.SetLocale(Godot.OS.GetLocaleLanguage());
}
}
}
GDScript main.gd
func _ready():
# language taken from internationalization test settings
var language = ProjectSettings.get_setting("internationalization/locale/test")
# if no language is set, use the language from the OS
if language == null:
TranslationServer.set_locale(OS.get_locale_language())
else:
TranslationServer.set_locale(language)
I’ve also copied the simple UI from the final version of that project.


I’ve kept the translation keys too. With gettext, it’s easy to choose which strings to replace, but you can still end up with situations where the context of a word matters. The example I gave previously is “close” as in “close the door” compared to “close” as in “the ogre is close to me.” I’d prefer to avoid these problems entirely and just stick to translation keys.
With everything ready, let’s make our translation file.
Generating the translation template
This is a very easy step, especially when compared to having to set up a CSV file.
Open up your Project Settings and go to Localization > POT Generation. Add your Main scene and your Inventory Scene

Then click “Generate POT”
Godot will ask you where to save the file and what to call it. I’ve created a new folder called “Locale” and saved the file as “messages.pot”
You can open this file with a text editor (e.g. Notepad++, or Kate) – it should look something like this.
# LANGUAGE translation for Localisation-gettext for the following files:
# res://Main.tscn
# res://Inventory/Inventory.tscn
#
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Localisation-gettext\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8-bit\n"
#: Main.tscn
msgid "CHARACTER_SHEET_TITLE"
msgstr ""
#: Main.tscn
msgid "CHARACTER_NAME_LABEL"
msgstr ""
#: Main.tscn
msgid "SELECT_BUTTON"
msgstr ""
#: Main.tscn
msgid "CLOSE_BUTTON"
msgstr ""
#: Inventory/Inventory.tscn
msgid "INVENTORY_HEADER_LABEL"
msgstr ""
#: Inventory/Inventory.tscn
msgid "INVENTORY_ITEM_HEADER"
msgstr ""
#: Inventory/Inventory.tscn
msgid "INVENTORY_ITEM_QUANTITY"
msgstr ""
You’ll notice that every translation key has been added, along with a comment about which scene it’s used in.
You can also open the file in poedit (or similar), where you’ll see a list of translation keys and a warning that the POT file is just a template that doesn’t contain any translations.

So we have a way to automatically add translation keys into a file. No more manually copying and pasting required. That’s awesome!
Excluding controls
However, there is a minor problem here. Let’s add a static string that we don’t want to translate – e.g. the name of the game.

Update the POT file by going into Project Settings > Localization > POT Generation and clicking “Generate POT” again. You’ll be prompted to choose a file name once more – you can safely overwrite your existing one, which you should do since a lot of the power of using gettext comes from just updating your template with new strings.
Now if we open the file, either in a text editor or poedit, we’ll see that a new translation key has been added – for the game name.

Fortunately, the Godot editor makes it really easy for us to exclude a control from translation.
Select the label you don’t want to translate and go to the very bottom of the Inspector tab. Here, you’ll see a section for Node > Auto Translate, which you can expand to find a Mode dropdown. Set that to “Disabled.”

Now if you regenerate your POT file, MY AWESOME GAME will no longer be a translation key.
Let’s start adding translations!
Translating the game
If you’re using a translation service, you can skip this section and just upload your POT file to that service. I would definitely recommend using a proper service for any real translations. For the purposes of this tutorial, however, I am not using such a service, and you may want to do your native language yourself.
The first step to using our POT file for translations is creating a locale file. If you don’t want to use poedit, you can run the following command in the same directory you saved the POT file.
msginit --no-translator --input=test.pot --locale=en
Replace the locale code with your desired language. I want to set up the localisation for my native language first. The above command will create a file named en.po. To add translations, you simply find the key you’re interested in and update the msgstr.
#: Main.tscn
msgid "CHARACTER_SHEET_TITLE"
msgstr "Character Sheet"
Alternatively, you can create a translation file from poedit by going to File > New From POT/PO File and then choosing your POT file in the prompt. You will then be prompted to choose your desired language, and can type in your translation in the translation box.

You’ll need to make sure you save this file in poedit. The program should suggest en.po in the folder where the POT file is.
Go ahead and add a translation for all your translation keys.

You’ll need to do this for each language you want your game to be in. I’ve created translations for French and German.
This is definitely a lengthier process than the CSV method. However, most of the time, you’ll just be maintaining the file for your native language, with the other languages handled by other people. The translation files can also be compressed down into .mo files, which can help with keeping the size of your game down.
Importing the translations
Let’s get these translations into the game. For me, poedit has automatically generated a compressed .mo file from the .po files, so I’m going to use those. If you don’t have .mo files, .po files work fine.
Go to Project Settings > Localization > Translations and click “Add…” From there, you can choose your translation files.

Now, when we run the game, those long translation keys will be replaced by the translations you added to the .po file.

And we can test the other languages by going into Project Settings with Advanced Settings switched on, and then finding Internationalization > Locale. Here, we can enter our test locale into the Test field.

Once you’ve done this, make sure to save in the editor or your choice will not be used.
Thanks to the code in our Main script, Godot will detect that we’ve chosen a test locale and apply the correct translations.

I’m very impressed with how easy Godot have made it to test different locales, regardless of what method you use to do the translations.
Let’s start adding some more features, and their translations, to this game.
Adding new translations
In the CSV version of my localisation prototype, we added items to the inventory via a script at runtime. Let’s do the same thing here.
In C#, we can do it like this:
public override void _Ready()
{
ItemsContainer = GetNode<GridContainer>("%ItemsContainer");
// using translation names to the add to grid function
AddItemToGrid(TrN("ITEM_SWORD", "ITEM_SWORDS", 1), 1);
AddItemToGrid(TrN("ITEM_POTION", "ITEM_POTIONS", 3), 3);
}
private void AddItemToGrid(string name, int quantity)
{
// the Tr function is a shortcut to TranslationServer.Translate
ItemsContainer.AddChild(new Label { Text = name });
ItemsContainer.AddChild(new Label { Text = quantity.ToString() });
}
And in GDScript:
func _ready():
# using translation keys when adding the items
add_item(tr_n("ITEM_SWORD", "ITEM_SWORDS", 1), 1)
add_item(tr_n("ITEM_POTION", "ITEM_POTIONS", 3), 3)
func add_item(item_name, quantity):
var name_label = Label.new()
name_label.text = item_name
var item_quantity = Label.new()
item_quantity.text = str(quantity)
$%ItemsContainer.add_child(name_label)
$%ItemsContainer.add_child(item_quantity)
If you read my previous post, you may have spotted that I’ve used the TrN function in C# and tr_n in GDScript vs Tr/tr. This is because localising via gettext lets us use pluralisation in our translations. The first string passed to TrN is the singular version, the second is the plural version, and the number at the end is the number of things that could be singular or plural.
I’ve added one sword (singular) and three potions (plural) to demonstrate this.
Running the game at this point doesn’t really do anything, except that we can see the plural version of potions is used.

And now for another great trick in the Godot Editor.
If you’re using GDScript, you can add scripts to the POT generation.
At the time of writing, those of us using C# cannot do this, but there is an open PR to add this functionality to the editor.
For now, we’ll use our GDScript file. Let’s start by adding it to our POT generation in the editor.

I’ve opened the new version of the POT file in poedit. At first glance, it might appear that something’s gone wrong as there’s only a translation key for ITEM_SWORD and ITEM_POTION – the plural version isn’t there. But, when I click on either of these, I can see that there’s an entry for both Singular and Plural. Godot has picked up the use of tr_n and automatically set up both a singular and plural entry. That’s very impressive!
If you’re dead set on C# (which I am), don’t fret. I don’t like to hard code translation keys into my code files if I can help it, so we’ll use resources and a custom parser later to get around this limitation.
But, for now, how do we get these new keys into the locale files?
Updating our locale files
If you’re not using poedit, you can run the following command in the folder that contains your pot and po files.
msgmerge --update --backup=none fr.po messages.pot
Replace “fr.po” with whichever locale you want to use.
Opening the .po file in a text editor, you’ll see that we now have the extra keys, with multiple msgstr fields. I’ve commented which one is singular and which is plural here.
#: Inventory/inventory.gd
msgid "ITEM_SWORD"
msgid_plural "ITEM_SWORDS"
msgstr[0] "" #: singular
msgstr[1] "" #: plural
#: Inventory/inventory.gd
msgid "ITEM_POTION"
msgid_plural "ITEM_POTIONS"
msgstr[0] "" #: singular
msgstr[1] "" #: plural
You will need to do this for each .po file.
If, like me, you are using poedit, you can open your po file and update from there. Go to Translations > Update from POT file, and choose your POT file when prompted.
When you click on one of our new keys, you’ll see, at the bottom, there are two tabs. Form 1 is the singular, and Form 2 is the plural.

You’ll need to repeat this for each language – potentially tedious, but you might not have to bother in a real project if you use a translation service. Remember to save your updated files!
Running the game now displays the translated text, as expected.

However, if we’re adding things dynamically, then most of the time we don’t want to hard code our translation strings. How can we get around that problem?
Translating resources
One way is to use one of Godot’s biggest features – resources. Let’s make an items resource.
My resource file in C# looks like this:
[GlobalClass]
public partial class Item : Resource
{
[Export]
public string Name { get; set; }
[Export]
public string[] Traits { get; set; }
}
The equivalent file in GDScript is:
extends Resource
@export var name: String
@export var traits: PackedStringArray
For this game, items have a name and can have multiple traits.
I’ve added three resources. The first is a potion:

The second is a sword:

And the third is a warhammer:

So that we can see the traits in the game, I’ve also expanded the inventory items grid to three columns and added a new header.

I’ve also updated the inventory script to load in the resources, and add them to the grid. As part of this, we’ll join the traits array into a single string and add it to the grid.
C# script:
public override void _Ready()
{
ItemsContainer = GetNode<GridContainer>("%ItemsContainer");
// loading the items from their resource files
var sword = GD.Load<Item>("res://Inventory/Items/Sword.tres");
var potion = GD.Load<Item>("res://Inventory/Items/Potion.tres");
var warhammer = GD.Load<Item>("res://Inventory/Items/Warhammer.tres");
// adding the items to the grid
AddItemToGrid(sword, 1);
AddItemToGrid(potion, 3);
AddItemToGrid(warhammer, 1);
}
private void AddItemToGrid(Item item, int quantity)
{
// the TrN function is a shortcut to TranslationServer.Translate
ItemsContainer.AddChild(new Label { Text = TrN(item.Name, $"{item.Name}S", quantity) });
ItemsContainer.AddChild(new Label { Text = quantity.ToString() });
// mapping the traits to their translated version and joining them into a single string
ItemsContainer.AddChild(new Label { Text = string.Join(", ", item.Traits.Select(trait => Tr(trait))) });
}
GDScript:
func _ready():
# loading the items from resources
var sword = load("res://Inventory/Items/Sword.tres")
var potion = load("res://Inventory/Items/Potion.tres")
var warhammer = load("res://Inventory/Items/Warhammer.tres")
# using translation keys when adding the items
add_item(sword, 1)
add_item(potion, 3)
add_item(warhammer, 1)
func add_item(item, quantity):
var name_label = Label.new()
# tr_n is the function that translates the given string
name_label.text = tr_n(item.name, "{str}S".format({"str": item.name}), quantity)
var item_quantity = Label.new()
item_quantity.text = str(quantity)
# mapping the traits to their translated version
var traits = Array(item.traits).map(tr)
var trait_label = Label.new();
trait_label.text = ", ".join(traits)
$%ItemsContainer.add_child(name_label)
$%ItemsContainer.add_child(item_quantity)
$%ItemsContainer.add_child(trait_label)
And now let’s update our POT file. You’ll notice that when we click “Add…” .tres files are not valid. Oh well, our script is still selected in the GDScript version of the project. (C# people hang on – we’re nearly at the part where we can start automatically adding translation keys again).
Let’s just try regenerating the POT file with what we’ve got.

Unfortunately, but not unexpectedly, the Godot editor hasn’t worked out that we want to include the translation keys from the resources, even though they’re referenced in our script.
We need a plugin to parse resource files.
Plugins to the rescue!
We’re going to write our very own plugin to find translation keys in resource files. Don’t worry – it’s not too hard.
To start with, open your Project Settings and go to Plugins. Once there, click “Create New Plugin” and you’ll be prompted to name it.

If you’re using GDScript, set the language to GDScript, and you’ll be able to select “Active now” – unfortunately for us C# fans, we have to rebuild the project before we can activate our plugin.
You don’t have to set a specific subfolder if you don’t want to. However, you can only have one plugin per folder, so it’s a good idea to create a subfolder that matches your plugin.
Once you’re happy, click Create, and you’ll see that a new folder has been added to your project’s file system – addons.
Before we can make our plugin, though, we first need to create a translation parser for resources. This is just a script file. I’ve added mine in the same folder as the plugin script file since they’re going to be used together.
The parser needs to inherit from EditorTranslationParserPlugin.
The C# version of this script:
#if TOOLS
using Godot;
using Godot.Collections;
[Tool]
public partial class ResourceTranslationParser : EditorTranslationParserPlugin
{
public override Array<string[]> _ParseFile(string path)
{
// load the resource
var resource = GD.Load(path);
// check if it's an Item
if (resource is Item item)
{
// if so, parse it as an item
return ParseItemResource(item);
}
return base._ParseFile(path);
}
// you will need to parse each resource type separately
public Array<string[]> ParseItemResource(Item item)
{
var ret = new Array<string[]>();
// add the item name translation key and its plural
// since I'm English, I've just used a very simple pluralisation
ret.Add([item.Name, "", $"{item.Name}S"]);
// add all the trait translation keys
foreach (var trait in item.Traits)
{
ret.Add([trait]);
}
return ret;
}
// this function tells the editor that we want to be able to parse .tres files
public override string[] _GetRecognizedExtensions()
{
return ["tres"];
}
}
#endif
And GDScript:
@tool
extends EditorTranslationParserPlugin
func _parse_file(path):
var res = load(path)
var item = load("res://Inventory/Items/item.gd").new();
if res.get_class() == item.get_class():
return parse_item_resouce(res)
return []
func parse_item_resouce(item):
var ret: Array[PackedStringArray] = []
ret.append(PackedStringArray([item.name, "", "{str}S".format({"str": item.name})]))
for t in item.traits:
ret.append(PackedStringArray([t]))
return ret
func _get_recognized_extensions():
return ["tres"]
You’ll notice that, even though we only have one resource type at the moment, I’ve created a separate function to parse the resource and check the type before parsing it. That’s so that, when we add more resource types, we can extend this resource parser by adding a new type check and parsing function.
In C#, we also need to add the [Tool] decorator to our resource class so that it can be used by plugins.
using Godot;
// this if is very important, otherwise the plugin can't parse the resource type
#if TOOLS
[Tool]
#endif
[GlobalClass]
public partial class Item : Resource
{
[Export]
public string Name { get; set; }
[Export]
public string[] Traits { get; set; }
}
Now we update the plugin file to use this new parser.
C#:
#if TOOLS
using Godot;
namespace Plugins
{
[Tool]
public partial class ResourceLocalisationPlugin : EditorPlugin
{
// creating a new instance of our parser
ResourceTranslationParser ResourceParser = new ResourceTranslationParser();
public override void _EnterTree()
{
// initialise the plugin
// here we add our parser
AddTranslationParserPlugin(ResourceParser);
}
public override void _ExitTree()
{
// cleaning up the plugin
if (IsInstanceValid(ResourceParser))
{
RemoveTranslationParserPlugin(ResourceParser);
ResourceParser = null;
}
base._ExitTree();
}
}
}
#endif
GDScript:
@tool
extends EditorPlugin
var resource_parser: EditorTranslationParserPlugin
func _enter_tree() -> void:
# Initialization of the plugin goes here.
resource_parser = load("res://addons/resource_localisation/resource_translation_parser.gd").new()
add_translation_parser_plugin(resource_parser)
pass
func _exit_tree() -> void:
# Clean-up of the plugin goes here.
remove_translation_parser_plugin(resource_parser)
pass
You may need to close and reopen the editor at this point, and C# users like me will need to rebuild the project before we can activate the plugin.
Now, when we go into Project Settings > Localization > POT Generation, we can add our .tres resource files.

And clicking Generate POT adds all the translation keys from the item resources!

With this, it’s incredibly easy to get every translation key referenced in our project with the click of a single button. I think that’s amazing, and I’m very impressed that this is built in to the Godot Editor.
What about other file types? Can we also grab translation keys from them?
Translation keys from json files
Another common format for data is json. Let’s say that we’ve decided we want to store monster data as json.
I’ve added a new “Bestiary” folder to my project with the monster data in “bestiary.json”, and two C# classes that represent the Bestiary object. If you’re doing GDScript, you don’t need these two files.

The json file looks like this.
{
"Monsters": [
{
"Name": "MONSTER_PIXIE_NAME",
"Description": "MONSTER_PIXIE_DESCRIPTION",
"Size": "SIZE_TINY"
},
{
"Name": "MONSTER_DRAGON_NAME",
"Description": "MONSTER_DRAGON_DESCRIPTION",
"Size": "SIZE_HUGE"
}
]
}
I’ve already set all my strings to be translation keys.
And the two C# classes are:
public class Bestiary
{
public Monster[] Monsters { get; set; }
}
public class Monster
{
public string Name { get; set; }
public string Description { get; set; }
public string Size { get; set; }
}
Next we need to create a new plugin. I tried adding my json parser to the existing resource localisation plugin, but found that it would only pick up file types for the first translation parser plugin added. So separate plugins it is! I called mine “Json Localisation,” which is unimaginative, but descriptive.
Then we create a translation parser for json files in the same way we made a resource parser. Here’s the C# version:
#if TOOLS
using System.Text.Json;
using Godot;
using Godot.Collections;
[Tool]
public partial class JsonTranslationParser : EditorTranslationParserPlugin
{
public override Array<string[]> _ParseFile(string path)
{
var ret = new Array<string[]>();
var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
var json = file.GetAsText();
if (DeserializeMonsterData(json, ret))
{
return ret;
}
return ret;
}
private bool DeserializeMonsterData(string json, Array<string[]> ret)
{
// I don't really like doing it this way - trying to deserialise into each type we need
// and moving onto the next type if there's an exception feels like very bad code
// but since it's just for a tool rather than game code, it will do.
try
{
// Note: this will throw an exception if it doesn't deserialise into the given type
var data = JsonSerializer.Deserialize<Bestiary>(json);
foreach (var monster in data.Monsters)
{
// adding the plural version for each monster too
ret.Add([monster.Name, "", $"{monster.Name}S"]);
ret.Add([monster.Size]);
ret.Add([monster.Description]);
}
return true;
}
catch (JsonException)
{
return false;
}
}
public override string[] _GetRecognizedExtensions()
{
return ["json"];
}
}
#endif
There’s a comment there about how I don’t like the way I’ve handled checking what type the json object is. Unfortunately, I’ve not found a better way to do it, although I’m sure there is one. If you do know of a better way, please let me know in the comments!
The GDScript json parser looks like this:
extends EditorTranslationParserPlugin
func _parse_file(path: String):
var file = FileAccess.open(path, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
# check if there's a Monsters key at the top level
if data.has("Monsters"):
# if so, assume it's the bestiary data and parse it
# note: this is not foolproof - if there's another json file with monsters
# as a top level key, then that would also be parsed here, so you need to be
# very careful
return parse_bestiary_data(data.Monsters)
func parse_bestiary_data(monsters):
var ret: Array[PackedStringArray] = []
for m in monsters:
# adding a pluralisation option to the monster name
ret.append(PackedStringArray([m.Name, "", "{str}S".format({"str": m.name})]))
ret.append(PackedStringArray([m.Description]))
ret.append(PackedStringArray([m.Size]))
return ret
func _get_recognized_extensions():
return ["json"]
My lack of familiarity with GDScript does mean that there are better ways to type-check a json file. If so, please let me know, and I suggest you use that instead!
Finally, we update our plugin script.
C#:
#if TOOLS
using Godot;
[Tool]
public partial class JsonLocalisationPlugin : EditorPlugin
{
// creating a new instance of our parser
JsonTranslationParser JsonParser = new JsonTranslationParser();
public override void _EnterTree()
{
// initialise the plugin
// here we add our parser
AddTranslationParserPlugin(JsonParser);
}
public override void _ExitTree()
{
// cleaning up the plugin
if (IsInstanceValid(JsonParser))
{
RemoveTranslationParserPlugin(JsonParser);
JsonParser = null;
}
base._ExitTree();
}
}
#endif
And GDScript:
@tool
extends EditorPlugin
var json_parser: EditorTranslationParserPlugin
func _enter_tree() -> void:
# Initialization of the plugin goes here.
json_parser = load("res://addons/json_localisation/json_translation_parser.gd").new()
add_translation_parser_plugin(json_parser)
pass
func _exit_tree() -> void:
# Clean-up of the plugin goes here.
remove_translation_parser_plugin(json_parser)
pass
Finally, we check that the plugin is enabled (and maybe disable/re-enable and reopen the editor), and we can add json files to POT Generation!

And if we run Generate POT…

…we get all our monster translation keys in the POT file. Ready for someone to translate them!
The beauty of this is that you can quite easily write a similar plugin for each file type you want to localise, and, from then on, Godot will automatically add any new translation keys when you update the file. This feels incredibly powerful to me.
gettext localisation summary
Phew, it’s been a long journey getting all that set up! Definitely a bit more work than the CSV version. Ultimately, however, I think this is significantly easier to scale up and is far less prone to human error.
Let’s summarise the general process:
- Add our translation keys into the UI
- Disable Auto Translate for any controls you don’t want to be translated
- Add the scenes to POT Generation in Project Settings > Localization
- Run Generate POT and either:
- Hand over the template file to a translation service, or
- Use something like poedit to make a localisation file for each language you want and fill these out yourself
- Import the .po or .mo files back into Godot via Project Settings > Localization > Translations
That doesn’t feel much harder than doing it via CSV, and you get the added bonus of pluralisation, along with better multi-line support.
But now we get into the true power of using POT generation. We can write plugins to process resources and other file types, parsing out the translation keys so Godot can add them to the POT file, ready for translation.
I showed you how to write a parser for Godot’s resource files, as well as for json files. It shouldn’t be too much work to add even more file types if you need them.
This does come with a maintenance cost though. You do need to write a custom parser for each type of resource, or each json schema. If you change the resource or json file schema, you then need to update your parser. For small games, this may not be worth it. For larger games with lots of text, reducing human error could save many hours of pain.
Overall, I like this method better than the CSV method, but there are definitely use cases for both.