cpp: Add set_external_translator() to register a custom translator#10979
cpp: Add set_external_translator() to register a custom translator#10979ubruhin wants to merge 2 commits intoslint-ui:masterfrom
Conversation
d26b0d4 to
5527d50
Compare
|
Thanks for tackling this :) My feeling is that a C++ class is the cleanest way to handle this. Yes, it means some glue code in Slint itself, but we need some translating glue anyway as I'd rather not have I think separating singular and plural handling into two functions makes for a cleaner interface overall, so I think the user needs to supply two functions. If those two functions are just plain freestanding functions, then indeed we could just take two function pointers and be done with it. However, I think we should make it possible to let implementations not require global state, so the function pointers become - in reality - closures and we'd store them as In that case, I see a tradeoff between two std::functions where the contained closures capture the same state versus a C++ class with two virtual functions and where So my conclusion is that the nicest API for our users would be a C+ class with (pure) virtual functions, where the Slint API accepts an instance. What do you think? |
|
Alright, I think that sounds good. It also allows to wrap the cbindgen API on C++ side to provide a nicer API to the user (e.g. From the user point of view I could imagine an API like this: #include <slint.h>
#include <memory>
#include <string_view>
class MyTranslator : public slint::Translator {
public:
MyTranslator(/* custom parameters */) {}
slint::SharedString translate(std::string_view string,
std::string_view context) override {
return slint::SharedString("Foo");
}
slint::SharedString ntranslate(uint64_t n,
std::string_view singular,
std::string_view plural,
std::string_view context) override {
return slint::SharedString("Bar");
}
};
// Registering (Slint will hold the shared_ptr)
auto translator = std::make_shared<MyTranslator>(/* custom arguments */);
slint::set_external_translator(translator);
// Unregistering (Slint releases the shared_ptr)
slint::set_external_translator(nullptr);Though I am not sure if developers of embedded devices (e.g. ESP32) will be happy to use |
|
Thanks for working on this.
I think there is no need for SLINT_FEATURE_TR feature, we could just enable tr crate unconditionally, this is a tiny crate. But if we really want to gate this behind a feature, it should get another name. |
My idea was that the user might still need to access the translation object even after passing it to Slint (e.g. for switching to another language at runtime, or whatever). That would be more cumbersome with unique_ptr. But I'm also fine with unique_ptr, no strong opinion here. I'll try to implement the translator with a C++ base class soon. |
9edb61f to
8655260
Compare
|
Hi! I'm I would like to apply some automated changes to this pull request, but it looks like I don't have the necessary permissions to do so. To get this pull request into a mergeable state, please do one of the following two things:
|
8655260 to
3473129
Compare
|
Alright, I implemented it now with a pure virtual base class Example: struct MyTranslator : public slint::Translator {
slint::SharedString translate(std::string_view string,
std::string_view context) const override {
return slint::SharedString("Singular String");
}
slint::SharedString ntranslate(uint64_t n,
std::string_view singular,
std::string_view plural,
std::string_view context) const override {
return slint::SharedString("Plural String");
}
};
slint::set_external_translator(std::make_unique<MyTranslator>());Ownership of the translator object is transfered to Slint and the object gets destroyed from the
Done. Some open questions:
|
8066703 to
d5a5894
Compare
Yes, that's correct, in principle.
That's because the
Yes, i'd say so.
Unfortunately here the situation is quite bad and we do more allocations than required.
We need to return an owned string, and the
Yes, that's another allocation. The thing is that with the current signature, since we take a string_view in, and a SharedString out, we always need to allocate a SharedString, even if just returning the input. Quite inefficient.
I guess that make sense, as translations shouldn't modify the object. Conceptually. So overall i think the design of the Translator API currently in this PR is what makes the most sense right now. As for the name One remaining piece are tests
We also need to specify what happens when using both bundle translations and a translator. |
|
Thanks for the review! Yeah I agree the use of the
Ah that would be nice for embedded devices indeed.
That's true, but I'd say it's not a big problem. The reason for registering a translator is to translate texts into another language. So for every language other than the source language, the returned string is expected to be different from the source string, it's rather an exception if the input will be returned as-is.
One thing I'm thinking about, if it would be better/possible to wrap a Rust
Agree, I will rename.
Yes, that's why the PR is still a draft :) I'll have a look at tests when the API is more or less clear. Btw, if I understand correctly, CI for ESP32 now fails because of |
|
Another idea: Would it be reasonable to gate the new public API with |
Yes, that will certainly make it easier to press the "Merge" button :-P |
Yes, But we could disable the set_translator on the freestanding build maybe. |
Translation support should also be available with no_std, but that is currently not possible since the tr crate requires std. So for now, translation support from C++ is only enabled for builds with std.
* Add pure virtual base class (interface) `slint::Translator` * Add function `slint::set_translator()` to register an instance that implements that interface * Gated behind the experimental feature flag due to unresolved discussions about the exact API
d5a5894 to
ddf0db0
Compare
This intends to implement #3715 for C++: Registering a custom translator from C++, using the
set_external_translator()function which has been added recently. For this, I extended the CMake setup to allow enabling the "tr" feature.Concept Discussion
However, this PR is intended as a base for discussions, it's not a finished implementation. I'd like to clarify first which approach you like to follow.
The most important question is basically how the C++ API should look like. I can imagine 3 different approaches:
Translatorbase class (interface) which users can derive from, and implementtranslate()andntranslate()exactly the same way as in thetrcrate. This seems the most obvious solution to me, but it involves a lot of complexity/boilerplate to finally pass a C++ translator object via cbindgen to Slint. The ownership/lifetime needs to be considered carefully for this approach, and I think we have to pass the object around as a struct with function pointers (IMHO complex, unreadable & error-prone).translate()andntranslate()to Slint. A translator object exists only on Rust side, but not on C++ side. It's more primitive, but reduces complexity & boilerplate a lot. Just a bit cumbersome that 2 independent callbacks need to be implemented.To be honest, even though variant 1 would be the cleanest from a conceptual point of view, I don't see it being worth adding so much complexity and lots of "stupid code" just to pass an interface through the C++/Rust bridge. Variant 2 I also consider as slightly cumbersome, as in practice the implementation of the 2 functions will be largely identical. So for now, I gave variant 3 a try, which is the implementation of this PR. And indeed I am actually surprised how simple and straightforward it is :) There's basically no complexity at all, it's dead-simple code. I think C++ developers are used to work with callbacks, so I don't mind the primitive concept compared to variant 1.
But I wonder what is your opinion? Any other ideas? Did I miss an easy way to implement variant 1?
I tested the current implementation with LibrePCB and so far it seems to work perfectly fine.
Open Questions
Note that the current design still has some open questions which I would try to figure out after the basic concept is clear:
nullptras callback.const uint64_t* nis used to distinguish between singular and plural form. I understand this can be considered as ugly :) But the question is, what would be an acceptable way?std::optional<uint64_t> n?bool is_plural?, ...?Slice<uint8_t>for the untranslated strings,SharedString*for returning the translated string. Maybe not the nicest interface, but I think we should choose something that doesn't involve unnecessary string copies (though this is probably not the case at the moment due to convertingSharedStringtoCow).ToDo
ToDo after the concept is clear:
set_translator()should probably be renamed toset_external_translator()for consistency with the Rust function