Bug 1363862 - Add a chrome-only DOM API for localization.
MozReview-Commit-ID: 9XbUtQByNR6
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -295,16 +295,18 @@ GK_ATOM(count, "count")
GK_ATOM(crop, "crop")
GK_ATOM(crossorigin, "crossorigin")
GK_ATOM(curpos, "curpos")
GK_ATOM(current, "current")
GK_ATOM(cutoutregion, "cutoutregion")
GK_ATOM(cycler, "cycler")
GK_ATOM(data, "data")
GK_ATOM(datalist, "datalist")
+GK_ATOM(datal10nid, "data-l10n-id")
+GK_ATOM(datal10nargs, "data-l10n-args")
GK_ATOM(dataType, "data-type")
GK_ATOM(dateTime, "date-time")
GK_ATOM(datasources, "datasources")
GK_ATOM(date, "date")
GK_ATOM(datetime, "datetime")
GK_ATOM(datetimebox, "datetimebox")
GK_ATOM(dblclick, "dblclick")
GK_ATOM(dd, "dd")
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -23,16 +23,19 @@
#include "mozilla/MemoryReporting.h"
#include "mozilla/ServoBindings.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/css/StyleRule.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
+#include "mozilla/dom/NodeBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/ShadowRoot.h"
#include "nsAttrValueOrString.h"
#include "nsBindingManager.h"
#include "nsCCUncollectableMarker.h"
#include "nsContentCreatorFunctions.h"
#include "nsContentList.h"
#include "nsContentUtils.h"
#include "nsCycleCollectionParticipant.h"
@@ -3123,8 +3126,202 @@ nsINode::IsStyledByServo() const
}
#endif
DocGroup*
nsINode::GetDocGroup() const
{
return OwnerDoc()->GetDocGroup();
}
+
+
+
+
+class LocalizationHandler : public PromiseNativeHandler
+{
+public:
+ LocalizationHandler() {}
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(LocalizationHandler)
+
+ nsTArray<nsCOMPtr<Element>>& Elements() { return mElements; }
+
+ void SetReturnValuePromise(Promise* aReturnValuePromise)
+ {
+ mReturnValuePromise = aReturnValuePromise;
+ }
+
+ virtual void
+ ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+ {
+ // Error handling could possibly be better here. Now we just reject with
+ // undefined.
+ nsTArray<L10nValue> l10nData;
+ if (aValue.isObject()) {
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+ if (!iter.valueIsIterable()) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+
+ JS::Rooted<JS::Value> temp(aCx);
+ while (true) {
+ bool done;
+ if (!iter.next(&temp, &done)) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+
+ if (done) {
+ break;
+ }
+
+ L10nValue* slotPtr =
+ l10nData.AppendElement(mozilla::fallible);
+ if (!slotPtr) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+
+ L10nValue& slot = *slotPtr;
+ if (!slot.Init(aCx, temp)) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+ }
+ }
+
+ if (mElements.Length() != l10nData.Length()) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+
+ ErrorResult rv;
+ for (size_t i = 0; i < l10nData.Length(); ++i) {
+ Element* elem = mElements[i];
+ nsString& content = l10nData[i].mContent;
+ if (!content.IsVoid()) {
+ elem->SetTextContent(content, rv);
+ if (rv.Failed()) {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+ }
+
+ Nullable<Sequence<AttributeNameValue>>& attributes =
+ l10nData[i].mAttributes;
+ if (!attributes.IsNull()) {
+ for (size_t j = 0; j < attributes.Value().Length(); ++j) {
+ // Use SetAttribute here to validate the attribute name!
+ elem->SetAttribute(attributes.Value()[j].mName,
+ attributes.Value()[j].mValue,
+ rv);
+ if (rv.Failed()) {
+
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ return;
+ }
+ }
+ }
+ }
+ mReturnValuePromise->MaybeResolveWithUndefined();
+ }
+
+ virtual void
+ RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+ {
+ mReturnValuePromise->MaybeRejectWithUndefined();
+ }
+
+private:
+ ~LocalizationHandler() {}
+
+ nsTArray<nsCOMPtr<Element>> mElements;
+ RefPtr<Promise> mReturnValuePromise;
+};
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalizationHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(LocalizationHandler)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalizationHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(LocalizationHandler)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalizationHandler)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mElements)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mReturnValuePromise)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalizationHandler)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElements)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReturnValuePromise)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+
+already_AddRefed<Promise>
+nsINode::Localize(mozilla::dom::L10nCallback& aCallback,
+ mozilla::ErrorResult& aRv)
+{
+ nsAutoString l10nId;
+ nsAutoString l10nArgs;
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(OwnerDoc()->GetParentObject())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ Sequence<L10nElement> l10nElements;
+ SequenceRooter<L10nElement> rooter(jsapi.cx(), &l10nElements);
+ RefPtr<LocalizationHandler> nativeHandler = new LocalizationHandler();
+ nsTArray<nsCOMPtr<Element>>& domElements = nativeHandler->Elements();
+ nsIContent* node = IsContent() ? AsContent() : GetFirstChild();
+ for (; node; node = node->GetNextNode(this)) {
+ if (!node->IsElement()) {
+ continue;
+ }
+
+ Element* domElement = node->AsElement();
+ if (domElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, l10nId)) {
+ domElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs);
+ L10nElement* element = l10nElements.AppendElement(fallible);
+ if (!element) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+ domElements.AppendElement(domElement);
+
+ domElement->GetNamespaceURI(element->mNamespaceURI);
+ element->mLocalName = domElement->LocalName();
+ element->mL10nId = l10nId;
+ if (!l10nArgs.IsEmpty()) {
+ JS::Rooted<JS::Value> json(jsapi.cx());
+ if (!JS_ParseJSON(jsapi.cx(), l10nArgs.get(), l10nArgs.Length(), &json) ||
+ !json.isObject()) {
+ aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+ return nullptr;
+ }
+ element->mL10nArgs = &json.toObject();
+ }
+ }
+ }
+
+ RefPtr<Promise> callbackResult = aCallback.Call(l10nElements, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(OwnerDoc()->GetParentObject(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ nativeHandler->SetReturnValuePromise(promise);
+ callbackResult->AppendNativeHandler(nativeHandler);
+
+ return promise.forget();
+}
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -77,18 +77,20 @@ class AccessibleNode;
struct BoxQuadOptions;
struct ConvertCoordinateOptions;
class DocGroup;
class DOMPoint;
class DOMQuad;
class DOMRectReadOnly;
class Element;
class EventHandlerNonNull;
+class L10nCallback;
template<typename T> class Optional;
class OwningNodeOrString;
+class Promise;
template<typename> class Sequence;
class Text;
class TextOrElementOrDocument;
struct DOMPointInit;
struct GetRootNodeOptions;
enum class CallerType : uint32_t;
} // namespace dom
} // namespace mozilla
@@ -1829,16 +1831,19 @@ public:
void BindObject(nsISupports* aObject);
// After calling UnbindObject nsINode object doesn't keep
// aObject alive anymore.
void UnbindObject(nsISupports* aObject);
void GetBoundMutationObservers(nsTArray<RefPtr<nsDOMMutationObserver> >& aResult);
void GenerateXPath(nsAString& aResult);
+ already_AddRefed<mozilla::dom::Promise>
+ Localize(mozilla::dom::L10nCallback& aCallback, mozilla::ErrorResult& aRv);
+
already_AddRefed<mozilla::dom::AccessibleNode> GetAccessibleNode();
/**
* Returns the length of this node, as specified at
* <http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#concept-node-length>
*/
uint32_t Length() const;
--- a/dom/webidl/Node.webidl
+++ b/dom/webidl/Node.webidl
@@ -8,16 +8,36 @@
*
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
* liability, trademark and document use rules apply.
*/
interface Principal;
interface URI;
+dictionary L10nElement {
+ required DOMString namespaceURI;
+ required DOMString localName;
+ required DOMString l10nId; // value of data-l10n-id
+ object? l10nArgs = null; // json value of data-l10n-args attribute
+};
+
+dictionary AttributeNameValue {
+ required DOMString name;
+ required DOMString value;
+};
+
+dictionary L10nValue {
+ DOMString? content = null;
+ sequence<AttributeNameValue>? attributes = null;
+};
+
+callback L10nCallback =
+ Promise<sequence<L10nValue>> (sequence<L10nElement> l10nElements);
+
interface Node : EventTarget {
const unsigned short ELEMENT_NODE = 1;
const unsigned short ATTRIBUTE_NODE = 2; // historical
const unsigned short TEXT_NODE = 3;
const unsigned short CDATA_SECTION_NODE = 4; // historical
const unsigned short ENTITY_REFERENCE_NODE = 5; // historical
const unsigned short ENTITY_NODE = 6; // historical
const unsigned short PROCESSING_INSTRUCTION_NODE = 7;
@@ -107,16 +127,19 @@ interface Node : EventTarget {
readonly attribute Principal nodePrincipal;
[ChromeOnly]
readonly attribute URI? baseURIObject;
[ChromeOnly]
sequence<MutationObserver> getBoundMutationObservers();
[ChromeOnly]
DOMString generateXPath();
+ [Throws] /*Make this ChromeOnly!*/
+ Promise<void> localize(L10nCallback l10nCallback);
+
#ifdef ACCESSIBILITY
[Pref="accessibility.AOM.enabled"]
readonly attribute AccessibleNode? accessibleNode;
#endif
};
dictionary GetRootNodeOptions {
boolean composed = false;
--- a/intl/l10n/DOMLocalization.jsm
+++ b/intl/l10n/DOMLocalization.jsm
@@ -509,17 +509,35 @@ class DOMLocalization extends Localizati
* with information about which translations to use.
*
* Returns a `Promise` that gets resolved once the translation is complete.
*
* @param {DOMFragment} frag - Element or DocumentFragment to be translated
* @returns {Promise}
*/
translateFragment(frag) {
- return this.translateElements(this.getTranslatables(frag));
+ return frag.ownerDocument.localize(l10nItems => {
+ let keys = l10nItems.map(l10nItem => [l10nItem.l10nId, l10nItem.l10nArgs]);
+ return this.formatEntities(keys).then(translations => {
+ return translations.map(translation => {
+ let attrs = [];
+ if (translation.attrs !== null) {
+ attrs = translation.attrs.map(([name, value]) => {
+ return {name, value};
+ });
+ }
+ return {
+ content: translation.value,
+ attributes: attrs
+ };
+ });
+ });
+
+ });
+ // return this.translateElements(this.getTranslatables(frag));
}
/**
* Translate a list of DOM elements asynchronously using this
* `DOMLocalization` object.
*
* Manually trigger the translation (or re-translation) of a list of elements.
* Use the `data-l10n-id` and `data-l10n-args` attributes to mark up the DOM