Standardize `data-lazy-l10n-id` behavior
Categories
(Core :: Internationalization, enhancement, P3)
Tracking
()
People
(Reporter: zbraniecki, Unassigned)
References
(Blocks 1 open bug)
Details
We currently use data-lazy-l10n-id
attribute for scenarios where we have a piece of DOM code that we want to localize lazily.
https://searchfox.org/mozilla-central/search?q=data-lazy&case=true®exp=false&path=
This is a very important part of the future browser.xhtml behavior, since we have ~2000 strings loaded right now during startup, but we should be able to lazify almost, if not all, of them.
Currently, the way it works, is that someone puts such a block in DOM:
<popupnotification id="appMenu-update-available-notification"
popupid="update-available"
data-lazy-l10n-id="appmenu-update-available"
data-l10n-attrs="buttonlabel, buttonaccesskey, secondarybuttonlabel, secondarybuttonaccesskey"
closebuttonhidden="true"
dropmarkerhidden="true"
checkboxhidden="true"
buttonhighlight="true"
hidden="true">
<popupnotificationcontent id="update-available-notification-content" orient="vertical">
<description id="update-available-description" data-lazy-l10n-id="appmenu-update-available-message"></description>
<label id="update-available-whats-new" is="text-link" data-lazy-l10n-id="appmenu-update-whats-new"/>
</popupnotificationcontent>
</popupnotification>
and piece like this in JS:
document.addEventListener("event", () => {
// Insert Fluent files when needed before notification is opened
MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
MozXULElement.insertFTLIfNeeded("browser/appMenuNotifications.ftl");
// After Fluent files are loaded into document replace data-lazy-l10n-ids with actual ones
document
.getElementById("appMenu-notification-popup")
.querySelectorAll("[data-lazy-l10n-id]")
.forEach(el => {
el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
el.removeAttribute("data-lazy-l10n-id");
});
});
The result is quite clean, but hard to introspect and requires a DYI code in JS that has to be maintained.
I'd like to discuss possible ways to make it more DOM-y and fit better in our standards.
Reporter | ||
Comment 1•5 years ago
|
||
One way we could approach it is via <template/>
tag:
<template id="appMenu-notification-popup-template">
<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="browser/appMenuNotifications.ftl"/>
<popupnotification id="appMenu-update-available-notification"
popupid="update-available"
data-l10n-id="appmenu-update-available"
data-l10n-attrs="buttonlabel, buttonaccesskey, secondarybuttonlabel, secondarybuttonaccesskey"
closebuttonhidden="true"
dropmarkerhidden="true"
checkboxhidden="true"
buttonhighlight="true"
hidden="true">
<popupnotificationcontent id="update-available-notification-content" orient="vertical">
<description id="update-available-description" data-l10n-id="appmenu-update-available-message"></description>
<label id="update-available-whats-new" is="text-link" data-l10n-id="appmenu-update-whats-new"/>
</popupnotificationcontent>
</popupnotification>
</template>
document.addEventListener("event", () => {
Helper.enableTemplate("appMenu-notification-popup-template");
});
where the helper would query for links and insert them into the document's localization context and then clone content of the template and insert it into body by itself.
How does it sound?
Comment 2•5 years ago
|
||
insert into the body? Is that what we'd like to do always? I would assume one would need to explicitly insert the cloned document fragment somewhere in DOM tree.
But using template for lazy localization in general could be reasonable.
Comment 3•5 years ago
|
||
I have more questions than answers or opinions here:
Do we want lazy
- l10n?
- javascript?
- html?
- css?
- custom elements?
AFAICT, we went for lazy l10n to use Fluent while we couldn't use Fluent on the startup path. Maybe with caching, we could just not be lazy now?
OTH, I can see value in actually doing fully lazy UI features, but that might involve all of the aspects of those features above.
Comment 4•5 years ago
|
||
I'm in favor of migrating more of the browser document to use <template>
for lazy DOM insertion. We've seen good performance results from lazifying DOM in about:preferences, and there's some significant potential wins for doing the menus: https://bugzilla.mozilla.org/show_bug.cgi?id=1534371#c0.
Gijs filed to track this type of work a while back (Bug 1544027) after noticing some performance regressions as we migrated XBL elements to Custom Elements. XBL has a feature where it won't run constructors on hidden elements. This makes a bunch of our DOM "halfway lazy" so it's pretty cheap to load the document up with a ton of elements that won't be seen by most people. But:
- It's still not free (as can be seen with some of the measurements for lazifying the menu which I believe were taken when the menus were primarily XBL).
- We don't want to re-add this feature for Custom Elements. It's caused a ton of headaches for frontend development with guessing when an element will be constructed and jumping through hoops to force construction. I also understand from Emilio that running JS during layout prevents updates to the layout engine.
So I'd be happy with anything we could do to make Fluent work well <template> in chrome. A couple thoughts:
- Being able to include a <link> in the template and have Fluent be able to detect it would be good for ergonomics (without frontend dev having to manually insert it, worry about duplicate <link>s, etc) Sort of like what insertFTLIfNeeded does, but automatic.
- I don't think we'd need a new attribute if we store markup in the template and do as smaug suggested to return a cloned document fragment (since Fluent would automatically detect and localize the inserted nodes). I'm not sure if Fluent would ignore <template> contents during parse but I guess we would want it to if we went this route.
- I don't think this helper functionality should be tied to Fluent specifically. There are use cases to lazify DOM even when l10n isn't involved. I'd expect this to live somewhere more global, like a new JSM, included on MozHTMLElement (similar to parseXULToFragment), or a chrome only method on document
Updated•5 years ago
|
Reporter | ||
Comment 5•5 years ago
|
||
I verified that we currently do not localize data-l10n-id
inside <template/>
and we do localize it when cloned fragment gets appended into the DOM.
I believe this is the behavior we wanted, so the remaining question is about how to handle any links that such a template may want to inject.
I agree with Brian that we don't have to make it l10n-specific, just l10n-aware.
The l10n-aware
behavior is exclusively about being able to list FTL resources that are required for the fragment.
Here's a new proposal for the API:
<template id="my-template">
<link rel="localization" href="./resource.ftl"></link>
<span data-l10n-id="key1"></span>
</template>
let t = document.getElementById("my-template");
// in-place
Helper.enableLazyFragment(t);
// onto a destination
Helper.enableLazyFragment(t, destinationParent);
If the second argument is omitted, the template is de-lazified "in-place" by attaching a clone of its content onto the parent of the template element.
The code would do something like that:
function enableLazyFragment(template, destination = template.parentNode) {
let clone = document.importNode(template.content, true);
for (let link of clone.querySelector("link[rel=localization]")) {
MozXULElement.insertFTLIfNeeded(link.href);
link.remove();
}
destination.appendChild(clone);
}
Does it sound like what we're looking for?
Comment 6•5 years ago
|
||
(In reply to Zibi Braniecki [:zbraniecki][:gandalf] from comment #5)
I verified that we currently do not localize
data-l10n-id
inside<template/>
and we do localize it when cloned fragment gets appended into the DOM.I believe this is the behavior we wanted, so the remaining question is about how to handle any links that such a template may want to inject.
I agree with Brian that we don't have to make it l10n-specific, just l10n-aware.
The
l10n-aware
behavior is exclusively about being able to list FTL resources that are required for the fragment.Here's a new proposal for the API:
<template id="my-template"> <link rel="localization" href="./resource.ftl"></link> <span data-l10n-id="key1"></span> </template>
let t = document.getElementById("my-template"); // in-place Helper.enableLazyFragment(t); // onto a destination Helper.enableLazyFragment(t, destinationParent);
If the second argument is omitted, the template is de-lazified "in-place" by attaching a clone of its content onto the parent of the template element.
The code would do something like that:
function enableLazyFragment(template, destination = template.parentNode) { let clone = document.importNode(template.content, true); for (let link of clone.querySelector("link[rel=localization]")) { MozXULElement.insertFTLIfNeeded(link.href); link.remove(); } destination.appendChild(clone); }
Does it sound like what we're looking for?
Yeah, I think something like that would work. I'm not sure that we need to support the appending behavior at first though - because we don't have much <template> usage yet and I'm not sure where a template would typically live in relation to the destination. Since this is more likely to be used in a single document (as adding the <template> in the markup would imply this isn't part of a shared widget), I do expect we would often be "unwrapping" the template (replacing it with itself), but I'm not sure until we started using it.
For a first pass, we could aim for the pattern to be something closer to parseXULToFragment, where it returns a cloned fragment and injects the links appropriately (which by the way we should update parseXULToFragment to do as well). Then consumers can be in charge of putting the fragment in the right place. Needinfo'ing Gijs in case he has an opinion wrt Bug 1544027.
So in the case of Comment 1 we could have something like:
document.addEventListener("event", () => {
let template = document.querySelector("#appMenu-notification-popup-template");
if (template) {
template.replaceWith(MozXULElement.getTemplateFragment(template));
}
// DOM is here now
let notification = document.querySelector("#appMenu-update-available-notification");
});
class MozElementMixin {
...
static function getTemplateFragment(template) {
let clone = document.importNode(template.content, true);
for (let link of clone.querySelector("link[rel=localization]")) {
MozXULElement.insertFTLIfNeeded(link.href);
link.remove();
}
return clone;
}
...
}
Comment 7•5 years ago
|
||
I think the plans here sound good. I agree that we'd want the <link>
reordering to be automatic.
In terms of ergonomics etc., I wonder if the simplest and most powerful abstraction would be if we put this inside a ChromeOnly
webidl method on HTML <template>
tags (implement in gecko/C++). Then we don't need jsms or anything else to use it, and it could be as simple as templateNode.unwrap()
or templateNode.copyTo(dest)
. Or, if there is already a web API that gets us a DocumentFragment from the contents of a <template>
(which it feels like there should be?) add a ChromeOnly equivalent of that with the extra arguments as appropriate.
Comment 8•5 years ago
|
||
(In reply to :Gijs (he/him) from comment #7)
In terms of ergonomics etc., I wonder if the simplest and most powerful abstraction would be if we put this inside a
ChromeOnly
webidl method on HTML<template>
tags (implement in gecko/C++). Then we don't need jsms or anything else to use it, and it could be as simple astemplateNode.unwrap()
ortemplateNode.copyTo(dest)
. Or, if there is already a web API that gets us a DocumentFragment from the contents of a<template>
(which it feels like there should be?) add a ChromeOnly equivalent of that with the extra arguments as appropriate.
Including this as a static function on MozXULElement would be consistent with how we expose similar types of functionality where (a) we don't anticipate a large perf improvement in C++ and (b) is always running in a context where a chrome window is available (since the <template> will have an ownerDocument). I'm thinking of functions like insertFTLIfNeeded
and parseXULToFragment
. It's possible that (a) is wrong and we should port more of these features into ChromeOnly webidl methods.
Comment 9•5 years ago
|
||
Whichever API we decide on, this should also help us remove the preprocessor in our xhtml files in a lot of cases (like https://searchfox.org/mozilla-central/rev/efdf9bb55789ea782ae3a431bda6be74a87b041e/toolkit/content/aboutSupport.xhtml#257-277 or https://searchfox.org/mozilla-central/rev/efdf9bb55789ea782ae3a431bda6be74a87b041e/browser/components/preferences/in-content/main.inc.xhtml#40-60), where we could <template> instead of #ifdef and then "unwrap" it from JS based on the matching AppConstants value.
Updated•2 years ago
|
Description
•