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

Consider fast-path for warm scenario async fluent-fallback functions #244

Open
zbraniecki opened this issue Dec 15, 2021 · 10 comments
Open

Comments

@zbraniecki
Copy link
Collaborator

The error fallbacking fluent-fallback's Localization class performs (both in JS and Rust) is that we fall back onto the next locale if a message is missing, but not if there's an error while resolving a message that is present.

That means that if the user is requesting ["key1", "key2", "key3"] and key3 is missing we will trigger a fallback onto the next locale for it, but if key1 references key3 which is missing, we'll resolve key1 in the first locale with a missing piece, like: Hello, { key3 }.

This is fairly arbitrary and when we designed it we want to retain ability to fine-tune the fallbacking model, which we now have a chance to better define when working on MF2.0.

But if we were to keep this model for MF2.0, there's an interesting optimization that may be useful for Firefox and Fluent DOM application: if we loaded strings (async), and we are asked to translate a list of keys, we can synchronously check if all keys are present in the top locale (by using bundles[0].has_message) and if so, we know we will not trigger any async I/O fallback!

This means, we could then shortcut to resolve this API call synchronously.

So an example DOM Localization call triggered by L10n Mutations could perform something like:

let all_available = document_localization.messages_available(list_of_keys);
if all_available {
    let messages = document_localization.format_messages_sync(list_of_keys);
} else {
   let messages = document_localization.format_messages(list_of_keys).await;
}

This would act the same way for initial call (triggering async), and for incomplete scenarios (still using async), but for the most common scenario it would allow us to execute frame faster.

This came to play in cases like https://bugzilla.mozilla.org/show_bug.cgi?id=1737951 where engineers want to flip l10n-id back and forth and expect that while initial l10n frame may take longer, the "warm" swapping of l10n-id during UI lifecycle should be very fast because the strings are loaded.

We could even do the messages_available check with iteration over the CachedBundles and then even if one of the keys is only available in a fallback but that fallback is already loaded, we still know we can operate in sync mode and no I/O will be needed.

To recap, this optimization would be useful, but it requires us to ensure that:

  1. We will in MF2.0 follow Fluent's fallbacking model
  2. We would need to re-add some _sync method calling ability in async scenario - for cases where we are certain all messages are available

I'm wondering how far are we from such guarantees and how should the Localization class API be redesigned to model for this use case.

@zbraniecki
Copy link
Collaborator Author

@nordzilla @eemeli @dminor

@eemeli
Copy link
Member

eemeli commented Dec 15, 2021

I think there are two actions which are currently conflated into one externally visible interface:

  1. Waiting for a message to become available
  2. Formatting a message

Combining these into one async format_message call makes sense when the loading and formatting are e.g. happening in separate threads, but it's clearly disadvantaging the use cases where certainty can somehow be achieved first that loading has completed before formatting is even attempted.

So yes, I would support providing a synchronous message formatting call. Regarding messages_available, I think at least in an MF2 world it'd make more sense as something like resources_available.

@zbraniecki
Copy link
Collaborator Author

The reason they are conflated is that setting an API as sync is a one way street and we didn't want to lock ourselves out of ability to execute async I/O late.

For example, a scenario may happen where you request key1 with a variable that resolves (success), and then request again with a variable that fails.
If we wanted to fallback onto a second locale in that case, we'd need to do it right at this moment so we wanted to maintain the method as sync.

Another scenario we wanted to explore is a threshold of messages resolvable in a given locale. If a screen has 100 messages and we resolved 90 in locale A, and 10 in locale B, then that's fine. But if we resolved 3 in locale A and 97 in locale B, then maybe it makes sense to translate the whole screen in B?

What if message exists, but references another message that doesn't? Should the top message fallback to locale B or stay in A? If stay in A, should it resolve the referenced message from locale B creating a mixed locale message, or return partially resolved message?

We never got to rethink the error fallbacking of messages and if MF2.0 will follow Fluent here, then maybe it is ok to strengthen the model by stating that the fallback to a different locale happens only if a directly requested message is missing?

Which, in return, would mean that if a directly requested message(s) are present we can guarantee that we will not perform I/O and call format synchronously?

@zbraniecki
Copy link
Collaborator Author

@stasm

@Pike
Copy link
Contributor

Pike commented Dec 15, 2021

FWIW, my post-l10n mind looks at bundle as a mistake. I think that a mono-lingual abstraction layer in a truly multi-lingual system creates more problems than it solves.

Maybe we're not talking about an entry-level thing, but more about caching already-rendered messages? That obviously raises the question what the cached thing is, as that, in general, still depend on message context, right?

In the context on fallback, term references might be special, as I can see reasons for them to favor terms in the same language over terms in the overall scheme of things. Maybe they even have a different language fallback chain like just [currentMessageLanguage, defaultLanguage]?

@zbraniecki
Copy link
Collaborator Author

FWIW, my post-l10n mind looks at bundle as a mistake. I think that a mono-lingual abstraction layer in a truly multi-lingual system creates more problems than it solves.

Interesting. What do you think should be the layer structure? Back of a napkin blurb is okay, no need to refine, just wondering what your mind holds now.

Maybe we're not talking about an entry-level thing, but more about caching already-rendered messages? That obviously raises the question what the cached thing is, as that, in general, still depend on message context, right?

With Message being a list of parts, we could store it resolved and "replace" dynamically just pieces that come from variables maybe? Not sure how far this will take us.

In the context on fallback, term references might be special, as I can see reasons for them to favor terms in the same language over terms in the overall scheme of things.

How are they special? In MF2.0 we do introduce dynamic references and I imagine you'd want to maintain consistency between message and a referenced message as well (both static and dynamic), am I wrong?

@Pike
Copy link
Contributor

Pike commented Dec 15, 2021

FWIW, my post-l10n mind looks at bundle as a mistake. I think that a mono-lingual abstraction layer in a truly multi-lingual system creates more problems than it solves.

Interesting. What do you think should be the layer structure? Back of a napkin blurb is okay, no need to refine, just wondering what your mind holds now.

My latest thinking was more around the Localization class holding a cloud of entries, bound to the language of their resource. That would also allow for bidi separators being more context sensitive.

The relationship between the Localization class and resources would be that it's directly feeding upon a generator of parsed resources per "source".

Message references would start at the top of the generator, term references might be different. That term refs are different is due to the quality that a term in the same language can bring the translation of a message. That implies that all term attributes and parametrized behavior is abstracted if the message language doesn't match the term language. match might be interesting here in terms of es and es-AR to pick the easy challenge. God forbid zh :-)

Maybe we're not talking about an entry-level thing, but more about caching already-rendered messages? That obviously raises the question what the cached thing is, as that, in general, still depend on message context, right?

With Message being a list of parts, we could store it resolved and "replace" dynamically just pieces that come from variables maybe? Not sure how far this will take us.

Yeah, that'd be a choice. Or only cache primitive messages.

In the context on fallback, term references might be special, as I can see reasons for them to favor terms in the same language over terms in the overall scheme of things.

How are they special? In MF2.0 we do introduce dynamic references and I imagine you'd want to maintain consistency between message and a referenced message as well (both static and dynamic), am I wrong?

This goes back to what I detailed earlier. The term attributes and parametrized terms only make sense within a particular context. That context is constrained by linguists creating an agreement on which meta data has which meaning. For zh that might be just constrained to the script, for es it probably depends more on the translators following a common ontology to represent Spanish grammar.

The straight-forward MVP approach to this would be to draw the lines across locales. Maybe there's a path for particular ecosystems to opt in to blurring those lines between particular locales.

@eemeli
Copy link
Member

eemeli commented Dec 17, 2021

For example, a scenario may happen where you request key1 with a variable that resolves (success), and then request again with a variable that fails. If we wanted to fallback onto a second locale in that case, we'd need to do it right at this moment so we wanted to maintain the method as sync.

The way that I want to solve this in MF2 is to 1) more clearly associate each message with an identifiable resource that is loaded in a single operation and 2) require term/message references to identify the resource that they're pointing at in a way that's independent of any runtime variables.

With those constraints, an async resource load operation can traverse its messages and find any external resource references that need to be loaded as dependencies of the current resource. This should allow for the above scenario to be identified already during the resource load.

Another scenario we wanted to explore is a threshold of messages resolvable in a given locale. If a screen has 100 messages and we resolved 90 in locale A, and 10 in locale B, then that's fine. But if we resolved 3 in locale A and 97 in locale B, then maybe it makes sense to translate the whole screen in B?

I wonder if "screen" is the right dimension in which to be considering the implementation of this? Sure, that's almost certainly the desired user experience, but it's rather distant from the spaces in which we actually have messages. Is this perhaps related to the multi-resource "context" you included in the DOM localization draft? I could see that as an appropriate place/level at which to configure this sort of fallback. Implementation-wise, I'm thinking of the context attaching an error handler wrapper to formatting calls of its constituent messages that would track overall failures and trigger fallback when some threshold is reached.

What if message exists, but references another message that doesn't? Should the top message fallback to locale B or stay in A? If stay in A, should it resolve the referenced message from locale B creating a mixed locale message, or return partially resolved message?

We never got to rethink the error fallbacking of messages and if MF2.0 will follow Fluent here, then maybe it is ok to strengthen the model by stating that the fallback to a different locale happens only if a directly requested message is missing?

Which, in return, would mean that if a directly requested message(s) are present we can guarantee that we will not perform I/O and call format synchronously?

I think this level of fallbacking should happen within/around resource loading, rather than message formatting. It should be possible to trigger the fallback based on a side effect of a message formatting call, i.e. in an error handler of some description.

In general, I guess I'm thinking that fallbacking to a different locale should be considered error recovery, and that this means that it's okay to even momentarily render broken strings while we're loading the fallback locale's resources.

@zbraniecki
Copy link
Collaborator Author

In general, I guess I'm thinking that fallbacking to a different locale should be considered error recovery, and that this means that it's okay to even momentarily render broken strings while we're loading the fallback locale's resources.

When designing Fluent we actually weren't certain how much we can commit to such claim.

The alternative, non "error recovery" scenario we explored is something we called "micro-locales" or "partial-locales" - for example Spanish has over 20 dialects. The idea was that we could have a "full" es locale with all resources, and then partial es-CL with just 10-20 strings that differ.
Your es-CL langpack would contain full es and partial es-CL and use the fallbacking to load strings at runtime.

The challenge is that it means that 90% of strings would have to fallback and fallback becomes "normal" behavior.
There's an alternative approach which most l10n system approach - build a "full" es-CL locale by pulling es strings into it and than at runtime the es-CL is complete.

We wanted to explore the no-build-time, runtime-only approach as more flexible and resilient assuming we can make it performant.
We never got to use that system in production, but the way Localization class API is designed was meant to make this model possible as a first-class scenario.

@eemeli
Copy link
Member

eemeli commented Dec 21, 2021

Okay, that makes sense. For micro-locales, fallbacking like this would indeed need to be treated as a normal rather than exceptional event. This does have us using one fallbacking system for two different purposes, though, and I'm not sure if that's optimal. As in, if the fallback chain would be es-CLesen-US, those two steps would be drastically different.

The way I was imagining this in my head would be that a single [es-CL, es] bundle would get built out of es and es-CL resources, i.e. something like your "alternative approach", and that message resolution within this bundle would not be considered fallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants