Closed Bug 1347799 Opened 7 years ago Closed 7 years ago

Create Localization DOM bindings module

Categories

(Core :: Internationalization, enhancement)

enhancement
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla57
Tracking Status
firefox57 --- fixed

People

(Reporter: zbraniecki, Assigned: zbraniecki)

References

(Blocks 1 open bug)

Details

Attachments

(1 file)

We'll want to have a single module that can be loaded into a document context that will be binding localization API into the DOM.
Depends on: 1347800
Assignee: nobody → gandalf
Status: NEW → ASSIGNED
This is the last piece of the new L10n API

It basically has three elements:

1) DOMLocalization class which has a set of methods allowing for DOM localization using Localization API

2) MutationObserver which can be connected to any number of roots (currently just document, but this is ussedful when we add XBL bindings or WebComponents with Shadow DOM in the future)

3) DOM Overlay technology

The DOM Overlay is the most interesting piece I believe as it allows us to create smart rich localizations.
It's explained here: https://blog.mozilla.org/l10n/2012/07/16/l20n-features-explained-dom-overlays/

We've done extensive security review around it when we added it to Firefox OS and we recently looked again at it with :freddyb.


The DOMLocalization declarative API with element attributes data-l10n-id and data-l10n-args will be the primary localization method for the UI. It's fully asynchronous, will handle fancy features like runtime fallback, live retranslations and even more powerful features in the future (like, adaptive localization that responds to available space).

Performance wise, I belive that if proven successful, we may want to move the whole DOMLocalization to DOM as some "MozWebL10n" API, but initially, the JS performs quite well.

There are two follow ups that I'll want to handle before we start using the API (both are blockers to bug 1365426):

1) During perf profiling when turned browser.xul to use this API, we identified that the main performance cost is due to us touching the DOM Elements early which forces reflections to be created.

Smaug wrote a POC patch (bug 1363862) which basically does the "collect elements with data-l10n-id" and "apply translations" on the C++ side. The patch works well and we may want to land it before enabling the API, but doesn't affect anything in the public API.

2) At the mement, we perform all translations in each Mutation Observer callback.

This is not a major issue, since dynamic updates to data-l10n-id/args aren't common and our experience from FxOS indicates that they're not common even once the product switches to it, but there is a possibility that we could batch a number of MutationObserver mutations together and perform localization just once per frame.
I filed bug 1389384 to investigate this.
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

https://reviewboard.mozilla.org/r/120912/#review173700

::: intl/l10n/DOMLocalization.jsm:105
(Diff revision 5)
> + * The goal of overlay is to move the children of `translationElement`
> + * into `sourceElement` such that `sourceElement`'s own children are not
> + * replaced, but only have their text nodes and their attributes modified.

As far as I can tell there are places where you'll throw away some source elements and strange things will happen when switching locales are runtime. Would it be better to mandate that (aside from textcontent) the localized element structure exactly match that of the source elements? When would we want localizers to add styling that the original document author didn't intend? If not maybe it would be easier to just always throw away the source if it is possible to make elements disappear anyway.

::: intl/l10n/DOMLocalization.jsm:127
(Diff revision 5)
> +  let k, attr;
> +
> +  // Take one node from translationElement at a time and check it against
> +  // the allowed list or try to match it with a corresponding element
> +  // in the source.
> +  let childElement;

This iterates over nodes so call it childNode.

::: intl/l10n/DOMLocalization.jsm:136
(Diff revision 5)
> +    if (childElement.nodeType === childElement.TEXT_NODE) {
> +      result.appendChild(childElement);
> +      continue;
> +    }
> +
> +    const index = getIndexOfType(childElement);

So you've removed the element from the template at this point, isn't this always going to return 0?

::: intl/l10n/DOMLocalization.jsm:146
(Diff revision 5)
> +      const sanitizedChild = childElement.ownerDocument.createElement(
> +        childElement.nodeName);

You should use createElementNS here.

::: intl/l10n/DOMLocalization.jsm:166
(Diff revision 5)
> +  sourceElement.textContent = '';
> +  sourceElement.appendChild(result);
> +
> +  // If we're overlaying a nested element, translate the allowed
> +  // attributes; top-level attributes are handled in `overlayElement`.
> +  // XXX Attributes previously set here for another language should be

File a bug for this.

::: intl/l10n/DOMLocalization.jsm:169
(Diff revision 5)
> +  // If we're overlaying a nested element, translate the allowed
> +  // attributes; top-level attributes are handled in `overlayElement`.
> +  // XXX Attributes previously set here for another language should be
> +  // cleared if a new language doesn't use them; https://bugzil.la/922577
> +  if (translationElement.attributes) {
> +    for (k = 0, attr; (attr = translationElement.attributes[k]); k++) {

Setting attr in the condition is sort of confusing and I'd like to avoid it. I think you can just do:

    for (const attr of translationElement.attributes)

::: intl/l10n/DOMLocalization.jsm:246
(Diff revision 5)
> +  return false;
> +}
> +
> +/**
> + * Get n-th immediate child of context that is of the same type as element.
> + * XXX Use querySelector(':scope > ELEMENT:nth-of-type(index)'), when:

File a bug for this

::: intl/l10n/DOMLocalization.jsm:304
(Diff revision 5)
> +   * @param {Array<String>}    resourceIds      - List of resource IDs
> +   * @param {Function}         generateMessages - Function that returns a
> +   *                                              generator over MessageContexts
> +   * @returns {DOMLocalization}
> +   */
> +  constructor(MutationObserver, resourceIds, generateMessages) {

It's a bit of a weird API to pass in the MutationObserver. I know why you need it but it feels more natural to say that this is a localization for a particular window then you can just get window.MutationObserver off it.

::: intl/l10n/DOMLocalization.jsm:306
(Diff revision 5)
> +   *                                              generator over MessageContexts
> +   * @returns {DOMLocalization}
> +   */
> +  constructor(MutationObserver, resourceIds, generateMessages) {
> +    super(resourceIds, generateMessages);
> +    this.query = '[data-l10n-id]';

Why is this per-instance rather than hardoded at the top of the file?

::: intl/l10n/DOMLocalization.jsm:319
(Diff revision 5)
> +    this.observerConfig = {
> +      attribute: true,
> +      characterData: false,
> +      childList: true,
> +      subtree: true,
> +      attributeFilter: ['data-l10n-id', 'data-l10n-args']

These strings are used often enough that it makes sense to put them as constants at the top of the file.

::: intl/l10n/DOMLocalization.jsm:367
(Diff revision 5)
> +   */
> +  setAttributes(element, id, args) {
> +    element.setAttribute('data-l10n-id', id);
> +    if (args) {
> +      element.setAttribute('data-l10n-args', JSON.stringify(args));
> +    }

You should probably remove the attribute in the else case since the caller might be retranslating an existing element.

::: intl/l10n/DOMLocalization.jsm:384
(Diff revision 5)
> +   * ```
> +   *
> +   * @param   {Element}  element - HTML element
> +   * @returns {{id: string, args: Object}}
> +   */
> +  getAttributes(element) {

Who would use this and why?

::: intl/l10n/DOMLocalization.jsm:397
(Diff revision 5)
> +   * Add `root` to the list of roots managed by this `DOMLocalization`.
> +   *
> +   * Additionally, if this `DOMLocalization` has an observer, start observing
> +   * `root` in order to translate mutations in it.
> +   *
> +   * @param {Element}      root - Root to observe.

Can this also be made to support windows or documents. Seems a pain to have to call connectRoot(document.documentElement) all the time.

::: intl/l10n/DOMLocalization.jsm:402
(Diff revision 5)
> +   * @param {Element}      root - Root to observe.
> +   */
> +  connectRoot(root) {
> +    this.roots.add(root);
> +    this.mutationObserver.observe(root, this.observerConfig);
> +  }

When would you want to connect a root but not translate it.

::: intl/l10n/DOMLocalization.jsm:419
(Diff revision 5)
> +   * @returns {boolean}
> +   */
> +  disconnectRoot(root) {
> +    this.roots.delete(root);
> +    // Pause and resume the mutation observer to stop observing `root`.
> +    this.pauseObserving();

This will empty the message queue possibly causing you to lose mutation events that need translating.

::: intl/l10n/DOMLocalization.jsm:515
(Diff revision 5)
> +   * Returns a `Promise` that gets resolved once the translation is complete.
> +   *
> +   * @param   {Element} element - HTML element to be translated
> +   * @returns {Promise}
> +   */
> +  translateElement(element) {

This would probably read better as an async function.

::: intl/l10n/DOMLocalization.jsm:529
(Diff revision 5)
> +   * @param {Array<Element>} elements
> +   * @param {Array<Object>}  translations
> +   * @private
> +   */
> +  applyTranslations(elements, translations) {
> +    this.pauseObserving();

Why does this need to pause observing?

::: intl/l10n/DOMLocalization.jsm:548
(Diff revision 5)
> +   * @private
> +   */
> +  getTranslatables(element) {
> +    const nodes = Array.from(element.querySelectorAll(this.query));
> +
> +    if (typeof element.hasAttribute === 'function' &&

Why wouldn't this function exist?

::: intl/l10n/DOMLocalization.jsm:573
(Diff revision 5)
> +    ];
> +  }
> +}
> +
> +this.DOMLocalization = DOMLocalization;
> +this.EXPORTED_SYMBOLS = [];

I didn't call this out in previous reviews because I didn't think it was a problem but I've realised that this will break the attempt to use this with defineLazyModuleGetter which we might well want to do for some of your modules. Can you please add the symbols to EXPORTED_SYMBOLS throughout.

::: intl/l10n/test/dom/test_domloc_overlay.html:17
(Diff revision 5)
> +    mc.addMessages('title = <strong>Hello</strong> World');
> +    mc.addMessages('title2 = This is <a>a link</a>!');

Please add some more tests:

* A test where the same element is repeated
* A test with two elements different where the source element has the same elements in reverse order
* A test where the source element has more elements than the translation
Attachment #8847967 - Flags: review?(dtownsend) → review-
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

https://reviewboard.mozilla.org/r/120912/#review173700

> As far as I can tell there are places where you'll throw away some source elements and strange things will happen when switching locales are runtime. Would it be better to mandate that (aside from textcontent) the localized element structure exactly match that of the source elements? When would we want localizers to add styling that the original document author didn't intend? If not maybe it would be easier to just always throw away the source if it is possible to make elements disappear anyway.

I'll let :stas comment on this. He's working this quarter on completing the DOM Overlays story. What we have in this patch is the foundation that enables the simple things to work, but are immediatelly useful for the common cases like "a single link in text".

> You should use createElementNS here.

I'm not sure why but when I do this, then `elem.querySelector('strong')`, `elem.getElementsByTagName('strong')` and `elem.getElementsByTagNameNS(elem.namespaceURI, 'strong')` don't work.

I kept it as is, since it works in both XUL and HTML, but if you want to dive deeper into this, I can file a follow up bug.

> File a bug for this.

There's already a bug filed (bug 922577) - I added it as a blocker for bug 1365426.

> File a bug for this

Stas decided to remove this feature for now.

> Who would use this and why?

Since the whole way we store id/args as `data-*` attributes is a bit hacky, we wanted to create a wrapper to standardize how to set/get the l10n related attributes from an element.

This is useful to avoid users manually having to pick the `data-l10n-args` and then `JSON.parse` on it. In theory, if users only rely on this, we could move from `data-*` attributes to something else, or from JSON to another format for the args.

> Can this also be made to support windows or documents. Seems a pain to have to call connectRoot(document.documentElement) all the time.

How would you want it to look like?

> When would you want to connect a root but not translate it.

There are cases where you want to control when you translate and when you start observing separately. This API allows for keeping a granular control over those two operations.

> Why does this need to pause observing?

I believe when we wrote it I was under the impression is that if I modify the MutationObserver targets in `onMutations` callback, the changes may be picked up by the MutationObserver again.

I removed it, but if you think I may be right, please, let me know :)

> Why wouldn't this function exist?

DocumentFragment doesn't have `hasAttribute`.

> I didn't call this out in previous reviews because I didn't think it was a problem but I've realised that this will break the attempt to use this with defineLazyModuleGetter which we might well want to do for some of your modules. Can you please add the symbols to EXPORTED_SYMBOLS throughout.

fixed and added the symbols to the previous modules.

> Please add some more tests:
> 
> * A test where the same element is repeated
> * A test with two elements different where the source element has the same elements in reverse order
> * A test where the source element has more elements than the translation

I added the first and third of those. Since we removed the ordering control, I didn't add the second yet. Once :stas adds it to DOM Overlays feature, I'll add tests for it. Is that ok?
Stas, can you comment on the :mossop's comment at the top of his review?
Flags: needinfo?(stas)
(In reply to Zibi Braniecki [:gandalf][:zibi] from comment #9)
> > Why does this need to pause observing?
> 
> I believe when we wrote it I was under the impression is that if I modify
> the MutationObserver targets in `onMutations` callback, the changes may be
> picked up by the MutationObserver again.
> 
> I removed it, but if you think I may be right, please, let me know :)

I think you may be right so you probably do need to pause observing and empty out the current queue. I don't think there is anything racy that can happen there but it's worth some thought.
Ok, re-added the pause/resume and aligned our fluent-dom module with DOMLocalization.jsm. Ready for review.
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

Waiting on stas to comment here
Attachment #8847967 - Flags: review?(dtownsend)
(In reply to Dave Townsend [:mossop] from comment #7)

> As far as I can tell there are places where you'll throw away some source
> elements and strange things will happen when switching locales are runtime.

This is correct; some elements might be removed if they're not present in the translation.  I think that for now this can be considered fine: HTML in translations is rare and changing the language on runtime is also rare, so the risk of running into troubles is low.

OTOH, I'm all for making this code simpler, more dumb and more strict. I'm open to making changes to it now so that we don't end up with leaky features that we'll have to maintain in the future.

> Would it be better to mandate that (aside from textcontent) the localized
> element structure exactly match that of the source elements? When would we
> want localizers to add styling that the original document author didn't
> intend?

One of the goals of the DOM overlays was to allow localizers to use text-level elements (em, strong etc.) which relate to the typography. To quote the design document:

> Make it possible for localizers to apply text-level semantics to the translations and make use of HTML entities. For instance, it should be possible for a localizer to use a sup element in "Mme" (an abbreviation of French "Madame") even if the original source string doesn't contain the sup element.

See https://github.com/l20n/spec/blob/master/dom-overlays.markdown#text-level-semantic-elements for the discussion and the list of allowed elements.

If we want to keep this goal, we can't treat the current elements in the DOM as authoritative: they might have been added by the localization.  We could require the translation to always at least include the non-text-level elements that are in the source which are important for the proper functioning of the UI: buttons, inputs and the like. This would guarantee that an runtime retranslation doesn't remove any of them.

> If not maybe it would be easier to just always throw away the source
> if it is possible to make elements disappear anyway.

What do you mean by "always throw away the source"?  The reason why we match elements in the translation with the elements in the source is to preserve any event handlers and logic attached to elements like buttons.
Flags: needinfo?(stas)
(In reply to Staś Małolepszy :stas from comment #15)
> (In reply to Dave Townsend [:mossop] from comment #7)
> What do you mean by "always throw away the source"?  The reason why we match
> elements in the translation with the elements in the source is to preserve
> any event handlers and logic attached to elements like buttons.

I mean just remove all the elements inside the source element and create new ones from the translation. If the translation can cause those elements with event handlers and logic attached to them to be thrown away then that will break the application so the application really shouldn't be relying on them being present.
Attachment #8847967 - Flags: review?(dtownsend)
Flags: needinfo?(stas)
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

https://reviewboard.mozilla.org/r/120912/#review174998

Throughout you switch between nodeName and tagName to get an element's name. You should probably actually be using localName to avoid namespace prefixes and uppercasing in HTML.

I'm not seeing any tests that attempt to add disallowed elements or attributes in both XUL and HTML.

::: intl/l10n/DOMLocalization.jsm:87
(Diff revision 7)
> +    } else {
> +      // Else start with an inert template element and move its children into
> +      // `element` but such that `element`'s own children are not replaced.
> +      const tmpl = element.ownerDocument.createElementNS(
> +        'http://www.w3.org/1999/xhtml', 'template');
> +      tmpl.innerHTML = value;

Does this ever execute scripts? Has there been a security review of the new system?

::: intl/l10n/DOMLocalization.jsm:136
(Diff revision 7)
> +    if (sourceChild) {
> +      // There is a corresponding element in the source, let's use it.
> +      sourceElement.removeChild(sourceChild);
> +      overlay(sourceChild, childNode);
> +      result.appendChild(sourceChild);
> +      continue;
> +    }

You don't test that isElementAllowed here. It might allow a poorly written app to allow a locale to overwrite a script element for example.

::: intl/l10n/DOMLocalization.jsm:198
(Diff revision 7)
> + *
> + * This method is used by the sanitizer when the translation markup contains
> + * DOM attributes, or when the translation has traits which map to DOM
> + * attributes.
> + *
> + * @param   {{name: string}} attr

Why is this an object with a name property rather than just the name itself?

::: intl/l10n/DOMLocalization.jsm:394
(Diff revision 7)
> +   * @returns {boolean}
> +   */
> +  disconnectRoot(root) {
> +    this.roots.delete(root);
> +    // Pause and resume the mutation observer to stop observing `root`.
> +    this.pauseObserving();

This could potentially cause us to see and asynchronously apply new translations to the root that just got removed. I don't see an easy way around that unless you want to assume that roots can't overlap so maybe just mention it in the comments?

::: intl/l10n/DOMLocalization.jsm:522
(Diff revision 7)
> +   * @param {Element} element
> +   * @returns {Array<Element>}
> +   * @private
> +   */
> +  getTranslatables(element) {
> +    const nodes = Array.from(element.querySelectorAll(l10nElementQuery));

This won't see XBL anonymous content or HTML shadow content I think. IS that an issue?
Attachment #8847967 - Flags: review?(dtownsend) → review-
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

https://reviewboard.mozilla.org/r/120912/#review174998

> Does this ever execute scripts? Has there been a security review of the new system?

The template element has inert DOM and doesn't execute scripts nor try to load images when innerHTML is assigned.  The security review was done in bug 925579.

> You don't test that isElementAllowed here. It might allow a poorly written app to allow a locale to overwrite a script element for example.

If we test `isElementAllowed` here, i.e. we use the whitelist, we'll filter out non-text level elements like buttons and inputs.  The idea behind this code was to allow the developer to allow more element types by putting them in the source.

Given that you already pointed out how this may break when retranslation happens or when the localization doesn't have all the elements, I wouldn't be opposed to removing this feature completely.  I don't think it's common these days to put an input or a button inline with the text in the middle of a sentence. Do you think this would be a better approach for the initial landing?

> Why is this an object with a name property rather than just the name itself?

I wanted this to be consistent with `isElementAllowed` which takes an attribute rather than its name.  Now that I think about it, it probably wasn't worth it; thanks for pointing that out.  For instance, it requires a special treatment in overlayElement where `isAttrAllowed` is used to test translation attributes (not DOM attributes).  We only have the name there, not a DOM attribute object, so something like `if (isAttrAllowed({ name }, element))` is needed.

How about we change this function to `isAttrNameAllowed`?

> This could potentially cause us to see and asynchronously apply new translations to the root that just got removed. I don't see an easy way around that unless you want to assume that roots can't overlap so maybe just mention it in the comments?

I'm not sure I see how translations could be applied to the removed root here?  `translateMutations` only cares about `addedNodes`.

I agree that it's worth mentioning that roots should not overlap.  That's assuming we don't count anonymous content and shadow content as overlapping.

> This won't see XBL anonymous content or HTML shadow content I think. IS that an issue?

If I recall correctly, the idea was the anonymous content and shadow content will need to be explicitly observed (and unobserved) via `connectRoot` in their lifecycle methods.  We used to have a working solution for XBL back in London 2016 which did `document.getAnonymousNodes(root)` somewhere between `translateRoots` and `translateFragment`.  Since then we removed it to make the initial code simpler.  We wanted to to focus on supporting HTML and XUL first.
(In reply to Staś Małolepszy :stas from comment #18)
> Comment on attachment 8847967 [details]
> Bug 1347799 - Add DOMLocalization module for the new Localization API.
> > You don't test that isElementAllowed here. It might allow a poorly written app to allow a locale to overwrite a script element for example.
> 
> If we test `isElementAllowed` here, i.e. we use the whitelist, we'll filter
> out non-text level elements like buttons and inputs.  The idea behind this
> code was to allow the developer to allow more element types by putting them
> in the source.
> 
> Given that you already pointed out how this may break when retranslation
> happens or when the localization doesn't have all the elements, I wouldn't
> be opposed to removing this feature completely.  I don't think it's common
> these days to put an input or a button inline with the text in the middle of
> a sentence. Do you think this would be a better approach for the initial
> landing?

I think mostly I worry that the way we do this overlaying of elements will get enshrined in the code and if we find the behaviour is broken we'll never be able to change it without breaking other code. I'd certainly prefer to just not do it in the first pass and then add it later if it comes up as needed.

> > Why is this an object with a name property rather than just the name itself?
> 
> I wanted this to be consistent with `isElementAllowed` which takes an
> attribute rather than its name.  Now that I think about it, it probably
> wasn't worth it; thanks for pointing that out.  For instance, it requires a
> special treatment in overlayElement where `isAttrAllowed` is used to test
> translation attributes (not DOM attributes).  We only have the name there,
> not a DOM attribute object, so something like `if (isAttrAllowed({ name },
> element))` is needed.
> 
> How about we change this function to `isAttrNameAllowed`?

Sounds good to me.

> > This could potentially cause us to see and asynchronously apply new translations to the root that just got removed. I don't see an easy way around that unless you want to assume that roots can't overlap so maybe just mention it in the comments?
> 
> I'm not sure I see how translations could be applied to the removed root
> here?  `translateMutations` only cares about `addedNodes`.

The call to pauseObserving causes you to take any pending mutation records from the queue which could include added nodes under the root that has just been removed. If you assume roots are non-overlapping then you'd be able to test every addedNode for whether it appears under a current root but that might be more of a performance hit than we want.

> I agree that it's worth mentioning that roots should not overlap.  That's
> assuming we don't count anonymous content and shadow content as overlapping.

You could verify that that is the case if you never want it to happen.
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

Updated the patch to some of the feedback from :mossop.

To do:

1) Add a couple tests :mossop asked for (gandalf)
2) Add a note about overlapping roots (gandalf)
3) Update DOMOverlays code to match the consensus (stas)
Attachment #8847967 - Flags: review?(dtownsend)
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

I added the check on connectRoot to prevent overlapping roots from being added.

:stas, I believe that now we're just waiting for the update to overlays.
Attachment #8847967 - Flags: review?(dtownsend)
Zibi, I think it would be helpful if you landed those changes in fluent.js's master. Here's a PR for the DOM overlays changes: https://github.com/projectfluent/fluent.js/pull/71
Flags: needinfo?(stas)
Attachment #8847967 - Flags: review?(dtownsend)
Dave, I believe that this is ready for your review. We updated the DOMOverlay logic in https://github.com/projectfluent/fluent.js/pull/71 and I applied your last round of feedback in https://github.com/projectfluent/fluent.js/pull/72
We set out to make the overlay logic less complex and more predictable.  Here's a summary of changes from the PR that Zibi linked above:

  - Only children of the white-listed types are allowed now. It's not possible anymore to put elements of other types in the source HTML to make exceptions. This effectively limits the use-case for DOM overlays to text-level elements. Buttons or inputs will always be removed from the source DOM which makes the whole system more deterministic (at the cost of being more limited).

  - The identity of the source element's children is explicitly not kept anymore. This allows us to treat the translation's DocumentFragment as the reference for iteration over child nodes. We still look for matching children in the source element, but we only use their attributes in the result.

  - The overlay function (now called `sanitizeUsing`) is no longer recursive. Any nested HTML will be lost and only its textContent will be preserved.
Comment on attachment 8847967 [details]
Bug 1347799 - Add DOMLocalization module for the new Localization API.

https://reviewboard.mozilla.org/r/120912/#review180644
Attachment #8847967 - Flags: review?(dtownsend) → review+
Pushed by zbraniecki@mozilla.com:
https://hg.mozilla.org/integration/autoland/rev/4e0649ecbe50
Add DOMLocalization module for the new Localization API. r=mossop
https://hg.mozilla.org/mozilla-central/rev/4e0649ecbe50
Status: ASSIGNED → RESOLVED
Closed: 7 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla57
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: