Internationalization in Godot
Overview
Section titled “Overview”This note covers what we did for Skeleseller (which used C#) to internationalize/localize the game. Godot has documentation on the general process here.
For translation, the high-level plan was to use machine translations as a foundation and community contributions to improve the machine translations. Note: the languages to target, at least in 2025, seem to be:
- French
- Italian
- German
- Spanish
- Simplified Chinese
- Japanese
- Korean
- Brazilian Portuguese
At a high level, you need to perform these steps:
- Finding strings in your codebase that should be translated
- Translate those strings (see Translation services)
- Use the translated strings accordingly
- This is simply a matter of calling into Godot’s translation functions (
Tr,TrN,TranslationServer.Translate,TranslationServer.TranslatePlural)
- This is simply a matter of calling into Godot’s translation functions (
Finding strings in your codebase that should be translated
Section titled “Finding strings in your codebase that should be translated”We used xgettext for this. For C#, xgettext can only find function calls with string literals:
- ✅
Translate("hi") - ❌
Translate(someString)- This won’t be found even if
someStringdefinitely points to a string.
- This won’t be found even if
As such, it’s best to wrap every translated string in some set of known function calls (like Translate or TranslatePlural). However, you can pass --extract-all to xgettext to have it extract every string from your files. This would include strings like Color.FromHtml("c70000") which don’t need to be localized. If you want to prevent finding those, you could make xgettext only search certain files/directories and consolidate all of your strings there.
Here’s the documentation for xgettext. It’s quite helpful and has some examples.
We run xgettext with the following arguments:
- “—keyword=TranslationServer.Translate:1”
- “—keyword=TranslationServer.Translate:1,2c”
- “—keyword=Translate:1”
- “—keyword=Translate:1,2c”
- “—keyword=TranslatePlural:1,2”
- “—keyword=TranslatePlural:1,2,4c”
- “—keyword=Tr:1”
- “—keyword=Tr:1,2c”
- “—keyword=TrN:1,2”
- “—keyword=TrN:1,2,4c”
- “—from-code=UTF-8”
- “—language=C#”
- “—add-comments=I18N”
- “—sort-by-file”
- “—force-po”
- “—files-from=/path/to/a_file_with_all_filepaths_in_it_on_separate_lines.txt”
- “—output=/path/to/a_pot_file.pot”
Explanations:
- The
--keywordarguments are for finding calls in the source C# code. The “c” that’s appended to some of the numbers indicates thecontextargument. For example,Translate:1,2cwill matchTranslate("Hello", "Greetings")in C# and emit it to the.potfile.- The context can be helpful if you have many strings that are the same in English but may not be the same in other languages. For example, you may have an ability called
Boulderand an item calledBoulder. The context could be set to"Abilities"or"Items"appropriately.
- The context can be helpful if you have many strings that are the same in English but may not be the same in other languages. For example, you may have an ability called
--add-comments=I18Nmeans that comments in C# that start withI18Nwhich immediately precede a translation call will make it to the.potfile. This can be helpful if there’s any context you want to add, e.g.// I18N This should be a short string.- We do not use
--join-existingso that the.potfile will be generated anew every time we runxgettext. This is so that comments don’t accumulate in the file, e.g. due to changing the line number of a translated string in C# and then callingxgettextagain.- The
.pofiles themselves are specific to each language and will maintain any comments starting with#in them even acrossxgettextgenerations. By using these comments, we can mark metadata about a string like whether it was translated by a machine or a human.
- The
Translation services
Section titled “Translation services”Azure and Google Cloud Platform have very permissive translation services:
Azure’s allows 2 million characters per month for free. They even allow you to train a custom model, but that won’t work for indie game devs since it requires ten thousand training sentences (reference).
I.e. using these services are basically like going to Google Translate, typing in text, and getting the output in another language. It’s not going to let you provide context or do any meaningful training (unless you already have ~10k sentences, which is doubtful).
Community contributions
Section titled “Community contributions”Originally, I wanted to write some custom, in-game code to allow people to see string identifiers and have a way to suggest new strings. Everyone said I over-engineered the solution, and I agreed, so I ended up using Weblate. It’s a self-hostable tool that lets you get translations from the community.
Formatting strings for localization
Section titled “Formatting strings for localization”There are a few cases to accommodate:
Pluralized strings
Section titled “Pluralized strings”Godot has built-in functions for these: TrN and TranslationServer.TranslatePlural:
TranslatePlural( "You have {0} unspent skill point.", "You have {0} unspent skill points.", points),This will generate something like this in the .pot file:
#: Src/YourFile.cs:122#, csharp-formatmsgid "You have {0} unspent skill point."msgid_plural "You have {0} unspent skill points."msgstr[0] ""msgstr[1] ""Strings with parameters
Section titled “Strings with parameters”Wrap your Translate call in string.Format (since you need Translate to take in a string literal for xgettext to find it):
string.Format( Translate( "Erratically shoots at a single enemy for {0} damage, but has a {1} chance to miss.", "Abilities" ), FormatNumber($"{damage:0}"), FormatNumber($"{MissChance:P0}"));Strings with BBCode
Section titled “Strings with BBCode”IMO, you should not have any BBCode enter your localization files because:
- Your machine translations may try translating them.
- They inflate the file and make it harder to tell what’s actually localized.
In the example below, the English string emitted is "5 damage (crit!)" (“crit!” is in red):
int damage = 5;string critMessage = $"[color=red]{Tr("(crit!)")}[/color]";string damageString = string.Format(Tr("{0} damage {1}"), damage, critMessage);As shown above, the [color=red] and its corresponding closing tag, [/color], will not appear in the .pot file:
#: Src/YourFile.cs:119msgid "(crit!)"msgstr "(crit_in_another_language!)"
#: Src/YourFile.cs:120#, csharp-formatmsgid "{0} damage {1}"msgstr "{0} damage_in_another_language {1}"Other helpful tips
Section titled “Other helpful tips”- Godot has built-in pseudolocalization (reference)
- Obsolete and fuzzy strings
#~represents an obsolete string (one that doesn’t correspond to an ID in the.potfile) (reference).- A
#, fuzzycomment means that an update was made to the source such that the translation probably needs to be looked at. For example, if you change “Fireball” to “Fireball 2” in C#, you’ll get the “fuzzy” tag in the resulting.potand.pofiles. This means a translator should look at it.