Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to do localization using the tr macros? #392

Closed
Soremwar opened this issue Aug 29, 2023 · 8 comments · Fixed by #596
Closed

Is it possible to do localization using the tr macros? #392

Soremwar opened this issue Aug 29, 2023 · 8 comments · Fixed by #596
Labels
c: engine Godot classes (nodes, resources, ...) feature Adds functionality to the library

Comments

@Soremwar
Copy link

Soremwar commented Aug 29, 2023

Godot has the tr and tr_n functions which allow you to generate gettext templates for the dialogues in your game. This process is handled through the Godot project settings for easy management

Is this something that GDExtension exposes so it can be implemented on the Rust side? Or should the manual approach (creating the templates yourself) be considered instead

@Bromeon
Copy link
Member

Bromeon commented Aug 29, 2023

In general, functions in Godot classes (or the global namespace) are available in godot-rust, too. See Object::tr() and the functions below it. The tr_ex() function is our way of dealing with default arguments, it can be used like this:

let obj: Gd<Node> = ...;
obj.tr("message");
obj.tr_ex("message").context("ctx").done();

However, the GDScript syntax with % placeholders is a bit cumbersome to achieve in Rust, and would need Variant::evaluate() with the VariantOperator::Modulo.

We could probably create a tr! macro that behaves like Rust's format!, if that is your question. Would you be interested in opening such a pull request? 🙂

Regarding templates themselves, could you elaborate where you see Rust's role in that process?

@Soremwar
Copy link
Author

Soremwar commented Aug 29, 2023

Would you be interested in opening such a pull request?

I'll look into it, but it's gonna be kind of a hassle because the way GDScript works (every GDScript file is a resource and hence an object) you can call tr pretty much everywhere, which is my ultimate goal

However this issue was aimed at implementing the ability of Godot to generate POT templates from tr calls by statically analyzing the provided files (using EditorTranslationParserPlugin). Doing this would require #333 to be implemented though

I'm gonna try a first pass so at least manually created PO files can be used without much hassle

@Bromeon
Copy link
Member

Bromeon commented Aug 29, 2023

Yeah, I don't think it's gdext's task to build elaborate tooling around these workflows (or maybe I'm overestimating how complex those can get). The primary scope of the library is to provide bindings to Godot functionality, and maybe pave the way for such integrations, e.g. as editor plugins.

I was simply thinking about a tr! macro as a more idiomatic way to call tr, since the % formatting syntax isn't available in Rust. Let's try to keep it simple in the beginning 🙂

@Bromeon
Copy link
Member

Bromeon commented Sep 11, 2023

I was simply thinking about a tr! macro as a more idiomatic way to call tr, since the % formatting syntax isn't available in Rust.

@Soremwar are you interested in adding this feature?

@Bromeon Bromeon added feature Adds functionality to the library c: engine Godot classes (nodes, resources, ...) labels Jan 5, 2024
@vortexofdoom
Copy link
Contributor

vortexofdoom commented Feb 5, 2024

So I have a basic tr! working:

macro_rules! tr {
    ($string:literal$(, $arg:expr)*$(,)?) => {
        Engine::singleton().tr(format!($string, $($arg)*).into())
    };

    ($string:literal$(, $arg:expr)*; $cxt:literal$(,)?) => {
        Engine::singleton()
            .tr_ex(format!($string $(, $arg)*).into())
            .context(format!("{}", $cxt).into())
            .done()
    };
}

// no context
tr!("I am {} years old", player_age);
// with context
tr!("{} and I are very close", player_name; "friendship");

Very quick and dirty, since tr() is really a static method semantically, decided to get a reference to the engine singleton instead of allocating a Gd for every invocation, but open to other clever ideas.

Wanted to be able to support custom context since my first go required the .done() after every invocation, which wasn't very ergonomically nice. Supports a trailing comma, isn't quite as flexible as normal format_args macros since it requires explicitly for them to be expressions instead of arbitrary token streams (to know when to break). I also did a version where context was specified before the format arguments, but that seemed terrible. It could go first, but that goes against the method signature. Semicolon seemed like the best separator, but thought about | and : as well.

tr_n! is a little trickier and I figure hashing this implementation out first is preferable, especially as tr_n in general is not really suited for multiple wildcard arguments.

@Bromeon
Copy link
Member

Bromeon commented Feb 5, 2024

@vortexofdoom That looks great! Using a singleton is definitely a good idea -- I don't even know why this is a non-static method in the first place.

The Godot implementation looks as follows. The only non-static access is _can_translate, which can be disabled using set_message_translation() -- I don't really understand the point here.

String Object::tr(const StringName &p_message, const StringName &p_context) const {
	if (!_can_translate || !TranslationServer::get_singleton()) {
		return p_message;
	}

	if (Engine::get_singleton()->is_editor_hint()) {
		return TranslationServer::get_singleton()->tool_translate(p_message, p_context);
	} else {
		return TranslationServer::get_singleton()->translate(p_message, p_context);
	}
}

There is also TranslationServer with several methods. tr() calls either TranslationServer::translate() or TranslationServer::tool_translate(); so there's this distinction depending on editor or gameplay context.

@vortexofdoom
Copy link
Contributor

The set_message_translation() and _can_translate might be for instances where all of your in-game text is managed through the translation system, but there are specific contexts that you don't want to translate. It seems very niche, and something where if you wanted to use it, you'd have an item handy to just call tr() on, even in gdscript.

If semicolon is a good separator, I might be able to do tr_n! as well, with the annotation that you have to specify where you want n to go in the format strings, which is probably the best plan regardless for something like that.

I feel like the Engine singleton is a good one to use? The function can apparently be called in contexts with no TranslationServer, but that would probably be the more natural choice? I was also wondering where I should put it. Right now it's in engine since a lot of the more specific classes are generated.

@Bromeon
Copy link
Member

Bromeon commented Feb 5, 2024

I think ; should be ok, main drawback it's that it's visually not very distinguishable from ,.
One option would also be to require an explicit keyword, which is syntax highlighted:

tr!("{} and I are very close", player_name in "friendship");

"in" because "in the context of..."; and there is similar syntax in for loops.

Engine should be good -- I'm not sure at which init-level it is available though? In case translation is supported on lower levels than Scene.

Would also be good to add links to the Godot docs in the Rustdoc for these macros.

@Bromeon Bromeon linked a pull request Feb 7, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: engine Godot classes (nodes, resources, ...) feature Adds functionality to the library
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants