--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -204,16 +204,17 @@
#include "nsWrapperCacheInlines.h"
#include "nsSandboxFlags.h"
#include "mozilla/dom/AnimatableBinding.h"
#include "mozilla/dom/AnonymousContent.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/ClientInfo.h"
#include "mozilla/dom/ClientState.h"
#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/DocumentL10n.h"
#include "mozilla/dom/DocumentTimeline.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/ImageTracker.h"
#include "mozilla/dom/MediaQueryList.h"
#include "mozilla/dom/NodeFilterBinding.h"
#include "mozilla/OwningNonNull.h"
@@ -1369,16 +1370,20 @@ nsIDocument::nsIDocument()
mCharacterSet(WINDOWS_1252_ENCODING),
mCharacterSetSource(0),
mParentDocument(nullptr),
mCachedRootElement(nullptr),
mNodeInfoManager(nullptr),
#ifdef DEBUG
mStyledLinksCleared(false),
#endif
+ mDocumentL10n(nullptr),
+ mL10nResourceContainerParsed(false),
+ mDOMParsed(false),
+ mPrincipalAllowsL10n(false),
mBidiEnabled(false),
mMathMLEnabled(false),
mIsInitialDocumentInWindow(false),
mIgnoreDocGroupMismatches(false),
mLoadedAsData(false),
mLoadedAsInteractiveData(false),
mMayStartLayout(true),
mHaveFiredTitleChange(false),
@@ -1857,16 +1862,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
}
// Traverse all nsIDocument pointer members.
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityInfo)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDisplayDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyForIdle)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAboutCapabilities)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n)
// Traverse all nsDocument nsCOMPtrs.
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParser)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptGlobalObject)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStyleSheets)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheetSetList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mScriptLoader)
@@ -2012,16 +2018,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
NS_IMPL_CYCLE_COLLECTION_UNLINK(mForms);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mScripts);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mApplets);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchors);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOrientationPendingPromise)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAboutCapabilities)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n)
tmp->mParentDocument = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadingImages)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntersectionObservers)
tmp->ClearAllBoxObjects();
@@ -2210,16 +2217,44 @@ nsIDocument::Reset(nsIChannel* aChannel,
mDocumentBaseURI = baseURI;
mChromeXHRDocBaseURI = nullptr;
}
}
mChannel = aChannel;
}
+bool
+PrincipalAllowsL10n(nsIPrincipal* principal) {
+ // Fast track privileged contexts
+ if (nsContentUtils::IsSystemPrincipal(principal)) {
+ return true;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = principal->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv) || !uri) {
+ return false;
+ }
+
+ bool isAbout;
+ rv = uri->SchemeIs("about", &isAbout);
+ if (NS_FAILED(rv) || (!isAbout)) {
+ return false;
+ }
+
+ bool isNonWeb;
+ rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_DANGEROUS_TO_LOAD, &isNonWeb);
+ if (NS_FAILED(rv) || !isNonWeb) {
+ return false;
+ }
+
+ return true;
+}
+
void
nsIDocument::ResetToURI(nsIURI* aURI,
nsILoadGroup* aLoadGroup,
nsIPrincipal* aPrincipal)
{
MOZ_ASSERT(aURI, "Null URI passed to ResetToURI");
MOZ_LOG(gDocumentLeakPRLog, LogLevel::Debug,
@@ -3156,16 +3191,22 @@ nsIDocument::SetPrincipal(nsIPrincipal *
bool isHTTPS;
if (!uri || NS_FAILED(uri->SchemeIs("https", &isHTTPS)) ||
isHTTPS) {
mAllowDNSPrefetch = false;
}
}
mNodeInfoManager->SetDocumentPrincipal(aNewPrincipal);
+ if (aNewPrincipal && PrincipalAllowsL10n(aNewPrincipal)) {
+ mPrincipalAllowsL10n = true;
+ } else {
+ mPrincipalAllowsL10n = false;
+ }
+
#ifdef DEBUG
// Validate that the docgroup is set correctly by calling its getter and
// triggering its sanity check.
//
// If we're setting the principal to null, we don't want to perform the check,
// as the document is entering an intermediate state where it does not have a
// principal. It will be given another real principal shortly which we will
// check. It's not unsafe to have a document which has a null principal in the
@@ -3350,16 +3391,121 @@ nsIDocument::GetAboutCapabilities(ErrorR
mAboutCapabilities = new AboutCapabilities(jsImplObj, sgo);
}
RefPtr<AboutCapabilities> aboutCapabilities =
static_cast<AboutCapabilities*>(mAboutCapabilities.get());
return aboutCapabilities.forget();
}
bool
+nsDocument::DocumentSupportsL10n(JSContext* aCx, JSObject* aObject)
+{
+ return PrincipalAllowsL10n(nsContentUtils::SubjectPrincipal(aCx));
+}
+
+void
+nsIDocument::LocalizationLinkAdded(Element* aLinkElement)
+{
+ auto l10n = GetLocalization(true);
+ if (!l10n) {
+ return;
+ }
+
+ Element* parent = aLinkElement->GetParentElement();
+ if (!parent) {
+ return;
+ }
+
+ Element* head = GetHeadElement();
+ if (parent != head && !parent->NodeInfo()->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)) {
+ // TODO log a warning
+ return;
+ }
+
+ nsString href;
+ aLinkElement->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
+ (*l10n)->AddResourceId(href);
+}
+
+void
+nsIDocument::LocalizationLinkRemoved(Element* aLinkElement)
+{
+ auto l10n = GetLocalization(true);
+ if (!l10n) {
+ return;
+ }
+
+ Element* parent = aLinkElement->GetParentElement();
+ if (!parent) {
+ return;
+ }
+
+ Element* head = GetHeadElement();
+ if (parent != head && !parent->NodeInfo()->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)) {
+ // TODO log a warning
+ return;
+ }
+
+ nsString href;
+ aLinkElement->GetAttr(kNameSpaceID_None, nsGkAtoms::href, href);
+ (*l10n)->RemoveResourceId(href);
+}
+
+mozilla::Maybe<RefPtr<DocumentL10n>>
+nsIDocument::GetLocalization(bool initialize)
+{
+ if (mDocumentL10n) {
+ return Some(mDocumentL10n);
+ } else if (initialize && mPrincipalAllowsL10n) {
+ mDocumentL10n = new DocumentL10n(this, mL10nResourceContainerParsed, mDOMParsed);
+ return Some(mDocumentL10n);
+ }
+ return Nothing();
+}
+
+void
+nsIDocument::OnL10nResourceContainerParsed()
+{
+ if (mL10nResourceContainerParsed) {
+ return;
+ }
+ auto l10n = GetLocalization(false);
+ if (l10n) {
+ (*l10n)->OnL10nResourceContainerParsed();
+ }
+ mL10nResourceContainerParsed = true;
+}
+
+void
+nsIDocument::OnDOMParsed()
+{
+ if (mDOMParsed) {
+ return;
+ }
+ auto l10n = GetLocalization(false);
+ if (l10n) {
+ (*l10n)->OnDOMParsed();
+ }
+ mDOMParsed = true;
+}
+
+already_AddRefed<DocumentL10n>
+nsIDocument::GetL10n(ErrorResult& aRv)
+{
+ auto l10nOrig = GetLocalization(true);
+ if (l10nOrig.isNothing()) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<DocumentL10n> l10n(l10nOrig.value());
+ return l10n.forget();
+}
+
+bool
nsDocument::IsElementAnimateEnabled(JSContext* aCx, JSObject* /*unused*/)
{
MOZ_ASSERT(NS_IsMainThread());
return nsContentUtils::IsSystemCaller(aCx) ||
nsContentUtils::AnimationsAPICoreEnabled() ||
nsContentUtils::AnimationsAPIElementAnimateEnabled();
}
@@ -5103,16 +5249,19 @@ nsIDocument::DispatchContentLoadedEvents
// Unpin references to preloaded images
mPreloadingImages.Clear();
// DOM manipulation after content loaded should not care if the element
// came from the preloader.
mPreloadedPreconnects.Clear();
+ // DOM has been parsed by now, we can start localization
+ OnDOMParsed();
+
if (mTiming) {
mTiming->NotifyDOMContentLoadedStart(nsIDocument::GetDocumentURI());
}
// Dispatch observer notification to notify observers document is interactive.
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
nsIPrincipal* principal = NodePrincipal();
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -156,16 +156,17 @@ public:
nsISupports* aContainer,
nsIStreamListener **aDocListener,
bool aReset = true,
nsIContentSink* aContentSink = nullptr) override = 0;
virtual void StopDocumentLoad() override;
static bool CallerIsTrustedAboutPage(JSContext* aCx, JSObject* aObject);
+ static bool DocumentSupportsL10n(JSContext* aCx, JSObject* aObject);
static bool IsElementAnimateEnabled(JSContext* aCx, JSObject* aObject);
static bool IsWebAnimationsEnabled(JSContext* aCx, JSObject* aObject);
static bool IsWebAnimationsEnabled(mozilla::dom::CallerType aCallerType);
virtual void EndUpdate() override;
virtual void BeginLoad() override;
virtual void EndLoad() override;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -143,16 +143,17 @@ class Attr;
class BoxObject;
class ClientInfo;
class ClientState;
class CDATASection;
class Comment;
struct CustomElementDefinition;
class DocGroup;
class DocumentFragment;
+class DocumentL10n;
class DocumentTimeline;
class DocumentType;
class DOMImplementation;
class DOMIntersectionObserver;
class DOMStringList;
class Element;
struct ElementCreationOptions;
class Event;
@@ -3134,16 +3135,22 @@ public:
void GetInputEncoding(nsAString& aInputEncoding) const;
already_AddRefed<mozilla::dom::Location> GetLocation() const;
void GetReferrer(nsAString& aReferrer) const;
void GetLastModified(nsAString& aLastModified) const;
void GetReadyState(nsAString& aReadyState) const;
already_AddRefed<mozilla::dom::AboutCapabilities> GetAboutCapabilities(
ErrorResult& aRv);
+ void LocalizationLinkAdded(Element* aLinkElement);
+ void LocalizationLinkRemoved(Element* aLinkElement);
+ mozilla::Maybe<RefPtr<mozilla::dom::DocumentL10n>> GetLocalization(bool initialize);
+ void OnL10nResourceContainerParsed();
+ void OnDOMParsed();
+ already_AddRefed<mozilla::dom::DocumentL10n> GetL10n(ErrorResult& aRv);
void GetTitle(nsAString& aTitle);
void SetTitle(const nsAString& aTitle, mozilla::ErrorResult& rv);
void GetDir(nsAString& aDirection) const;
void SetDir(const nsAString& aDirection);
nsIHTMLCollection* Images();
nsIHTMLCollection* Embeds();
nsIHTMLCollection* Plugins()
@@ -3808,16 +3815,20 @@ protected:
// focus has never occurred then mLastFocusTime.IsNull() will be true.
mozilla::TimeStamp mLastFocusTime;
mozilla::EventStates mDocumentState;
RefPtr<mozilla::dom::Promise> mReadyForIdle;
RefPtr<mozilla::dom::AboutCapabilities> mAboutCapabilities;
+ RefPtr<mozilla::dom::DocumentL10n> mDocumentL10n;
+ bool mL10nResourceContainerParsed : 1;
+ bool mDOMParsed : 1;
+ bool mPrincipalAllowsL10n : 1;
// True if BIDI is enabled.
bool mBidiEnabled : 1;
// True if a MathML element has ever been owned by this document.
bool mMathMLEnabled : 1;
// True if this document is the initial document for a window. This should
// basically be true only for documents that exist in newly-opened windows or
--- a/dom/html/HTMLBodyElement.cpp
+++ b/dom/html/HTMLBodyElement.cpp
@@ -294,16 +294,21 @@ nsresult
HTMLBodyElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
nsIContent* aBindingParent,
bool aCompileEventHandlers)
{
nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
aBindingParent,
aCompileEventHandlers);
NS_ENSURE_SUCCESS(rv, rv);
+
+ // We now have a body, if l10n resources were to be loaded, it would happen
+ // already.
+ OwnerDoc()->OnL10nResourceContainerParsed();
+
return mAttrsAndChildren.ForceMapped(this, OwnerDoc());
}
nsresult
HTMLBodyElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aValue,
const nsAttrValue* aOldValue,
nsIPrincipal* aSubjectPrincipal,
--- a/dom/html/HTMLLinkElement.cpp
+++ b/dom/html/HTMLLinkElement.cpp
@@ -144,16 +144,23 @@ HTMLLinkElement::BindToTree(nsIDocument*
if (IsInComposedDoc()) {
TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender();
}
void (HTMLLinkElement::*update)() = &HTMLLinkElement::UpdateStyleSheetInternal;
nsContentUtils::AddScriptRunner(
NewRunnableMethod("dom::HTMLLinkElement::BindToTree", this, update));
+ if (aDocument) {
+ nsAutoString rel;
+ GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel);
+ if (rel.EqualsLiteral("localization"))
+ aDocument->LocalizationLinkAdded(this);
+ }
+
CreateAndDispatchEvent(aDocument, NS_LITERAL_STRING("DOMLinkAdded"));
return rv;
}
void
HTMLLinkElement::LinkAdded()
{
@@ -184,16 +191,23 @@ HTMLLinkElement::UnbindFromTree(bool aDe
// from the parser.
nsCOMPtr<nsIDocument> oldDoc = GetUncomposedDoc();
// Check for a ShadowRoot because link elements are inert in a
// ShadowRoot.
ShadowRoot* oldShadowRoot = GetBindingParent() ?
GetBindingParent()->GetShadowRoot() : nullptr;
+ if (oldDoc) {
+ nsAutoString rel;
+ GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel);
+ if (rel.EqualsLiteral("localization"))
+ oldDoc->LocalizationLinkRemoved(this);
+ }
+
CreateAndDispatchEvent(oldDoc, NS_LITERAL_STRING("DOMLinkRemoved"));
nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
Unused << UpdateStyleSheetInternal(oldDoc, oldShadowRoot);
}
bool
HTMLLinkElement::ParseAttribute(int32_t aNamespaceID,
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -497,16 +497,20 @@ partial interface Document {
readonly attribute FlashClassification documentFlashClassification;
};
// Allows about: pages to query aboutCapabilities
partial interface Document {
[Throws, Func="nsDocument::CallerIsTrustedAboutPage"] readonly attribute AboutCapabilities aboutCapabilities;
};
+partial interface Document {
+ [Throws, Func="nsDocument::DocumentSupportsL10n"] readonly attribute DocumentL10n l10n;
+};
+
Document implements XPathEvaluator;
Document implements GlobalEventHandlers;
Document implements DocumentAndElementEventHandlers;
Document implements TouchEventHandlers;
Document implements ParentNode;
Document implements OnErrorEventHandlerForNodes;
Document implements GeometryUtils;
Document implements FontFaceSource;
new file mode 100644
--- /dev/null
+++ b/dom/webidl/DocumentL10n.webidl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+dictionary L10nKey {
+ required DOMString id;
+
+ object? args = null;
+};
+
+[NoInterfaceObject]
+interface DocumentL10n {
+ [Throws] void setAttributes(Element aElement, DOMString aId, optional object aArgs);
+
+ [Throws] Promise<DOMString> formatValue(DOMString aId, optional object aArgs);
+
+ [Throws] Promise<sequence<DOMString>> formatValues(sequence<L10nKey> aKeys);
+
+ [Throws] readonly attribute Promise<any> ready;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -455,16 +455,17 @@ WEBIDL_FILES = [
'DataTransferItemList.webidl',
'DecoderDoctorNotification.webidl',
'DedicatedWorkerGlobalScope.webidl',
'DelayNode.webidl',
'DeviceMotionEvent.webidl',
'Directory.webidl',
'Document.webidl',
'DocumentFragment.webidl',
+ 'DocumentL10n.webidl',
'DocumentOrShadowRoot.webidl',
'DocumentTimeline.webidl',
'DocumentType.webidl',
'DOMError.webidl',
'DOMException.webidl',
'DOMImplementation.webidl',
'DOMMatrix.webidl',
'DOMParser.webidl',
--- a/dom/xul/XULDocument.cpp
+++ b/dom/xul/XULDocument.cpp
@@ -74,16 +74,17 @@
#include "nsIObserverService.h"
#include "nsNodeUtils.h"
#include "nsIDocShellTreeOwner.h"
#include "nsIXULWindow.h"
#include "nsXULPopupManager.h"
#include "nsCCUncollectableMarker.h"
#include "nsURILoader.h"
#include "mozilla/BasicEvents.h"
+#include "mozilla/dom/DocumentL10n.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/NodeInfoInlines.h"
#include "mozilla/dom/ProcessingInstruction.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/XULDocumentBinding.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/Preferences.h"
@@ -1405,16 +1406,19 @@ XULDocument::AddElementToDocumentPost(El
if (aElement == GetRootElement()) {
ResetDocumentDirection();
}
// We need to pay special attention to the keyset tag to set up a listener
if (aElement->NodeInfo()->Equals(nsGkAtoms::keyset, kNameSpaceID_XUL)) {
// Create our XUL key listener and hook it up.
nsXBLService::AttachGlobalKeyHandler(aElement);
+ } else if (aElement->NodeInfo()->Equals(nsGkAtoms::linkset, kNameSpaceID_XUL)) {
+ // If l10n resources were to be loaded, it would happen already.
+ OnL10nResourceContainerParsed();
}
return NS_OK;
}
nsresult
XULDocument::AddSubtreeToDocument(nsIContent* aContent)
{
@@ -2707,16 +2711,22 @@ XULDocument::DoneWalking()
// the |if (!mDocumentLoaded)| check above and since
// mInitialLayoutComplete will be false will follow the else branch
// there too. See the big comment there for how such reentry can
// happen.
mDocumentLoaded = true;
NotifyPossibleTitleChange(false);
+
+ // We parsed the whole document.
+ // If l10n resources were to be loaded, it would happen already.
+ OnL10nResourceContainerParsed();
+ OnDOMParsed();
+
nsContentUtils::DispatchTrustedEvent(
this,
static_cast<nsIDocument*>(this),
NS_LITERAL_STRING("MozBeforeInitialXULLayout"),
true,
false);
// Before starting layout, check whether we're a toplevel chrome
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.cpp
@@ -0,0 +1,218 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/DocumentL10n.h"
+#include "mozilla/dom/DocumentL10nBinding.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/Promise.h"
+#include "nsISupports.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DocumentL10n, mDocument, mDocumentLocalization)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DocumentL10n)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DocumentL10n)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/**
+ * This can get lazily initialized at multiple points during the lifecycle
+ * of the document. It can get initialized when the first link is registered,
+ * or while document is still being parsed, or much later.
+ *
+ * For that reason we initialize the `DocumentLocalization` with information
+ * about those two states, and if either of them is `false` at the time
+ * of initialization, the corresponding method is expected to be fired
+ * by the parsed later.
+ */
+DocumentL10n::DocumentL10n(nsIDocument* aDocument, bool aL10nResourceContainerParsed, bool aDOMParsed)
+ : mDocument(aDocument)
+ , mDocumentLocalization(nullptr)
+ , mL10nResourceContainerParsed(aL10nResourceContainerParsed)
+ , mDOMParsed(aDOMParsed)
+{
+ printf("DocumentL10n::DocumentL10n\n");
+ if (aL10nResourceContainerParsed) {
+ printf("DocumentL10n::DocumentL10n L10nResourceContainerParsed\n");
+ }
+ if (aDOMParsed) {
+ printf("DocumentL10n::DocumentL10n DOMParsed\n");
+ }
+ nsresult rv;
+ nsCOMPtr<mozIDocumentLocalization> docL10n = do_CreateInstance("@mozilla.org/intl/documentlocalization;1", &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ mDocumentLocalization = docL10n;
+ mDocumentLocalization->Init(mDocument, mL10nResourceContainerParsed, mDOMParsed);
+}
+
+DocumentL10n::~DocumentL10n()
+{
+ mDocumentLocalization = nullptr;
+}
+
+void
+DocumentL10n::OnDOMParsed()
+{
+ printf("DocumentL10n::OnDOMParsed\n");
+ if (!mDOMParsed) {
+ if (nsContentUtils::IsSafeToRunScript()) {
+ mDocumentLocalization->OnDOMParsed();
+ } else {
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("mozIDocumentLocalization::OnDOMParsed",
+ mDocumentLocalization,
+ &mozIDocumentLocalization::OnDOMParsed));
+ }
+ mDOMParsed = true;
+ }
+}
+
+void
+DocumentL10n::OnL10nResourceContainerParsed()
+{
+ printf("DocumentL10n::OnL10nResourceContainerParsed\n");
+ if (!mL10nResourceContainerParsed) {
+ if (nsContentUtils::IsSafeToRunScript()) {
+ mDocumentLocalization->OnL10nResourceContainerParsed();
+ } else {
+ nsContentUtils::AddScriptRunner(
+ NewRunnableMethod("mozIDocumentLocalization::OnL10nResourceContainerParsed",
+ mDocumentLocalization,
+ &mozIDocumentLocalization::OnL10nResourceContainerParsed));
+ }
+ mL10nResourceContainerParsed = true;
+ }
+}
+
+JSObject*
+DocumentL10n::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return DocumentL10nBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+DocumentL10n::AddResourceId(const nsAString& aResourceId)
+{
+ //XXX: If I uncomment it, the Preferences pane doesn't load resources, seems
+ // like the NewRunnableMethod doesn't work :(
+ printf("DocumentL10n::AddResourceId\n");
+ /* if (nsContentUtils::IsSafeToRunScript()) { */
+ printf("DocumentL10n::AddResourceId IsSafeToRunScript\n");
+ mDocumentLocalization->AddResourceId(aResourceId);
+ /* } else { */
+ /* printf("DocumentL10n::AddResourceId IsNotSafeToRunScript\n"); */
+ /* nsContentUtils::AddScriptRunner( */
+ /* NewRunnableMethod<const nsAString&>("mozIDocumentLocalization::AddResourceId", */
+ /* mDocumentLocalization, */
+ /* &mozIDocumentLocalization::AddResourceId, */
+ /* aResourceId)); */
+ /* } */
+}
+
+void
+DocumentL10n::RemoveResourceId(const nsAString& aResourceId)
+{
+ /* if (nsContentUtils::IsSafeToRunScript()) { */
+ mDocumentLocalization->RemoveResourceId(aResourceId);
+ /* } else { */
+ /* nsContentUtils::AddScriptRunner( */
+ /* NewRunnableMethod<const nsAString&>("mozIDocumentLocalization::RemoveResourceId", */
+ /* mDocumentLocalization, */
+ /* &mozIDocumentLocalization::RemoveResourceId, */
+ /* aResourceId)); */
+ /* } */
+}
+
+void
+DocumentL10n::SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+ JS::RootedValue args(cx);
+
+ if (aArgs.WasPassed()) {
+ args = JS::ObjectValue(*aArgs.Value());
+ } else {
+ args = JS::UndefinedValue();
+ }
+
+ nsresult rv = mDocumentLocalization->SetAttributes(&aElement, aId, args);
+
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ }
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv)
+{
+ JS::RootedValue args(cx);
+
+ if (aArgs.WasPassed()) {
+ args = JS::ObjectValue(*aArgs.Value());
+ } else {
+ args = JS::UndefinedValue();
+ }
+
+ RefPtr<Promise> promise;
+ nsresult rv = mDocumentLocalization->FormatValue(aId, args, getter_AddRefs(promise));
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv)
+{
+ nsTArray<JS::HandleValue> jsKeys;
+ jsKeys.SetCapacity(aKeys.Length());
+
+ for (auto& key : aKeys) {
+ JS::RootedValue jsKey(cx);
+ if (!ToJSValue(cx, key, &jsKey)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ jsKeys.AppendElement(jsKey);
+ }
+
+ RefPtr<Promise> promise;
+ nsresult rv = mDocumentLocalization->FormatValues(jsKeys.Elements(), jsKeys.Length(), getter_AddRefs(promise));
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise>
+DocumentL10n::GetReady(ErrorResult& aRv)
+{
+ RefPtr<Promise> promise;
+
+ nsresult rv = mDocumentLocalization->GetReady(getter_AddRefs(promise));
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/intl/l10n/DocumentL10n.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_DocumentL10n_h
+#define mozilla_dom_DocumentL10n_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsIDocument.h"
+#include "mozIDocumentLocalization.h"
+
+namespace mozilla {
+namespace dom {
+
+class Element;
+class Promise;
+struct L10nKey;
+
+class DocumentL10n final : public nsISupports,
+ public nsWrapperCache
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DocumentL10n)
+
+public:
+ explicit DocumentL10n(nsIDocument* aDocument, bool aL10nResourceContainerParsed, bool aDOMParsed);
+
+protected:
+ virtual ~DocumentL10n();
+
+ nsCOMPtr<nsIDocument> mDocument;
+ nsCOMPtr<mozIDocumentLocalization> mDocumentLocalization;
+ bool mL10nResourceContainerParsed;
+ bool mDOMParsed;
+
+public:
+ void OnDOMParsed();
+ void OnL10nResourceContainerParsed();
+
+ nsIDocument* GetParentObject() const { return mDocument; };
+
+ virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ void AddResourceId(const nsAString& aResourceId);
+
+ void RemoveResourceId(const nsAString& aResourceId);
+
+ void SetAttributes(JSContext* cx, Element& aElement, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+
+ already_AddRefed<Promise> FormatValue(JSContext* cx, const nsAString& aId, const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
+
+ already_AddRefed<Promise> FormatValues(JSContext* cx, const Sequence<L10nKey>& aKeys, ErrorResult& aRv);
+ already_AddRefed<Promise> GetReady(ErrorResult& aRv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_DocumentL10n_h
--- a/intl/l10n/moz.build
+++ b/intl/l10n/moz.build
@@ -17,16 +17,28 @@ XPIDL_SOURCES += [
XPIDL_MODULE = 'locale'
EXTRA_COMPONENTS += [
'mozDocumentLocalization.js',
'mozDocumentLocalization.manifest',
]
+EXPORTS.mozilla.dom += [
+ 'DocumentL10n.h',
+]
+
+UNIFIED_SOURCES += [
+ 'DocumentL10n.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '/dom/base',
+]
+
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
JAR_MANIFESTS += ['jar.mn']
SPHINX_TREES['l10n'] = 'docs'
--- a/intl/l10n/test/chrome.ini
+++ b/intl/l10n/test/chrome.ini
@@ -9,8 +9,18 @@
[dom/test_domloc_translateRoots.html]
[dom/test_domloc_mutations.html]
[dom/test_domloc_overlay.html]
[dom/test_domloc_overlay_repeated.html]
[dom/test_domloc_overlay_missing_all.html]
[dom/test_domloc_overlay_missing_children.html]
[dom/test_domloc_overlay_sanitized.html]
[dom/test_domloc.xul]
+
+[document_l10n/xul/test_api_before_links.xul]
+[document_l10n/xul/test_dom_loc.xul]
+[document_l10n/xul/test_lazy_resource_injection.xul]
+[document_l10n/xul/test_lazy_webcomponent.xul]
+[document_l10n/xul/test_no_loc.xul]
+[document_l10n/xul/test_no_loc_api_calls.xul]
+
+[document_l10n/html/test_dom_loc.html]
+[document_l10n/html/test_no_loc.html]
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/html/test_dom_loc.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <title>Testing that the DOM is localized at firstPaint</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /></script>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ document.l10n.ready.then(() => {
+ // Test that it's before first paint
+ const label = document.getElementById("label");
+ isnot(label.textContent, "");
+ SimpleTest.finish();
+ });
+ </script>
+ <link rel="localization" href="browser/preferences/preferences.ftl"/>
+ </head>
+ <body>
+ <label id="label" data-l10n-id="pane-general-title"/>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/html/test_no_loc.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <title>
+ Testing that document.l10n methods resolve even when there are no resources
+ </title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /></script>
+ <script type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ async function test() {
+ let value = await document.l10n.formatValue("pane-general-title");
+ is(value, undefined);
+ SimpleTest.finish();
+ }
+ test();
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_api_before_links.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testing that document.l10n methods resolve once links are loaded">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ async function test() {
+ let value = await document.l10n.formatValue("pane-general-title");
+ isnot(value, undefined);
+ SimpleTest.finish();
+ }
+ test();
+ ]]>
+ </script>
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
+ </linkset>
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_dom_loc.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testing that the DOM is localized at firstPaint">
+ <label id="label-pre" data-l10n-id="pane-home-title"/>
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ document.l10n.ready.then(() => {
+ // Test that it's before first paint
+ const label1 = document.getElementById("label-pre");
+ const label2 = document.getElementById("label-post");
+ isnot(label1.textContent, "");
+ isnot(label2.textContent, "");
+ SimpleTest.finish();
+ });
+ ]]>
+ </script>
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
+ </linkset>
+ <label id="label-post" data-l10n-id="pane-general-title"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_lazy_resource_injection.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testing that a resource injected late becomes available">
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function injectResource() {
+ let link = document.createElementNS("http://www.w3.org/1999/xhtml", "link");
+ link.setAttribute("rel", "localization");
+ link.setAttribute("href", "browser/preferences/colors.ftl");
+ let linkset = document.querySelector("linkset");
+ linkset.appendChild(link);
+ }
+
+ function removeResource() {
+ let link = document.querySelector("link[href='browser/preferences/colors.ftl']");
+ link.parentNode.removeChild(link);
+ }
+
+ document.l10n.ready.then(async () => {
+ // Test that it's before first paint
+ const label1 = document.getElementById("label");
+ isnot(label1.textContent, "");
+
+ injectResource();
+
+ let value = await document.l10n.formatValue("colors-links-header");
+ isnot(value, undefined);
+
+ removeResource();
+
+ let value2 = await document.l10n.formatValue("colors-links-header");
+ is(value2, undefined);
+
+
+ SimpleTest.finish();
+ });
+ ]]>
+ </script>
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
+ </linkset>
+ <label id="label" data-l10n-id="pane-general-title"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_lazy_webcomponent.xul
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testing that a lazily injected web component can be localized">
+ <!-- <linkset> -->
+ <!-- <html:link rel="localization" href="browser/preferences/translation.ftl" /> -->
+ <!-- </linkset> -->
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ function waitForMutation(target, opts, cb) {
+ return new Promise((resolve) => {
+ let observer = new MutationObserver(() => {
+ if (!cb || cb(target)) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(target, opts);
+ });
+ }
+
+ function waitForMessageChange(element, cb, opts = { childList: true }) {
+ return waitForMutation(element, opts, cb);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ function appendL10nResource(id) {
+ let linkset = document.querySelector("linkset");
+ if (!linkset) {
+ linkset = document.createElement("linkset");
+ document.documentElement.appendChild(linkset);
+ }
+ let link = document.createElementNS("http://www.w3.org/1999/xhtml", "link");
+ link.setAttribute("rel", "localization");
+ link.setAttribute("href", id);
+ linkset.appendChild(link);
+ }
+
+ function registerCE() {
+ customElements.define("foo-bar", class MyElement extends XULElement {
+ connectedCallback() {
+ this.appendChild(MozXULElement.parseXULToFragment(
+ `<label data-l10n-id='translation-sites-disabled-desc' />`));
+ }
+ });
+ }
+
+ function injectCE() {
+ document.documentElement.append(document.createElement("foo-bar"));
+ }
+
+ addLoadEvent(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ appendL10nResource("browser/preferences/translation.ftl");
+
+ let expectedText = await document.l10n.formatValue("translation-sites-disabled-desc");
+ isnot(expectedText, undefined);
+
+ registerCE();
+ injectCE();
+
+ let elem = document.querySelector("label");
+ await waitForMessageChange(elem);
+ is(elem.textContent, expectedText);
+
+ SimpleTest.finish();
+ });
+ ]]>
+ </script>
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_no_loc.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Testing that document.l10n methods resolve even when there are no resources">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ async function test() {
+ let value = await document.l10n.formatValue("pane-general-title");
+ is(value, undefined);
+ SimpleTest.finish();
+ }
+ test();
+ ]]>
+ </script>
+</window>
new file mode 100644
--- /dev/null
+++ b/intl/l10n/test/document_l10n/xul/test_no_loc_api_calls.xul
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Testing that API resolve if called late with no linkset">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ window.onload = async () => {
+ await document.l10n.ready;
+ is(1, 1);
+ SimpleTest.finish();
+ };
+ ]]>
+ </script>
+</window>
--- a/xpcom/ds/nsGkAtomList.h
+++ b/xpcom/ds/nsGkAtomList.h
@@ -543,16 +543,17 @@ GK_ATOM(left, "left")
GK_ATOM(leftmargin, "leftmargin")
GK_ATOM(legend, "legend")
GK_ATOM(length, "length")
GK_ATOM(letterValue, "letter-value")
GK_ATOM(level, "level")
GK_ATOM(li, "li")
GK_ATOM(line, "line")
GK_ATOM(link, "link")
+GK_ATOM(linkset, "linkset")
//GK_ATOM(list, "list") # "list" is present below
GK_ATOM(listbox, "listbox")
GK_ATOM(listboxbody, "listboxbody")
GK_ATOM(listcell, "listcell")
GK_ATOM(listcol, "listcol")
GK_ATOM(listcols, "listcols")
GK_ATOM(listener, "listener")
GK_ATOM(listhead, "listhead")