-
Notifications
You must be signed in to change notification settings - Fork 21
Description
I understand why localization is always treated as an afterthought and every time a new approach to GUI comes up it comes with some version of:
Label::new(format!("current count: {}", self.count)).build(cx);
but I'm here to argue that this is both a bad paradigm and that the reason it doesn't get fixed is that once the GUI toolkit is mature enough to actually tackle the problem, there's too much sunk cost in this model to revisit it.
So I'd like to suggest you actually tackle it early :)
First, translation is not a string that one plasters onto a widget. It's a binding. Bind your widget to a localization unit.
In 20-years-ago command line textual apps, you could get away with such glorified printf
, but that model very badly translates onto UI trees.
UI element may be nested, have some internal structure, it's value may have markup and structure (think, <strong>
, <sup>
, <span>
in HTML inside a string), it may have attributes, both textual (button's accesskey or tooltip) but also non-textual (color may be culturally dependent, icon associated with a button may be different for some locales, like rewind back/fwd in RTL cultures or some emojis in Japanese culture) and so on.
So instead of thinking of localization as a printf
into a String
, we should start talking about a compound object with multiple elements inside it - a Label or a Button or a Menuitem, and a compound localization unit that contains information needed so that those two combined can be laid out and painted on the screen.
Second, in reactive UI, retranslate every time the variable changes, or the locale changes (or the locale resources get updated!) during life cycle of the app.
Those two concepts work together really well - if you annotated by binding, you can just walk your UI Tree and retranslate at will using different localization resources whenever needed. You can cache the result, invalidate that cache and so on, without any overhead on the developer.
You can localize asynchronously (think, you localize into fr-CA
but midway through the localization stage you realize that some button cannot be localized and you want to fallback on fr
resources, asynchronously load them and have the button be in fr
as a fallback). You can do it because the localization pass is not related to how developer annotates the element with localization unit.
How would it look like?
In HTML we do:
l10n.res
key1 = You have { $emailCount } unread emails.
.accesskey = K
.tooltip = Number of unread emails
file.html
<label l10n-id="key1" l10n-args="{emailCount: 5}"/>
I think you can do better here. Maybe:
let mut label = Label::new();
label.l10n.args.set("emailCount", 5);
label.l10n.id = "key1";
label.build(cx);
There are deep consequences to that change in three directions:
- how you think about UI, ergonomics, developer UX
- how you think about DOM, layout, painting, invalidation, reactiveness etc.
- how you think about locale management, live language switching and updating etc.
I can dive in all three of them, but I just wanted to start by suggesting shifting the approach to string injections early.
You can also treat an element as either controlled by l10n or manually:
let mut label = Label::new();
// Either
label.content = Content::Manual {
value: "Static Value with { $emailCount }",
attributes: {
"accesskey": "C",
"tooltip": "Foo"
},
args: args!(
"emailCount": 5
)
};
// Or
label.content = Content::L10n {
id: "key1",
args: l10n_args!(
"emailCount": 5
)
};
label.build(cx);
This is actually fairly common in my experience of working with Fluent. An example is when there's a menuitem that either takes a value from some database (say, credit card name) or displays a localized message "Unknown Type".
In such case we want to write:
cc-name-unknown = Unknown Credit Card
.accesskey = U
.tooltip = The type of a credit card could not be recognized
.icon = @icon-cc-unknown
menuitem.content = if let Some((name, accesskey, description)) = credit_card.get_label_info() {
Content::Manual {
value: name,
attributes: {
"accesskey": accesskey,
"tooltip": description,
"icon": format!("@icon-cc-{}", name),
}
}
} else {
Content::L10n {
id: "cc-name-unknown",
}
};
This Rust code can be called every time menuitem list has to be rebuilt, but a function that updates translations is independent from it and can run on a different scheduler and react to different events, and be asynchronously loading resources blocking layout/paint, but not blocking this function.