Fluent Translation for Godot via a Rust GDExtension.
TranslationFluent
resource or Project Settings.FluentGenerator
singleton.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:
tr(TranslationFluent.args("message", { ... }))
internationalization/fluent/parse_args_in_message
to be enabled.tr("message", { ... })
addons
folder.godot
folder of this repository.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 with pattern_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!
internationalization/fluent/generator/locales
project setting to define a list of locales to generate.internationalization/fluent/generator/file_patterns
project setting to define how files should be generated:
(.+)\.tscn
would find all scene files in your project.
{$locale}
is replaced with each of the locales listed in the locales
project setting (creating multiple files).{$n}
is replaced with the n-th capture group (so {$1}
would contain the first capture group that matched).res://i18n/{$1}.{$locale}.ftl
would create files like i18n/my_scene.en.ftl
in your project root.@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 toFile -> 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!