Fluent Translation for Godot via a Rust GDExtension.
- Load .ftl translation files via the
TranslationFluent
resource or Project Settings. - Support for args (variables), terms and attributes.
- Register custom functions that can be called in placeables.
- Generate .ftl files from scene files via the
FluentGenerator
singleton. - Optionally strip comments on exported FTL files.
If you simply wish to download and install this extension, keep reading.
If you are a developer and wish to build this extension yourself (e.g. to use a specific Godot version), go to BUILDING to learn more about your choices.
This extension can be downloaded in two different versions, each with their own benefits and downsides:
- Works with official releases of Godot v4.3 or newer.
- Available on the Asset Library.
- Has some limitations
- Translations that use variables are written like
tr(TranslationFluent.args("message", { ... }))
- Requires
internationalization/fluent/parse_args_in_message
to be enabled.
- Requires
- Translation files must be loaded via code, not via Project Settings.
- Translations that use variables are written like
- Requires a special custom ("forked") version of Godot, included as a separate download (see installation instructions). It might not be updated very frequently.
- Better engine integration
- Translations that use variables can be written like
tr("message", { ... })
- Translation files can be loaded via code or via Project Settings.
- Translations that use variables can be written like
Due to Godot's translation system being very inflexible, it is not possible for an extension to modify certain parts about it. While I'd love for all features to work with the official version of Godot, it is unlikely for all of my changes to be included in any upcoming version.
This is why you have the choice between a version that has better engine support, or one that "just works".
- Decide which extension version you want to use (see above).
- Download the corresponding zip release.
- The Default version is also available on the Asset Library.
- Extract the zip file contents to your project folder, so that the project root contains an
addons
folder. - Download a compatible version of Godot.
- Default: Godot v4.3 or newer.
- Forked: Download forked builds here.
- Start Godot editor. If it was already running, restart it to fix autocompletion.
- Follow the sample code to see if installation was successful. Remove any code which contains errors, as some of it only works depending on the version you've installed.
- You can also try one of the sample projects in the
godot
folder of this repository.
- You can also try one of the sample projects in the
func _init():
# Four ways to load FTL translations:
# 1. load(path) with locale in file name (Portuguese).
var tr_filename = load("res://test.pt_PT.ftl")
# 2. load(path) with locale in folder name (German).
var tr_foldername = load("res://de/german-test.ftl")
# 3. Manually create a TranslationFluent resource.
var tr_inline = TranslationFluent.new()
# Ensure that you fill the locale before adding any contents (English).
tr_inline.locale = "en"
# 4. Forked only - [Project Settings -> Localization -> Translations] and add a .ftl file there.
# You may need to change the file filter to "All Files" to see .ftl files in the file selector dialog.
# Godot automatically converts spaces to tabs for multi-line strings, but tabs are invalid in
# FTL syntax. So convert tabs to four spaces. Returns an error that you should handle.
var err_inline = tr_inline.append_from_text("""
-term = email
HELLO =
{ $unreadEmails ->
[one] You have one unread { -term }.
*[other] You have { $unreadEmails } unread { -term }s.
}
.meta = An attr.
""".replace("\t", " "))
# Define custom functions to use in messages.
# positional is an array, named is a dictionary.
tr_filename.add_function("STRLEN", func(positional, named):
if positional.is_empty():
return 0
return len(str(positional[0]))
)
# Register via TranslationServer.
TranslationServer.add_translation(tr_filename)
TranslationServer.add_translation(tr_foldername)
TranslationServer.add_translation(tr_inline)
func _notification(what: int) -> void:
if what == NOTIFICATION_TRANSLATION_CHANGED:
# Fluent supports $variables, which can be filled when translating a message.
# Default version: use a wrapper function to pass arguments:
$Label.text = atr(TranslationFluent.args("HELLO", { "unreadEmails": $SpinBox.value }))
# Forked version: pass arguments directly to tr() and friends:
$Label.text = atr("HELLO", { "unreadEmails": $SpinBox.value })
# The context field is used to retrieve .attributes of a message.
$Label2.text = atr("HELLO", "meta") # Default
$Label2.text = atr("HELLO", {}, "meta") # Forked
Tip
If you don't see some of these settings, make sure you have Advanced Settings enabled.
Localization
tab →Translations
tab: Add .ftl files in this page to automatically load them on startup (Forked version only).internationalization/locale/fallback
: Fallback locale is used when the selected language does not have a date/time/number formatter available.internationalization/fluent/use_unicode_isolation
: When mixing RTL with LTR languages, enable this to insert additional control characters for forcing the correct reading direction. See this page for a more detailed explanation.internationalization/fluent/parse_args_in_message
: Decides whether variables can be filled via the message parameter. This is the only way to pass args when using the Default version, so only makes sense to use in that case.
These settings only apply to translation files loaded by load()
or via project settings.
For manually created TranslationFluent
instances, custom logic can be implemented to emulate these settings.
internationalization/fluent/loader/locale_by_file_regex
: If specified, file name is first checked for locale via regex. Can contain a capture group which matches a possible locale. Always case-insensitive.internationalization/fluent/loader/locale_by_folder_regex
: If specified, the folder hierarchy is secondly traversed to check for locale via regex. Can contain a capture group which matches a possible locale. Always case-insensitive.internationalization/fluent/loader/pattern_by_file_regex
: If specified, file name is first checked for message pattern via regex. Can contain capture groups which can later be used construct the message pattern. Can be made case-insensitive by prefixing with(?i)
.internationalization/fluent/loader/pattern_by_folder_regex
: If specified, the folder hierarchy is secondly traversed to check for message pattern via regex. Can contain capture groups which can later be used construct the message pattern. Can be made case-insensitive by prefixing with(?i)
.internationalization/fluent/loader/message_pattern
: If specified together withpattern_by_*_regex
, decides how the pattern should be formatted. The placeholder{$n}
is replaced with the n-th capture group (so{$1}
would contain the first capture group that matched). A single capture group like(.+)
must be specified to capture the actual message. Can be made case-insensitive by prefixing with(?i)
.
These settings apply to the FluentGenerator
singleton:
internationalization/fluent/generator/locales
: See below.internationalization/fluent/generator/file_patterns
: See below.internationalization/fluent/generator/invalid_message_handling
: If a message identifier is invalid (e.g. contains symbols or spaces), should it be skipped or should the invalid symbols be replaced with underscores?
You can automatically extract message IDs from your scene files!
- Edit the
internationalization/fluent/generator/locales
project setting to define a list of locales to generate. - Edit the
internationalization/fluent/generator/file_patterns
project setting to define how files should be generated:- Both the key and the value should be type String.
- The key represents a regular expression to locale a set of source files. For example
(.+)\.tscn
would find all scene files in your project.- Note that capture groups can be used later, so make sure to make good use of non-capturing groups to ensure your group indices are consistent.
- The value represents the path to the generated FTL file. It can contain placeholders that get replaced:
{$locale}
is replaced with each of the locales listed in thelocales
project setting (creating multiple files).{$n}
is replaced with the n-th capture group (so{$1}
would contain the first capture group that matched).- For example, with the above regex,
res://i18n/{$1}.{$locale}.ftl
would create files likei18n/my_scene.en.ftl
in your project root.
- If a FTL file already exists (or is matched multiple times, e.g. by different patterns), it will be merged with the existing file. No messages are ever deleted, and existing messages will remain untouched.
- Run the generator by creating a tool script such as this one:
@tool
extends EditorScript
func _run() -> void:
var generator = FluentGenerator.create()
generator.generate()
Tip
To run an EditorScript
, open it in the script editor and go to File -> Run
.
This system provides maximal flexibility and very little maintenance once set up properly.
Currently, only .tscn
files are properly handled (similarly to the POT generator feature built into Godot).
A plug-in system to customize message extraction is planned but currently not possible to implement.
This is not a production-ready project and will likely have breaking API changes without warning. Please consider this if you intend on using this library.
Due to Godot needing breaking API changes to have this extension work, it is unlikely to become easily usable out-of-the-box. Not much I can do besides wait for another major release that would accept this breaking change.
Any help in continuing development for this library is welcome!