On Message- and Termreferences

We’ve had a number of conversations in the past two weeks about Message- and Termreferences. They’ve all been in various combinations of individuals involved, or on only somewhat related issues.

To set the stage, there’s been questions on the why of references. I’ll start with that, as that’s already a mouthful.

I’ll give this post a bit of time to settle and get feedback, and then add more about the problems we have with the current design, and what we could do to address those.

Use-cases

Firstly, this is my personal perspective on them, that’s why I want to give this first segment of the conversation a chance to actually be a conversation.

The use of references differs a bit between Messages and Terms.

Avoid repetition

This is often cited, but actually an anti-use-case. As stated in the Fluent docs, it’s OK to Write Everything Twice. Or Thrice. Or to break the acronym, Do Repeat Yourself.

Terminology

Using Terms and Termreferences is a way to encode terminology in a Localization project. The right use of Terminology is also language dependent. A special case of Terminology is …

Branding

Product owners care even more strongly about the use of branding throughout localizations. This is a tricky problem in particular in software localization, as some languages don’t use programs as subjects in phrases, so the use of Terms can’t be enforced in general.

One aspect of branding in libraries is that they can be dynamic. A localization of Firefox talks about Firefox, or Nightly, depending on update channel. Or a string in toolkit might talk about Firefox or Thunderbird.

This isn’t just a gecko problem. Think about a store platform, where the individual stores have different brands.

Consistency

This is similar to Terminology, and the boundaries are in flux. Basically, the difference is between 2-3 vs 15 uses. Also, literal quotations are a good example, where TM/MT can’t help to ensure consistency. Also, different technical implementations of the same thing in two places (.value vs .description, for example).

Here, you’ll end up using message references instead of introducing terms and references.

Back-up strings

Say, you have a good string, and a better string. You’d love to pick up the better string, but be OK with the good string.

One way to do this would be to add a fall-back string, that just redirects to the good string:

better-string = {good_string}

Sneak peek, I’ve dropped this from my recommendations as I don’t see that working in the current implementations. At least not better than just asking for localizations of better-string right away.

Factorization

If you have a often-used segment that contains logic, you would like to create a single place for that logic, and reference it where needed. This is a term reference again. For example, update-channel-dependent branding would be an example.

There’s also a language-dependent variant of this, for example to implement platform variations in Japanese. This is a good deal tougher on the l10n ecosystem, though. This requires support for localized entities that only exist in a single localization, which is rare in l10n processes, and thus not widely implemented.

Factorization squared

aka dynamic references. Message and Term references are dynamic when you build Localization objects (and by that, bundles). Dynamic references take that a step further by introducing an option for the call into the Localization.formatValue to take varying references to resolve to.

To line up the use-cases with their expectations and short-comings, we’ll need to understand what we solve for.

One important hypothesis is that partial localizations are better than none, and thus are a common request for a localization ecosystem. There’s a report by csa-research (sorry, paywall) in support of that, for example.

On the more selfish side, mostly anything we do at Mozilla ships with partial localizations.

In this light, I’d like to start by looking at

Consistency

In a partial localization, a message with references to another message can come from any language in the fallback chain. To have something to talk about, say, our fallback chain is

ca -> es -> en

Also, let’s have:

ca:
save = Desa

es:
save-changes = Guardar cambios

en:
this-or-that = Click "{save}" or "{save-changes}"
save = Save
save-changes = Save changes

The way we currently implement message references, this leads to

| Click "Save" or "Save changes"|
| Desa     | Guardar cambios    |

That’s not nice, but let’s turn this the other way around:

ca:
this-or-that = Feu clic a "{save}" o a "{save-changes}"

es:
save = Guardar
save-changes = Guardar cambios

en:
this-or-that = Click "{save}" or "{save-changes}"
save = Save
save-changes = Save changes

The way we currently implement message references, this leads to

| Feu clic a "{save}" o a "{save-changes}" |
| Guardar     | Guardar cambios            |

(Pardon my French, err, Catalan)

Which is a lot of text and, granted, two pathological examples.

But these are the reason why message references aren’t a good tool for localization right now. Translation memory and Machine Translation post-editing are much better at achieving consistency among a translation than using message references.

I’d consider dropping them, if there wasn’t a way to fix them.

Proposal

Message references should resolve within the scope of the Localization concept, and not within just a single bundle.

There’s two possible algorithms to implement this that we’ve come up with so far.

Both algorithms should allow to separate the general resolver logic from IO code paths, which would be both sync and async for some implementations. You could also keep the current bundle-based implementation, but an implementation that wants sync IO needs all sync code paths to MessageReference, and if it wants async IO, all async code paths to MessageReference, and if it wants both, well, it needs two implementations. Meh.

Inspection

One way to implement this is for the Localization to inspect Patterns for message references, and to resolve those first. This would also implement the protection against cyclic references. It would store resolved message values in the scope, and the message references itself would look up in the scope instead of the bundle.

There’s caveats. Patterns become less opaque, but it’s really just APIs they implement, and not public data. Within format-to-parts, the resolved message values are also iterables, and they need to be re-iterable.

Another algorithm would be …

Iterables

This looked really nice, and as long as all your references are just in Placeables, you could just yield a MessageReferenceToken as data type for format-to-parts, and the localization could look that up, and yield from there, and then continue to exhaust the original pattern generator.

Sadly, TermReferences can show up in selectors, and message references in function arguments.

This algorithm would still work, but effectively the full resolver would move into the Localization, and Bundle itself would be just a dumb container. And Pattern would be just iterable and that’s it.

The upside of this algorithm is that it’s actually more together, I think. The inspection algorithm has this split between inspection, dependency resolution, bundle work, localization work, etc.

I’ll take another break here, let you folks think about this, and then follow up with extending this proposal to Term references.

That’s a great take on the issue Axel! Thank you for putting time into it!

I see a different use case for message references, when working with DOM and shapes. We have cases where we want to display the same “unit” in several “shapes”:

panel-general =
    .title = General
    .tooltip = This is a general panel
    .accesskey = G
    .key = g

menu-panel-general = { panel-general.title }

context-panel-general =
    .title = { panel-general.title }
    .accesskey = { panel-general.accesskey }

customui-panel-general = { panel-general.title }
    .key = { panel-general.key }

panel-general-shortcut =
   .key = { panel-general.key }

Now, yes, we could lean on MT for that, but I actually am not confident that in this very case MT will be reliable.
Partially, because some of the attributes here are a single letter and I don’t know if MT will be great at picking up consistency between them (do we provide IDs to MT as context to help it?).

The other issue is that this tightens the source locale consistency. If a developer wants to update tooltip or accesskey, it’s relatively easy, and greppable, for them to do so.

But if we instead hardcode the strings, then it may be very tricky for the developer to find around the codebase the same accesskey used in different contexts. We can hope that all strings will live in a single FTL resource in a single group, or we can do what we did in the DTD days and write comments asking people to “keep X in sync with Y”, but when going through migrations I noticed that quite a bit of those comments get out of sync and become unsalvagable.

p.s. I realize that compound units are in question overall, but we need to solve the DOM widgets somehow and I haven’t seen a better l10n UX than compound units in Fluent so far.
p.p.s. I’m open to a shift of references to less “tight” model - { MESSAGE_REFERENCE("panel-general.accesskey") } function or { ATTRIBUTE_REFERENCE("panel-general", "accesskey") } would likely work similarly well, and at the same time make it a bit less appealing to overuse while giving us a decent way to hook any logic into the function - like your mentioned Localization level referencing.
p.p.p.s. The only “tricky” part in such case would be passing of arguments.

I’d like to spend a bit more time on this hypothesis. I think there are two kinds of partial localizations:

  • partial by mistake or neglect, when the target localization has errors or is otherwise temporarily incomplete,
  • partial by design, when the target localization extends the base one.

I consider partial by mistake to be error scenarios and I don’t see the need to design the entire architecture and the API around them.

On the other hand, I consider partial by design to be a big feature, not entirely new but also one that we haven’t explicitly prioritized before. As such, I think it’s reasonable to scope it with some limitations, by bounding it by (to be discussed) requirements. In other words, I’d like to be able to state that in order to use Fluent to do partial-by-design localization, one needs to meet some additional requirements.

I’d like to spend a bit more time thinking about whether the same rules should apply to message references and term references. Each term has an API which may only be good in the language the term has been originally defined in. A term’s API may change, and the other (partial) localization referencing it would still expect the old API. It might be that terms should only ever be resolved just within the bundle of the message referencing them.

These could be solved by bounding the feature with requirements: term references would only be resolved within the same bundle, and message references could be forbidden from function arguments.

And then the iterable approach could actually be a viable one.

Message references keep encouraging bad practices, like excessive factorization in the spirit of DRY. This creates more work for the reviewers and localizers. The same goals can be achieved via CAT tools, with the benefit of allowing more customization and expression. Deprecating message references from the syntax could be a good move long term, which I’d like to keep on the table for now.

I’m concerned about this pattern. Just looking at your example, I think it’s unfortunate that the default scenario creates so many messages with no work to be done by localizers. I understand that this approach gives more control to those localizers who want to customize one particular translation in one particular case. But the trade-off seems to be high.

Ideally, I’d like to move the handling of element shapes to the bindings, where for some element you could say “map panel-general.title to this element’s textContent”.

I agree.

I noticed a delicate difference between an intent of message references and the use for shape shifting.

In the original intent of message referencing, the reference is part of the pattern. In the shape fitting case it is full pattern.

Like, in data model, for regular references, we’d like to say:

Message {
  value: Pattern [
    TextElement("Click "),
    MessageReference("save"),
    TextElement(" or "),
    MessageReference("save-changes")
  ]
}

but in the shape shifting it’ll be more like:

Message {
  value: MessageReference("panel-general", "title")
  attributes: [
    Attribute {
      name: "key",
      value: MessageReference("panel-general", "key")
    }
  ]
}

If ever we’d like the pattern to actually contain some original text elements beyond just referencing another value/attribute, then we’d completely switch that case to the former problem described by Axel.
So I’m ok treating this problem as a separate one - one which we used message references as we have them now just because they solve it - not because the system is designed for them. In result, this problem should not be used to justify message references as we know them, it just needs to be solved.

Now, to solve it, I also agree with you, that there are two separate elements to that system: There’s a compound translation unit, and there’s a number of UI widget shapes which want to use it.

panel-general =
    .title = General
    .tooltip = This is a general panel
    .accesskey = G
    .key = g

Having this unit as a single message has benefits - it can use single meta information, same comments, it gets translated and updated together, if it gets broken or missing, it falls back together, and logically it makes sense to be treated as a single “undividable” thing.

The rest, as you pointed out, is not really localization world:

menu-panel-general = { panel-general.title }

context-panel-general =
    .title = { panel-general.title }
    .accesskey = { panel-general.accesskey }

customui-panel-general = { panel-general.title }
    .key = { panel-general.key }

panel-general-shortcut =
   .key = { panel-general.key }

This should stay the same across all localizations and generally “follow” the translation unit if it ever gets updated.

I think one way to solve that, which I remember you suggesting was to put all of that binding information into DOM. Sth like:

<element data-l10n-bindings="value: panel-general.title, key: panel-general.key"/>

There are two issues I have with such solution:

First, it generates significant overhead for developers compared to the current solution. I am concerned about our ability to justify that overhead compared to the current model, which is just cleaner.

Second, it doesn’t really fit well into a mixed scenario where the UI widget contains both - original translation and such references.

key1 =
  .title = This is my translation
  .tooltip = This is my tooltip

panel-general =
  .key = g

<element data-l10n-id="key1" data-l10n-bindings="key: panel-general.key"/>

Now, this creates a significant overhead where part of the shape comes from the key1 shape, and part from the bindings matching, and if that were ever to change the developer has to jump back and forth between different mental models.
Once again, the current solution is clear - the shape is defined by the translation unit and information about all attributes to be localized is clearly visible by looking at its shape.

Stating those challenges, I still think there there is hope for a good solution that preserves much of the current ergonomics while liberating Fluent from the Message References abuse and misuse.

1 Like

A couple more mozilla-specific remarks on partial translations.

Partial translations is the Fluent feature that decouples Firefox updates from language pack updates. As such, I think this is the selling point that got Fluent into Firefox. All the other reasons are great, but this one is the kicker.

Shipping partial translations is also a cornerstone of our SLA for community translation. There’s sadly only a bit of info on that on mana. Things that need to be a 100% at some point go to vendors now.

Personally, I’d claim: Partial since 2007. Not that I could find slides to back that up :wink:

For message shapes, I’d like to throw in that we can put shape shifters into content.

That is, we could add a non-localized Fluent file, that’s just full of messages and message references, and that’s not exposed to l10n.

If that’s packaged in all locales like we do right now for preliminary Fluent files, or if we’d add a Source that’s locale-independent to the registry would be an implementation detail.

Obviously, this only works for DOM nodes which are pure shape shifters, and not for nodes which combine a label from here with a different accesskey, for example.

Aaaand on locales and bundles.

Right now, there’s nothing that specifies that a particular locale has only one bundle in a generateBundles iterator.

With our current APIs, that’s also hard to enforce.

To give a practical example, if there’s two files on both a language pack and packaged locales (omni.ja), you get four bundles for a single locale.

This could be changed by switching how the L10nRegistry combines resources into bundles, but would also intentionally create “conflicting” resources and add them to the same bundle.

An additional example here would be hot-patches delivered by Remote Settings into Firefox. To make those easy to ship, we might not ship all localized content in them, but just the strings that are changed. That’d be a explicitly partial and multi-bundle-per-locale scenario.

This approach would introduce a similar level of indirection as the one I was thinking about: <element l10n-bind="key1 title:key1.title">, except that it would also be prone to issues related to cross-channel, project config, error recovery, and parsing cost.

The big advantage of the l10n-bind approach is that it’s defined right where it’s used, in the source, and thus it moves with that source wherever it goes. It also makes it more discoverable and maintainable by not requiring a lookup in a different file.

I 100% agree that shipping partial translations is important. However, what got us into Firefox was that we could do partial translations without causing a YSOD. I’m realistic: getting message references right is secondary, especially given how rarely they are used the way you described it above (Click "{save}" or "{save-changes}"). I ran a quick test on gecko-strings and found 8 messages using this pattern. That’s fewer than 0.2% of all Fluent messages in Firefox.

I think it would be slightly more fair to evaluate the percentage of messagereferences that use it, than a percentage of all strings that use it.

But I agree that it’s a rare use case so far.

My expectation is that it will become slightly more common as we start migrating secondary UIs which reference primary UIs.
Either way, I hope we’ll have a better model to evaluate what works and what doesn’t within the next 3-6 months when majority of Firefox is in Fluent.

1 Like

That’s fair. I ran the numbers again. I understand that this is just a data point, that not all messages are in Fluent, and that there might be use-cases that we’re missing here.

  • All strings in Gecko: 10,644
  • All Fluent messages in Gecko: 4,264 (40% of all strings)
  • Fluent messages with MessageReferences: 90 (2% of all Fluent messages)
  • Fluent messages with MessageReferences used for consistency across UI widgets: 8 (9% of messages with MessageReferences, 0.18% of all Fluent messages)
  • The remaining references are for shape shifting or to avoid repetition of common phrases.
  • I didn’t count TermReferences.

The current use of MessageReferences is no indication of their usefulness, tbh.

Both flod and I strongly advise against using them, because the problems that made me open this thread make them not do what people hope for.

If those problems weren’t affecting us, I’d expect to have more of them.