Description
Is your feature request related to a problem? Please describe.
There are scenarios where a translation message needs to reference another message which is not statically known.
To illustrate, let's start with a pair of messages and a message that references them:
board-name = Board
dashboard-name = Dashboard
remove-board = Remove { board-name }
remove-dashboard = Remove { dashboard-name }
let msg = api.get(semaphor ? "remove-board" : "remove-dashboard");
This is already possible and well within the scope of regular message references, but it doesn't scale well to scenarios where the referencing is more nested and/or the number of items grows.
For example a computer game may have 10, 100 or 200 monsters and a lot of messages want to reference any of them.
Having to write 3 messages to nest 3 levels deep, or having to write a remove-X
message for each X
is not sustainable and blows up payload size, maintenance complexity etc.
Describe the solution you'd like
Dynamic references is a concept that allows for the message ID which is to be referenced to be decided at runtime:
board-name = Board
dashboard-name = Dashboard
remove-item = Remove { $item }
let msg = api.get("remove-item", {
item: MSG_REF(semaphor ? "board-name") : "dashboard-name")
});
or:
monster-dinosaur = Dinosaur
monster-elephant = Elephant
monster-ogre = Ogre
killed-notice = You've been killed by a { $monster }
let msg = api.get("killed-notice", {
item: MSG_REF(validatedMonsterName)
});
Describe why your solution should shape the standard
I believe that the use cases where the dynamic references are needed are very badly served by workarounds, and if we don't provide a good API for it, users will develop data and code for handling such cases that will be inherently hard to maintain and costly to clean up.
Additional context or examples
There are implication on our decision on this feature for other facets we're considering:
- Arguments passing (implicit, explicit etc.)
- Fallbacks
- CAT tool meta-data
The common workaround an engineer may do today is:
let item = api.get(semaphor ? "board-name" : "dashboard-name");
let msg = api.get("remove-item", {
item
});
which has multiple issues with it.
- It loses context between calls as from the second call's perspective the
item
looks like a hard-coded string, rather than another message from the same context. - Cross-language fallbacking is impossible.
- Any CAT tools, CMS, TMS and MT operate blindly with no way to convey information about the related messages
- Layout cannot rely on directionality of placeholder since it looks like potentially requiring a reset
Future GUI bindings impact
Lastly, this paradigm is particularly painful for GUI bindings (#118). I understand that we consider #118 to be out of scope in some ways, but I think this issue is a good testbed for how far ahead we want to think and design for.
Assuming l10n bindings for GUI will want to use declarative bindings resolved asynchronously for animation frame, cases where dynamic references are needed are particularly painful, because the user cannot declare ID and MSG_REF as an argument on the UI widget and let the l10n system resolve it.
The user has to fetch the references message, resolve it, and then declaratively define the ID and resolved message as a String argument in the binding.
If we want to allow for localization to be asynchronous, without dynamic references we'd do:
let monster = await api.get(semaphor ? "monster-elephant" : "monster-ogre"); // String
element.setL10n({id: "killed-notice", args: { monster }});
which in case of HTML for example may lead to:
<p l10n-id="killed-notice" l10n-args="{monster: 'Elephant'}"/>
Now not only did it complicate the async code, but it also hardcoded Elephant
in the binding as if it was a hardcoded String.
If the system needs to retranslate this UI widget, because user changed locale, it will be able to pull up new killed-notice
but will pass the pre-resolved Elephant
in the old language.
To de-hardcode it, we'd need to write a helper callback to be called on each localization cycle like this:
let monster = await api.get(semaphor ? "monster-elephant" : "monster-ogre"); // String
element.setL10n({id: "killed-notice", args: { monster }});
element.onBeforeL10n(async () => {
let monster = await api.get(semaphor ? "monster-elephant" : "monster-ogre");
self.setL10nArgs({monster});
});
This will allow the system to update the element's argument before updating the main message leading to proper translation on change, but is pretty complicated to maintain and I'd say a bad developer experience.
With Dynamic References, the user can just do:
let monster = MSG_REF(semaphor ? "monster-elephant" : "monster-ogre");
element.setL10n({id: "killed-notice", args: { monster }});
which in case of HTML for example may lead to:
<p l10n-id="killed-notice" l10n-args="{monster: {type: 'msgref', id: 'monster-elephant'}}"/>
and now the DOM contains all the information to retranslate as needed without any additional information, the declarations are synchronous, the localization can be asynchronous, the state is preserved and the separation of concerns between translation/retranslation and updates is easy.
There is more background info in Fluent issue.