Giter Site home page Giter Site logo

redmser / godot-fluent-translation Goto Github PK

View Code? Open in Web Editor NEW
12.0 3.0 0.0 154 KB

Fluent Translation as Godot extension

License: Other

GDScript 5.35% Rust 89.31% Fluent 2.21% JavaScript 3.14%
fluent godot godot-engine i18n localization translation

godot-fluent-translation's Introduction

Godot Fluent Translation

Logo of Godot Fluent Translation

Fluent Translation for Godot via a Rust GDExtension.

Demo project

Features

  • Load .ftl translation files via the TranslationFluent resource or Project Settings.
  • Support for args (variables) and attributes.
  • Generate .ftl files from scene files via the FluentGenerator singleton.

Available Versions

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:

Default

Forked

  • 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.

Why two versions?

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".

Installation

  • Decide which extension version you want to use (see above).
  • Download the corresponding zip release.
  • Extract the zip file contents to your project folder, so that the project root contains an addons folder.
  • Download a compatible version of Godot.
  • 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.

Code Sample

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.add_bundle_from_text("""
-term = email
HELLO =
    { $unreadEmails ->
        [one] You have one unread { -term }.
       *[other] You have { $unreadEmails } unread { -term }s.
    }
    .meta = An attr.
""".replace("\t", "    "))

    # 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

Project Settings

Tip

If you don't see some of these settings, make sure you have Advanced Settings enabled.

General

  • 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.

Loader

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).

Generator

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?

FTL Generator

You can automatically extract message IDs from your scene files!

  1. Edit the internationalization/fluent/generator/locales project setting to define a list of locales to generate.
  2. 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 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).
      • For example, with the above regex, res://i18n/{$1}.{$locale}.ftl would create files like i18n/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.
  3. 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.

About this Project

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!

godot-fluent-translation's People

Contributors

redmser avatar

Stargazers

Jakub Wabik avatar voided avatar Sheb Fench avatar Luca Vazzano avatar  avatar hiroki arai avatar Oxters Wyzgowski avatar Dams4K avatar Marine S. avatar Sena avatar Bruno Macabeus avatar Vovkiv avatar

Watchers

 avatar Samuel DIDIER avatar  avatar

godot-fluent-translation's Issues

FTL Generator: Integrate into Godot Editor

Instead of user writing custom tool script, find a convenient place to expose the generator as a button out-of-the-box.

Ideas

  • Next to POT Generator tab, but unsure if this area is extensible.
  • Somewhere in the menus.
  • Custom button in Project Settings -> Fluent, but unsure if that is possible (inspector plugin maybe?).

Other ideas are welcome.

Allow registering custom functions

FluentBundle::add_function allows registering a custom function to call in FTL.
This could be exposed to GDScript to allow registering custom callables.

Remove comments from export

Simpler implementation of #6

Comments from exported games FTL files could be stripped, to avoid leaking information and to slightly reduce file size.

Support older Godot versions

As outlined in BUILDING.md, there are some APIs which only exist in v4.3 dev 6, locking us to this version.

Would be nice to allow going back to v4.2 or even v4.1 (anything earlier is impossible due to binary incompatibility).

Open Question: How do we run backwards compatibility methods, and choose to take that code path based on engine version? Ideally, we should make use of e.g. AutoTranslateMode/resize methods if they exist, but fallback to old APIs otherwise. So this shouldn't be cfg macros, but rather some form of dynamic calls and version check.

Will likely only affect the Default builds / AssetLib, since I'll release Forked versions only for latest Godot.
But of course older Forked builds can be self-compiled as well then.

FTL generation

Could take the PotGenerator class, create an abstract base class / plugin registration system, then allow generating FTL files the same way. Obviously needs larger upstream changes.

While POT uses English messages as translation keys, FTL has separate keys. So unlike POT, you'll want to enter translation keys into your UI, and have those get extracted like key = UNTRANSLATED. Not sure how this works with ETR/ETRN translations though (I don't think FTL supports spaces in translation keys, so those might need to be converted to a "compatible" format)...

Alternative approach would be a custom EditorPlugin that adds some kind of UI. This provides more flexibility:

  • It could be interesting to allow multiple FTL files split up in some way, e.g. by folder or by file.
  • Existing files can be merged so that existing changes don't get lost - new keys are appended.
  • Similar "translation extraction plugin" class that the user can write to decide how certain files should fill a FTL:
    • Would also support extracting attributes and terms.
    • Could let the user choose whether they want the messages themselves to be translation keys (with some "convert illegal characters to underscores/dashes" logic), or define custom translation keys.
    • Could generate boilerplate for plural selectors?

First draft for how to configure this (no UI, just plaintext multiline):

locales = ["en", "de", ...]

[source files regex]
[generated ftl files]
...
  • Each file in the project is iterated and matched against the specified source regex.
    • Capture groups can be re-used later.
  • If a file matches, loop over all configured locales.
    • For each locale, fill out the ftl file string's placeholders:
      • {$locale} is replaced with the current locale
      • {$n} is replaced with the n-th matched capture group
    • If the ftl file does not exist, create it.
    • If the ftl file already exists, merge it with the existing contents.
      • Create new messages.
      • Leave existing messages untouched.
      • Mark no longer existing messages with a comment for inspection.
  • Repeat this process for all source > ftl lines.

Example syntax:

ui/(.+)\.tscn
i18n/{$1}.{$locale}.ftl

Investigate binary AST storage

Games could store the AST in a binary serialization, so that parsing of .ftl files is not required at runtime. This could be done in a conversion step by an editor import plugin (related to #1). Formats like .png get imported and remapped to a .ctex file, this would be a similar approach.

This saves disk space, can combine multiple files into one, and could minify the data by e.g. removing all comment tokens.

See projectfluent/fluent-rs#352 for upstream support, although unlikely to happen.

Since fluent_bundle::FluentResource only takes a String as input, might need to use a compatible trait and/or insert entries by hand.

Continuous Deployment

I'm assuming that configs for this already exist, so implementing this should just be a matter of research and fine-tuning the configuration for this project.

CI

  • Compile and run linter.
    • Probably fine to only cancel on errors, but ignore linter stuff. My Rust isn't pretty :D
  • On master branch and for pull requests.
  • Publish custom tags + releases on my Godot fork, and pull forked engine builds from there instead.
    • The download instructions + gdextension releases page would simply link to the releases page on there.

CD

  • Build artifacts from CI.
    • Needs a separate workflow to create all needed files for a release:
      • Windows [Default] debug+release
      • Linux [Default] debug+release
      • Windows [Forked] debug+release
      • Linux [Forked] debug+release

Avoid "experimental-threads" feature

Need "experimental-threads" feature since Godot calls get_message threaded, or at least gd-rust seems to think so.

Might be an upstream issue (there are other similar issues on the repo, but not exactly the same).

Replace complex Project Settings with a plug-in API

Many project settings are too complex for their own good, using RegEx or Dictionary for very complex lookups and string manipulation.

Instead it would be good to have a plug-in API that allows the user to implement virtual methods like func _compute_locale(path: String) -> String which could in the end just do a RegEx lookup, but also anything else.

Not sure about the registration mechanism, since it would need to be available as soon as ResourceLoader is ready (during project startup). Maybe refer to plug-in classes by path or by class_name in ProjectSettings?

May depend on godot-rust/gdext#426

Better error handling

The error handling could be a bit more accurate. See some of the TODOs in the Rust code.

Only add it where it makes sense and where it doesn't add too much code complexity.

FTL Generator: API for construction

  • Create static method to make a new FTL Generator based on Project Settings.
  • Create instance methods to add/remove/get locales and patterns.

Platform Compatibility

See BUILDING.md for an up-to-date list of which platforms are supported.

Compiling

First of all, it'll help to know which platforms compile out-of-the-box and which ones need extra attention to get working.

I'd like to set up the CI/CD for all supported platforms, to make this easier to keep track of. While my fork builds compile on all major platforms, the CI/CD of this repo does not.

Testing

Even if a platform compiles, it would be nice to test if both the forked executable runs, and if the extension works as expected.

Create fallback build that works without forked Godot

  • Feature flag that decides whether your Godot build is forked or master/stable.
  • Translate args: instead of tr(msg, args, context) it could be something like tr(var_to_str(args) + msg, context)
    • So code would pass an empty args dict by default, but if it sees that the message starts with { it would parse the string to a Dictionary variant and use that for args.
    • Could create a helper function fluent_args(msg, args) which creates a string formatted like this.
  • #19

Upstream API changes to Godot

Applied in fork

Not yet implemented

  • Backwards compatibility methods for the "args" parameter.
  • ScriptEditor::open_file only checks for Script and JSON resources to determine what it can open
    • TODO: should check if this is also what determines if double-clicking a file in FileSystem dock opens the script editor

Edit FTL files directly in Godot

  • Make .FTL files editable in script editor (see #11 under "not yet implemented")
  • Add simple syntax highlighting - see how the vscode extension highlights stuff, which textmate rules it has

Resource importer for FTL files + Better API

Check the CSV importer for reference on how to implement this.

  • ResourceFormatLoader: works in game and in editor. Could load .ftl files at runtime.
  • EditorImportPlugin: only works in editor.
    • Variant 1: Saves .translation files like CSV, which would lose FTL data (only keeping messages with empty args) -> this approach does not make sense.
    • Variant 2: Store AST for quicker reconstruction of FTL data (see #6).

Ideally it should be possible to have multiple FTL files for one language, which can be merged. But neither a requirement, nor am I sure how Godot currently handles multiple Translations for the same locale. Would need to check for conflicting entries as well, or maybe adding multiple bundle files to the same Translation resource...

API should be improved e.g. by removing redundant language ID.

(Default Version) Automated loading of Translations on startup

Forked version allows Project Settings to contain .ftl files like normal. This does not work for default builds, see #11.

There are code workarounds for loading translations, so it's probably fine. Maybe could introduce a helper function which recursively finds all *.ftl files in the project and loads them? Could optionally filter by locale, or path glob.

(This would also make .ftl files show in the translation picker dialog - no need to do anything else)

CI: Use latest Godot version instead of a specific tag

For [Default]โ€‹ builds, we currently hard-code a specific tag to download. We should switch to latest: true and preRelease: true, but then following commands won't know the executable file name anymore.

Needs to use downloaded_files output.

We have to find out what the extracted executable file name is. Can this be inferred from the zip file name? Or do we need to look through the variable to find a matching executable?

Investigate usage of Time/TextServer singletons

Fluent does some time/date/number formatting using its own functions.

But Godot also has similar functions, so we should use those instead to save on file size and make formatting consistent with the engine.

Have not investigated the differences and if it's even worth doing this...

Create integration tests

  • Both the forked and default sample projects should contain a simple integration test. Load translations in different ways, translate a message with args, run the generator, ...
  • Run this test either using a framework like Gut or via godot --headless --script test.gd
  • Insert this into the CI pipeline, after the respective builds completed. (So after Windows Forked, run forked test. After Linux Default, run default test.)

Class documentation

Depends on godot-rust/gdext#178 or a direct call to editor_help_load_xml_from_utf8_chars (not exposed by godot-rust).

Will only really be helpful once the API has more members - not sure if ProjectSettings can already be documented for GDExtensions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.