From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/l10n/DOMLocalization.cpp | 672 +++++++++++++++++++++ dom/l10n/DOMLocalization.h | 137 +++++ dom/l10n/DocumentL10n.cpp | 347 +++++++++++ dom/l10n/DocumentL10n.h | 88 +++ dom/l10n/L10nMutations.cpp | 362 +++++++++++ dom/l10n/L10nMutations.h | 101 ++++ dom/l10n/L10nOverlays.cpp | 557 +++++++++++++++++ dom/l10n/L10nOverlays.h | 111 ++++ dom/l10n/components.conf | 14 + dom/l10n/moz.build | 35 ++ dom/l10n/tests/gtest/TestL10nOverlays.cpp | 77 +++ dom/l10n/tests/gtest/moz.build | 11 + dom/l10n/tests/mochitest/browser.ini | 6 + dom/l10n/tests/mochitest/chrome.ini | 47 ++ dom/l10n/tests/mochitest/document_l10n/README.txt | 3 + .../document_l10n/non-system-principal/README.txt | 3 + .../non-system-principal/browser_resource_uri.js | 109 ++++ .../non-system-principal/localization/test.ftl | 4 + .../document_l10n/non-system-principal/test.html | 37 ++ .../test_connectRoot_webcomponent.html | 90 +++ .../test_connectRoot_webcomponent_lazy.html | 98 +++ .../mochitest/document_l10n/test_docl10n.html | 66 ++ .../mochitest/document_l10n/test_docl10n.xhtml | 60 ++ .../mochitest/document_l10n/test_docl10n_lazy.html | 44 ++ .../document_l10n/test_docl10n_ready_rejected.html | 29 + .../test_docl10n_removeResourceIds.html | 59 ++ .../mochitest/document_l10n/test_docl10n_sync.html | 54 ++ .../mochitest/document_l10n/test_telemetry.html | 83 +++ .../document_l10n/test_unpriv_iframe.html | 26 + .../dom_localization/test_attr_sanitized.html | 49 ++ .../dom_localization/test_connectRoot.html | 45 ++ .../test_connectRoot_webcomponent.html | 72 +++ .../dom_localization/test_disconnectRoot.html | 60 ++ .../mochitest/dom_localization/test_domloc.xhtml | 68 +++ .../dom_localization/test_getAttributes.html | 49 ++ .../dom_localization/test_l10n_mutations.html | 57 ++ .../mochitest/dom_localization/test_overlay.html | 60 ++ .../dom_localization/test_overlay_missing_all.html | 37 ++ .../test_overlay_missing_children.html | 53 ++ .../dom_localization/test_overlay_repeated.html | 50 ++ .../dom_localization/test_overlay_sanitized.html | 52 ++ .../dom_localization/test_repeated_l10nid.html | 60 ++ .../dom_localization/test_setAttributes.html | 79 +++ .../dom_localization/test_translateElements.html | 47 ++ .../dom_localization/test_translateFragment.html | 48 ++ .../dom_localization/test_translateRoots.html | 56 ++ .../test_append_content_post_dcl.html | 30 + .../test_append_content_pre_dcl.html | 28 + .../test_append_fragment_post_dcl.html | 39 ++ .../test_disconnectedRoot_webcomponent.html | 148 +++++ .../l10n_mutations/test_pause_observing.html | 44 ++ .../l10n_mutations/test_remove_element.html | 68 +++ .../l10n_mutations/test_remove_fragment.html | 67 ++ .../l10n_mutations/test_set_attributes.html | 37 ++ .../mochitest/l10n_mutations/test_template.html | 37 ++ .../mochitest/l10n_overlays/test_attributes.html | 86 +++ .../l10n_overlays/test_extra_text_markup.html | 136 +++++ .../l10n_overlays/test_functional_children.html | 344 +++++++++++ .../l10n_overlays/test_l10n_overlays.xhtml | 87 +++ .../mochitest/l10n_overlays/test_same_id.html | 57 ++ .../mochitest/l10n_overlays/test_same_id_args.html | 57 ++ .../l10n_overlays/test_text_children.html | 74 +++ .../tests/mochitest/l10n_overlays/test_title.html | 60 ++ dom/l10n/tests/mochitest/mochitest.ini | 1 + 64 files changed, 5672 insertions(+) create mode 100644 dom/l10n/DOMLocalization.cpp create mode 100644 dom/l10n/DOMLocalization.h create mode 100644 dom/l10n/DocumentL10n.cpp create mode 100644 dom/l10n/DocumentL10n.h create mode 100644 dom/l10n/L10nMutations.cpp create mode 100644 dom/l10n/L10nMutations.h create mode 100644 dom/l10n/L10nOverlays.cpp create mode 100644 dom/l10n/L10nOverlays.h create mode 100644 dom/l10n/components.conf create mode 100644 dom/l10n/moz.build create mode 100644 dom/l10n/tests/gtest/TestL10nOverlays.cpp create mode 100644 dom/l10n/tests/gtest/moz.build create mode 100644 dom/l10n/tests/mochitest/browser.ini create mode 100644 dom/l10n/tests/mochitest/chrome.ini create mode 100644 dom/l10n/tests/mochitest/document_l10n/README.txt create mode 100644 dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt create mode 100644 dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js create mode 100644 dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl create mode 100644 dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_telemetry.html create mode 100644 dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_overlay.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_translateElements.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html create mode 100644 dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html create mode 100644 dom/l10n/tests/mochitest/l10n_mutations/test_template.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html create mode 100644 dom/l10n/tests/mochitest/l10n_overlays/test_title.html create mode 100644 dom/l10n/tests/mochitest/mochitest.ini (limited to 'dom/l10n') diff --git a/dom/l10n/DOMLocalization.cpp b/dom/l10n/DOMLocalization.cpp new file mode 100644 index 0000000000..456b67ab22 --- /dev/null +++ b/dom/l10n/DOMLocalization.cpp @@ -0,0 +1,672 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "js/ForOfIterator.h" // JS::ForOfIterator +#include "js/JSON.h" // JS_ParseJSON +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "DOMLocalization.h" +#include "mozilla/intl/L10nRegistry.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/L10nOverlays.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::intl; + +NS_IMPL_CYCLE_COLLECTION_CLASS(DOMLocalization) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMLocalization, Localization) + tmp->DisconnectMutations(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMutations) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoots) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMLocalization, Localization) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMutations) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoots) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(DOMLocalization, Localization) +NS_IMPL_RELEASE_INHERITED(DOMLocalization, Localization) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMLocalization) +NS_INTERFACE_MAP_END_INHERITING(Localization) + +DOMLocalization::DOMLocalization(nsIGlobalObject* aGlobal, bool aSync) + : Localization(aGlobal, aSync) { + mMutations = new L10nMutations(this); +} + +DOMLocalization::DOMLocalization(nsIGlobalObject* aGlobal, bool aIsSync, + const ffi::LocalizationRc* aRaw) + : Localization(aGlobal, aIsSync, aRaw) { + mMutations = new L10nMutations(this); +} + +already_AddRefed DOMLocalization::Constructor( + const GlobalObject& aGlobal, + const Sequence& aResourceIds, + bool aIsSync, const Optional>& aRegistry, + const Optional>& aLocales, ErrorResult& aRv) { + auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; + Maybe> locales; + + if (aLocales.WasPassed()) { + locales.emplace(); + locales->SetCapacity(aLocales.Value().Length()); + for (const auto& locale : aLocales.Value()) { + locales->AppendElement(locale); + } + } + + RefPtr raw; + bool result; + + if (aRegistry.WasPassed()) { + result = ffi::localization_new_with_locales( + &ffiResourceIds, aIsSync, aRegistry.Value().Raw(), + locales.ptrOr(nullptr), getter_AddRefs(raw)); + } else { + result = ffi::localization_new_with_locales(&ffiResourceIds, aIsSync, + nullptr, locales.ptrOr(nullptr), + getter_AddRefs(raw)); + } + + if (result) { + nsCOMPtr global = + do_QueryInterface(aGlobal.GetAsSupports()); + + return do_AddRef(new DOMLocalization(global, aIsSync, raw)); + } + aRv.ThrowInvalidStateError( + "Failed to create the Localization. Check the locales arguments."); + return nullptr; +} + +JSObject* DOMLocalization::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return DOMLocalization_Binding::Wrap(aCx, this, aGivenProto); +} + +void DOMLocalization::Destroy() { DisconnectMutations(); } + +DOMLocalization::~DOMLocalization() { Destroy(); } + +bool DOMLocalization::HasPendingMutations() const { + return mMutations && mMutations->HasPendingMutations(); +} + +/** + * DOMLocalization API + */ + +void DOMLocalization::ConnectRoot(nsINode& aNode) { + nsCOMPtr global = aNode.GetOwnerGlobal(); + if (!global) { + return; + } + MOZ_ASSERT(global == mGlobal, + "Cannot add a root that overlaps with existing root."); + +#ifdef DEBUG + for (nsINode* root : mRoots) { + MOZ_ASSERT( + root != &aNode && !root->Contains(&aNode) && !aNode.Contains(root), + "Cannot add a root that overlaps with existing root."); + } +#endif + + mRoots.Insert(&aNode); + + aNode.AddMutationObserverUnlessExists(mMutations); +} + +void DOMLocalization::DisconnectRoot(nsINode& aNode) { + if (mRoots.Contains(&aNode)) { + aNode.RemoveMutationObserver(mMutations); + mRoots.Remove(&aNode); + } +} + +void DOMLocalization::PauseObserving() { mMutations->PauseObserving(); } + +void DOMLocalization::ResumeObserving() { mMutations->ResumeObserving(); } + +void DOMLocalization::SetAttributes( + JSContext* aCx, Element& aElement, const nsAString& aId, + const Optional>& aArgs, ErrorResult& aRv) { + if (aArgs.WasPassed() && aArgs.Value()) { + nsAutoString data; + JS::Rooted val(aCx, JS::ObjectValue(*aArgs.Value())); + if (!nsContentUtils::StringifyJSON(aCx, val, data, + UndefinedIsNullStringLiteral)) { + aRv.NoteJSContextException(aCx); + return; + } + if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nargs, data, + eCaseMatters)) { + aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, data, true); + } + } else { + aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, true); + } + + if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nid, aId, + eCaseMatters)) { + aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, aId, true); + } +} + +void DOMLocalization::GetAttributes(Element& aElement, L10nIdArgs& aResult, + ErrorResult& aRv) { + nsAutoString l10nId; + nsAutoString l10nArgs; + + if (aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nid, l10nId)) { + CopyUTF16toUTF8(l10nId, aResult.mId); + } + + if (aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs)) { + ConvertStringToL10nArgs(l10nArgs, aResult.mArgs.SetValue(), aRv); + } +} + +void DOMLocalization::SetArgs(JSContext* aCx, Element& aElement, + const Optional>& aArgs, + ErrorResult& aRv) { + if (aArgs.WasPassed() && aArgs.Value()) { + nsAutoString data; + JS::Rooted val(aCx, JS::ObjectValue(*aArgs.Value())); + if (!nsContentUtils::StringifyJSON(aCx, val, data, + UndefinedIsNullStringLiteral)) { + aRv.NoteJSContextException(aCx); + return; + } + if (!aElement.AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nargs, data, + eCaseMatters)) { + aElement.SetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, data, true); + } + } else { + aElement.UnsetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, true); + } +} + +already_AddRefed DOMLocalization::TranslateFragment(nsINode& aNode, + ErrorResult& aRv) { + Sequence> elements; + GetTranslatables(aNode, elements, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return TranslateElements(elements, aRv); +} + +/** + * A Promise Handler used to apply the result of + * a call to Localization::FormatMessages onto the list + * of translatable elements. + */ +class ElementTranslationHandler : public PromiseNativeHandler { + public: + explicit ElementTranslationHandler(DOMLocalization* aDOMLocalization, + nsXULPrototypeDocument* aProto) + : mDOMLocalization(aDOMLocalization), mProto(aProto){}; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ElementTranslationHandler) + + nsTArray>& Elements() { return mElements; } + + void SetReturnValuePromise(Promise* aReturnValuePromise) { + mReturnValuePromise = aReturnValuePromise; + } + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + ErrorResult rv; + + nsTArray> 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 temp(aCx); + while (true) { + bool done; + if (!iter.next(&temp, &done)) { + mReturnValuePromise->MaybeRejectWithUndefined(); + return; + } + + if (done) { + break; + } + + Nullable* slotPtr = + l10nData.AppendElement(mozilla::fallible); + if (!slotPtr) { + mReturnValuePromise->MaybeRejectWithUndefined(); + return; + } + + if (!temp.isNull()) { + if (!slotPtr->SetValue().Init(aCx, temp)) { + mReturnValuePromise->MaybeRejectWithUndefined(); + return; + } + } + } + } + + bool allTranslated = + mDOMLocalization->ApplyTranslations(mElements, l10nData, mProto, rv); + if (NS_WARN_IF(rv.Failed()) || !allTranslated) { + mReturnValuePromise->MaybeRejectWithUndefined(); + return; + } + + mReturnValuePromise->MaybeResolveWithUndefined(); + } + + virtual void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + mReturnValuePromise->MaybeRejectWithClone(aCx, aValue); + } + + private: + ~ElementTranslationHandler() = default; + + nsTArray> mElements; + RefPtr mDOMLocalization; + RefPtr mReturnValuePromise; + RefPtr mProto; +}; + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ElementTranslationHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(ElementTranslationHandler) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ElementTranslationHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ElementTranslationHandler) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ElementTranslationHandler) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mElements) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMLocalization) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReturnValuePromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mProto) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ElementTranslationHandler) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElements) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMLocalization) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReturnValuePromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProto) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +already_AddRefed DOMLocalization::TranslateElements( + const nsTArray>& aElements, ErrorResult& aRv) { + return TranslateElements(aElements, nullptr, aRv); +} + +already_AddRefed DOMLocalization::TranslateElements( + const nsTArray>& aElements, + nsXULPrototypeDocument* aProto, ErrorResult& aRv) { + Sequence l10nKeys; + RefPtr nativeHandler = + new ElementTranslationHandler(this, aProto); + nsTArray>& domElements = nativeHandler->Elements(); + domElements.SetCapacity(aElements.Length()); + + if (!mGlobal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + for (auto& domElement : aElements) { + if (!domElement->HasAttr(nsGkAtoms::datal10nid)) { + continue; + } + + OwningUTF8StringOrL10nIdArgs* key = l10nKeys.AppendElement(fallible); + if (!key) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + GetAttributes(*domElement, key->SetAsL10nIdArgs(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (!domElements.AppendElement(domElement, fallible)) { + // This can't really happen, we SetCapacity'd above... + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + } + + RefPtr promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (IsSync()) { + nsTArray> l10nMessages; + + FormatMessagesSync(l10nKeys, l10nMessages, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + bool allTranslated = + ApplyTranslations(domElements, l10nMessages, aProto, aRv); + if (NS_WARN_IF(aRv.Failed()) || !allTranslated) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + RefPtr callbackResult = FormatMessages(l10nKeys, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + nativeHandler->SetReturnValuePromise(promise); + callbackResult->AppendNativeHandler(nativeHandler); + return MaybeWrapPromise(promise); +} + +/** + * Promise handler used to set localization data on + * roots of elements that got successfully translated. + */ +class L10nRootTranslationHandler final : public PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(L10nRootTranslationHandler) + + explicit L10nRootTranslationHandler(Element* aRoot) : mRoot(aRoot) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + DOMLocalization::SetRootInfo(mRoot); + } + + void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override {} + + private: + ~L10nRootTranslationHandler() = default; + + RefPtr mRoot; +}; + +NS_IMPL_CYCLE_COLLECTION(L10nRootTranslationHandler, mRoot) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nRootTranslationHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nRootTranslationHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nRootTranslationHandler) + +already_AddRefed DOMLocalization::TranslateRoots(ErrorResult& aRv) { + nsTArray> promises; + + for (nsINode* root : mRoots) { + RefPtr promise = TranslateFragment(*root, aRv); + if (MOZ_UNLIKELY(aRv.Failed())) { + return nullptr; + } + + // If the root is an element, we'll add a native handler + // to set root info (language, direction etc.) on it + // once the localization finishes. + if (root->IsElement()) { + RefPtr nativeHandler = + new L10nRootTranslationHandler(root->AsElement()); + promise->AppendNativeHandler(nativeHandler); + } + + promises.AppendElement(promise); + } + AutoEntryScript aes(mGlobal, "DOMLocalization TranslateRoots"); + return Promise::All(aes.cx(), promises, aRv); +} + +/** + * Helper methods + */ + +/* static */ +void DOMLocalization::GetTranslatables( + nsINode& aNode, Sequence>& aElements, + ErrorResult& aRv) { + nsIContent* node = + aNode.IsContent() ? aNode.AsContent() : aNode.GetFirstChild(); + for (; node; node = node->GetNextNode(&aNode)) { + if (!node->IsElement()) { + continue; + } + + Element* domElement = node->AsElement(); + + if (!domElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) { + continue; + } + + if (!aElements.AppendElement(*domElement, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } +} + +/* static */ +void DOMLocalization::SetRootInfo(Element* aElement) { + nsAutoCString primaryLocale; + LocaleService::GetInstance()->GetAppLocaleAsBCP47(primaryLocale); + aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::lang, + NS_ConvertUTF8toUTF16(primaryLocale), true); + + nsAutoString dir; + if (LocaleService::GetInstance()->IsAppLocaleRTL()) { + nsGkAtoms::rtl->ToString(dir); + } else { + nsGkAtoms::ltr->ToString(dir); + } + + uint32_t nameSpace = aElement->GetNameSpaceID(); + nsAtom* dirAtom = + nameSpace == kNameSpaceID_XUL ? nsGkAtoms::localedir : nsGkAtoms::dir; + + aElement->SetAttr(kNameSpaceID_None, dirAtom, dir, true); +} + +bool DOMLocalization::ApplyTranslations( + nsTArray>& aElements, + nsTArray>& aTranslations, + nsXULPrototypeDocument* aProto, ErrorResult& aRv) { + if (aElements.Length() != aTranslations.Length()) { + aRv.Throw(NS_ERROR_FAILURE); + return false; + } + + PauseObserving(); + + bool hasMissingTranslation = false; + + nsTArray errors; + for (size_t i = 0; i < aTranslations.Length(); ++i) { + nsCOMPtr elem = aElements[i]; + if (aTranslations[i].IsNull()) { + hasMissingTranslation = true; + continue; + } + // If we have a proto, we expect all elements are connected up. + // If they're not, they may have been removed by earlier translations. + // We will have added an error in L10nOverlays in this case. + // This is an error in fluent use, but shouldn't be crashing. There's + // also no point translating the element - skip it: + if (aProto && !elem->IsInComposedDoc()) { + continue; + } + + // It is possible that someone removed the `data-l10n-id` from the element + // before the async translation completed. In that case, skip applying + // the translation. + if (!elem->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nid)) { + continue; + } + L10nOverlays::TranslateElement(*elem, aTranslations[i].Value(), errors, + aRv); + if (NS_WARN_IF(aRv.Failed())) { + hasMissingTranslation = true; + continue; + } + if (aProto) { + // We only need to rebuild deep if the translation has a value. + // Otherwise we'll only rebuild the attributes. + aProto->RebuildL10nPrototype(elem, + !aTranslations[i].Value().mValue.IsVoid()); + } + } + + ReportL10nOverlaysErrors(errors); + + ResumeObserving(); + + return !hasMissingTranslation; +} + +/* Protected */ + +void DOMLocalization::OnChange() { + Localization::OnChange(); + RefPtr promise = TranslateRoots(IgnoreErrors()); +} + +void DOMLocalization::DisconnectMutations() { + if (mMutations) { + mMutations->Disconnect(); + DisconnectRoots(); + } +} + +void DOMLocalization::DisconnectRoots() { + for (nsINode* node : mRoots) { + node->RemoveMutationObserver(mMutations); + } + mRoots.Clear(); +} + +void DOMLocalization::ReportL10nOverlaysErrors( + nsTArray& aErrors) { + nsAutoString msg; + + for (auto& error : aErrors) { + if (error.mCode.WasPassed()) { + msg = u"[fluent-dom] "_ns; + switch (error.mCode.Value()) { + case L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE: + msg += u"An element of forbidden type \""_ns + + error.mTranslatedElementName.Value() + + nsLiteralString( + u"\" was found in the translation. Only safe text-level " + "elements and elements with data-l10n-name are allowed."); + break; + case L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING: + msg += u"An element named \""_ns + error.mL10nName.Value() + + u"\" wasn't found in the source."_ns; + break; + case L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH: + msg += u"An element named \""_ns + error.mL10nName.Value() + + nsLiteralString( + u"\" was found in the translation but its type ") + + error.mTranslatedElementName.Value() + + nsLiteralString( + u" didn't match the element found in the source ") + + error.mSourceElementName.Value() + u"."_ns; + break; + case L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED: + msg += u"The element using message \""_ns + error.mL10nName.Value() + + nsLiteralString( + u"\" was removed from the DOM when translating its \"") + + error.mTranslatedElementName.Value() + u"\" parent."_ns; + break; + case L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM: + msg += nsLiteralString( + u"While translating an element with fluent ID \"") + + error.mL10nName.Value() + u"\" a child element of type \""_ns + + error.mTranslatedElementName.Value() + + nsLiteralString( + u"\" was removed. Either the fluent message " + "does not contain markup, or it does not contain markup " + "of this type."); + break; + case L10nOverlays_Binding::ERROR_UNKNOWN: + default: + msg += nsLiteralString( + u"Unknown error happened while translating an element."); + break; + } + nsPIDOMWindowInner* innerWindow = GetParentObject()->AsInnerWindow(); + Document* doc = innerWindow ? innerWindow->GetExtantDoc() : nullptr; + if (doc) { + nsContentUtils::ReportToConsoleNonLocalized( + msg, nsIScriptError::warningFlag, "DOM"_ns, doc); + } else { + NS_WARNING("Failed to report l10n DOM Overlay errors to console."); + } + printf_stderr("%s\n", NS_ConvertUTF16toUTF8(msg).get()); + } + } +} + +void DOMLocalization::ConvertStringToL10nArgs(const nsString& aInput, + intl::L10nArgs& aRetVal, + ErrorResult& aRv) { + if (aInput.IsEmpty()) { + // There are no properties. + return; + } + // This method uses a temporary dictionary to automate + // converting a JSON string into an IDL Record via a dictionary. + // + // Once we get Record::Init(const nsAString& aJSON), we'll switch to + // that. + L10nArgsHelperDict helperDict; + if (!helperDict.Init(u"{\"args\": "_ns + aInput + u"}"_ns)) { + nsTArray errors{ + "[dom/l10n] Failed to parse l10n-args JSON: "_ns + + NS_ConvertUTF16toUTF8(aInput), + }; + MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); + return; + } + for (auto& entry : helperDict.mArgs.Entries()) { + L10nArgs::EntryType* newEntry = aRetVal.Entries().AppendElement(fallible); + if (!newEntry) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + newEntry->mKey = entry.mKey; + newEntry->mValue = entry.mValue; + } +} diff --git a/dom/l10n/DOMLocalization.h b/dom/l10n/DOMLocalization.h new file mode 100644 index 0000000000..ab66194d31 --- /dev/null +++ b/dom/l10n/DOMLocalization.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_l10n_DOMLocalization_h +#define mozilla_dom_l10n_DOMLocalization_h + +#include "nsTHashSet.h" +#include "nsXULPrototypeDocument.h" +#include "mozilla/intl/Localization.h" +#include "mozilla/dom/DOMLocalizationBinding.h" +#include "mozilla/dom/L10nMutations.h" +#include "mozilla/dom/L10nOverlaysBinding.h" +#include "mozilla/dom/LocalizationBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/intl/L10nRegistry.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsINode.h" + +namespace mozilla::dom { + +class Element; +class L10nMutations; + +class DOMLocalization : public intl::Localization { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DOMLocalization, Localization) + + void Destroy(); + + static already_AddRefed Constructor( + const dom::GlobalObject& aGlobal, + const dom::Sequence& aResourceIds, + bool aIsSync, + const dom::Optional>& aRegistry, + const dom::Optional>& aLocales, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext*, JS::Handle aGivenProto) override; + + bool HasPendingMutations() const; + + /** + * DOMLocalization API + * + * Methods documentation in DOMLocalization.webidl + */ + + void ConnectRoot(nsINode& aNode); + void DisconnectRoot(nsINode& aNode); + + void PauseObserving(); + void ResumeObserving(); + + void SetAttributes(JSContext* aCx, Element& aElement, const nsAString& aId, + const Optional>& aArgs, + ErrorResult& aRv); + void GetAttributes(Element& aElement, L10nIdArgs& aResult, ErrorResult& aRv); + + void SetArgs(JSContext* aCx, Element& aElement, + const Optional>& aArgs, ErrorResult& aRv); + + already_AddRefed TranslateFragment(nsINode& aNode, ErrorResult& aRv); + + already_AddRefed TranslateElements( + const nsTArray>& aElements, ErrorResult& aRv); + already_AddRefed TranslateElements( + const nsTArray>& aElements, + nsXULPrototypeDocument* aProto, ErrorResult& aRv); + + already_AddRefed TranslateRoots(ErrorResult& aRv); + + /** + * Helper methods + */ + + /** + * Accumulates all translatable elements (ones containing + * a `data-l10n-id` attribute) from under a node into + * a list of elements. + */ + static void GetTranslatables(nsINode& aNode, + Sequence>& aElements, + ErrorResult& aRv); + + /** + * Sets the root information such as locale and direction. + */ + static void SetRootInfo(Element* aElement); + + /** + * Applies l10n translations on translatable elements. + * + * If `aProto` gets passed, it'll be used to cache + * the localized elements. + * + * Result is `true` if all translations were applied + * successfully, and `false` otherwise. + */ + bool ApplyTranslations(nsTArray>& aElements, + nsTArray>& aTranslations, + nsXULPrototypeDocument* aProto, ErrorResult& aRv); + + bool SubtreeRootInRoots(nsINode* aSubtreeRoot) { + for (const auto* key : mRoots) { + nsINode* subtreeRoot = key->SubtreeRoot(); + if (subtreeRoot == aSubtreeRoot) { + return true; + } + } + return false; + } + + DOMLocalization(nsIGlobalObject* aGlobal, bool aSync); + DOMLocalization(nsIGlobalObject* aGlobal, bool aIsSync, + const intl::ffi::LocalizationRc* aRaw); + + protected: + virtual ~DOMLocalization(); + void OnChange() override; + void DisconnectMutations(); + void DisconnectRoots(); + void ReportL10nOverlaysErrors(nsTArray& aErrors); + void ConvertStringToL10nArgs(const nsString& aInput, intl::L10nArgs& aRetVal, + ErrorResult& aRv); + + RefPtr mMutations; + nsTHashSet> mRoots; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/l10n/DocumentL10n.cpp b/dom/l10n/DocumentL10n.cpp new file mode 100644 index 0000000000..828a0583e3 --- /dev/null +++ b/dom/l10n/DocumentL10n.cpp @@ -0,0 +1,347 @@ +/* -*- 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 "DocumentL10n.h" +#include "nsIContentSink.h" +#include "nsContentUtils.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentL10nBinding.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::intl; +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentL10n) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentL10n, DOMLocalization) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContentSink) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentL10n, DOMLocalization) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentSink) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(DocumentL10n, DOMLocalization) +NS_IMPL_RELEASE_INHERITED(DocumentL10n, DOMLocalization) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentL10n) +NS_INTERFACE_MAP_END_INHERITING(DOMLocalization) + +bool DocumentL10n::mIsFirstBrowserWindow = true; + +/* static */ +RefPtr DocumentL10n::Create(Document* aDocument, bool aSync) { + RefPtr l10n = new DocumentL10n(aDocument, aSync); + + IgnoredErrorResult rv; + l10n->mReady = Promise::Create(l10n->mGlobal, rv); + if (NS_WARN_IF(rv.Failed())) { + return nullptr; + } + + return l10n.forget(); +} + +DocumentL10n::DocumentL10n(Document* aDocument, bool aSync) + : DOMLocalization(aDocument->GetScopeObject(), aSync), + mDocument(aDocument), + mState(DocumentL10nState::Constructed) { + mContentSink = do_QueryInterface(aDocument->GetCurrentContentSink()); +} + +JSObject* DocumentL10n::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return DocumentL10n_Binding::Wrap(aCx, this, aGivenProto); +} + +class L10nReadyHandler final : public PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(L10nReadyHandler) + + explicit L10nReadyHandler(Promise* aPromise, DocumentL10n* aDocumentL10n) + : mPromise(aPromise), mDocumentL10n(aDocumentL10n) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + mDocumentL10n->InitialTranslationCompleted(true); + mPromise->MaybeResolveWithUndefined(); + } + + void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override { + mDocumentL10n->InitialTranslationCompleted(false); + + nsTArray errors{ + "[dom/l10n] Could not complete initial document translation."_ns, + }; + IgnoredErrorResult rv; + MaybeReportErrorsToGecko(errors, rv, mDocumentL10n->GetParentObject()); + + /** + * We resolve the mReady here even if we encountered failures, because + * there is nothing actionable for the user pending on `mReady` to do here + * and we don't want to incentivized consumers of this API to plan the + * same pending operation for resolve and reject scenario. + * + * Additionally, without it, the stderr received "uncaught promise + * rejection" warning, which is noisy and not-actionable. + * + * So instead, we just resolve and report errors. + */ + mPromise->MaybeResolveWithUndefined(); + } + + private: + ~L10nReadyHandler() = default; + + RefPtr mPromise; + RefPtr mDocumentL10n; +}; + +NS_IMPL_CYCLE_COLLECTION(L10nReadyHandler, mPromise, mDocumentL10n) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nReadyHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nReadyHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nReadyHandler) + +void DocumentL10n::TriggerInitialTranslation() { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + if (mState >= DocumentL10nState::InitialTranslationTriggered) { + return; + } + if (!mReady) { + // If we don't have `mReady` it means that we are in shutdown mode. + // See bug 1687118 for details. + InitialTranslationCompleted(false); + return; + } + + mInitialTranslationStart = mozilla::TimeStamp::Now(); + + AutoAllowLegacyScriptExecution exemption; + + nsTArray> promises; + + ErrorResult rv; + promises.AppendElement(TranslateDocument(rv)); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + InitialTranslationCompleted(false); + mReady->MaybeRejectWithUndefined(); + return; + } + promises.AppendElement(TranslateRoots(rv)); + Element* documentElement = mDocument->GetDocumentElement(); + if (!documentElement) { + InitialTranslationCompleted(false); + mReady->MaybeRejectWithUndefined(); + return; + } + + DOMLocalization::ConnectRoot(*documentElement); + + AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslation"); + RefPtr promise = Promise::All(aes.cx(), promises, rv); + + if (promise->State() == Promise::PromiseState::Resolved) { + // If the promise is already resolved, we can fast-track + // to initial translation completed. + InitialTranslationCompleted(true); + mReady->MaybeResolveWithUndefined(); + } else { + RefPtr l10nReadyHandler = + new L10nReadyHandler(mReady, this); + promise->AppendNativeHandler(l10nReadyHandler); + + mState = DocumentL10nState::InitialTranslationTriggered; + } +} + +already_AddRefed DocumentL10n::TranslateDocument(ErrorResult& aRv) { + MOZ_ASSERT(mState == DocumentL10nState::Constructed, + "This method should be called only from Constructed state."); + RefPtr promise = Promise::Create(mGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + Element* elem = mDocument->GetDocumentElement(); + if (!elem) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + // 1. Collect all localizable elements. + Sequence> elements; + GetTranslatables(*elem, elements, aRv); + if (NS_WARN_IF(aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + + RefPtr proto = mDocument->GetPrototype(); + + // 2. Check if the document has a prototype that may cache + // translated elements. + if (proto) { + // 2.1. Handle the case when we have proto. + + // 2.1.1. Move elements that are not in the proto to a separate + // array. + Sequence> nonProtoElements; + + uint32_t i = elements.Length(); + while (i > 0) { + Element* elem = elements.ElementAt(i - 1); + MOZ_RELEASE_ASSERT(elem->HasAttr(nsGkAtoms::datal10nid)); + if (!elem->HasElementCreatedFromPrototypeAndHasUnmodifiedL10n()) { + if (NS_WARN_IF(!nonProtoElements.AppendElement(*elem, fallible))) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + elements.RemoveElement(elem); + } + i--; + } + + // We populate the sequence in reverse order. Let's bring it + // back to top->bottom one. + nonProtoElements.Reverse(); + + AutoAllowLegacyScriptExecution exemption; + + nsTArray> promises; + + // 2.1.2. If we're not loading from cache, push the elements that + // are in the prototype to be translated and cached. + if (!proto->WasL10nCached() && !elements.IsEmpty()) { + RefPtr translatePromise = + TranslateElements(elements, proto, aRv); + if (NS_WARN_IF(!translatePromise || aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + promises.AppendElement(translatePromise); + } + + // 2.1.3. If there are elements that are not in the prototype, + // localize them without attempting to cache and + // independently of if we're loading from cache. + if (!nonProtoElements.IsEmpty()) { + RefPtr nonProtoTranslatePromise = + TranslateElements(nonProtoElements, nullptr, aRv); + if (NS_WARN_IF(!nonProtoTranslatePromise || aRv.Failed())) { + promise->MaybeRejectWithUndefined(); + return promise.forget(); + } + promises.AppendElement(nonProtoTranslatePromise); + } + + // 2.1.4. Collect promises with Promise::All (maybe empty). + AutoEntryScript aes(mGlobal, "DocumentL10n InitialTranslationCompleted"); + promise = Promise::All(aes.cx(), promises, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + // 2.2. Handle the case when we don't have proto. + + // 2.2.1. Otherwise, translate all available elements, + // without attempting to cache them. + promise = TranslateElements(elements, nullptr, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return promise.forget(); +} + +void DocumentL10n::MaybeRecordTelemetry() { + mozilla::TimeStamp initialTranslationEnd = mozilla::TimeStamp::Now(); + + nsAutoString documentURI; + ErrorResult rv; + rv = mDocument->GetDocumentURI(documentURI); + if (rv.Failed()) { + return; + } + + nsCString key; + + if (documentURI.Find(u"chrome://browser/content/browser.xhtml") == 0) { + if (mIsFirstBrowserWindow) { + key = "browser_first_window"; + mIsFirstBrowserWindow = false; + } else { + key = "browser_new_window"; + } + } else if (documentURI.Find(u"about:home") == 0) { + key = "about:home"; + } else if (documentURI.Find(u"about:newtab") == 0) { + key = "about:newtab"; + } else if (documentURI.Find(u"about:preferences") == 0) { + key = "about:preferences"; + } else { + return; + } + + mozilla::TimeDuration totalTime(initialTranslationEnd - + mInitialTranslationStart); + Accumulate(Telemetry::L10N_DOCUMENT_INITIAL_TRANSLATION_TIME_US, key, + totalTime.ToMicroseconds()); +} + +void DocumentL10n::InitialTranslationCompleted(bool aL10nCached) { + if (mState >= DocumentL10nState::Ready) { + return; + } + + Element* documentElement = mDocument->GetDocumentElement(); + if (documentElement) { + SetRootInfo(documentElement); + } + + mState = DocumentL10nState::Ready; + + MaybeRecordTelemetry(); + + RefPtr doc = mDocument; + doc->InitialTranslationCompleted(aL10nCached); + + // In XUL scenario contentSink is nullptr. + if (mContentSink) { + nsCOMPtr sink = mContentSink.forget(); + sink->InitialTranslationCompleted(); + } + + // From now on, the state of Localization is unconditionally + // async. + SetAsync(); +} + +void DocumentL10n::ConnectRoot(nsINode& aNode, bool aTranslate, + ErrorResult& aRv) { + if (aTranslate) { + if (mState >= DocumentL10nState::InitialTranslationTriggered) { + RefPtr promise = TranslateFragment(aNode, aRv); + } + } + DOMLocalization::ConnectRoot(aNode); +} + +Promise* DocumentL10n::Ready() { return mReady; } + +void DocumentL10n::OnCreatePresShell() { mMutations->OnCreatePresShell(); } diff --git a/dom/l10n/DocumentL10n.h b/dom/l10n/DocumentL10n.h new file mode 100644 index 0000000000..e7b2fd5189 --- /dev/null +++ b/dom/l10n/DocumentL10n.h @@ -0,0 +1,88 @@ +/* -*- 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_l10n_DocumentL10n_h +#define mozilla_dom_l10n_DocumentL10n_h + +#include "mozilla/dom/DOMLocalization.h" + +class nsIContentSink; + +namespace mozilla::dom { + +class Document; + +enum class DocumentL10nState { + // State set when the DocumentL10n gets constructed. + Constructed = 0, + + // State set when the initial translation got triggered. This happens + // if DocumentL10n was constructed during parsing of the document. + // + // If the DocumentL10n gets constructed later, we'll skip directly to + // Ready state. + InitialTranslationTriggered, + + // State set the DocumentL10n has been fully initialized, potentially + // with initial translation being completed. + Ready, +}; + +/** + * This class maintains localization status of the document. + * + * The document will initialize it lazily when a link with a localization + * resource is added to the document. + * + * Once initialized, DocumentL10n relays all API methods to an + * instance of mozILocalization and maintains a single promise + * which gets resolved the first time the document gets translated. + */ +class DocumentL10n final : public DOMLocalization { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocumentL10n, DOMLocalization) + + static RefPtr Create(Document* aDocument, bool aSync); + + protected: + explicit DocumentL10n(Document* aDocument, bool aSync); + virtual ~DocumentL10n() = default; + + RefPtr mDocument; + RefPtr mReady; + DocumentL10nState mState; + nsCOMPtr mContentSink; + + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + Promise* Ready(); + + void TriggerInitialTranslation(); + already_AddRefed TranslateDocument(ErrorResult& aRv); + + void InitialTranslationCompleted(bool aL10nCached); + + Document* GetDocument() const { return mDocument; }; + void OnCreatePresShell(); + + void ConnectRoot(nsINode& aNode, bool aTranslate, ErrorResult& aRv); + + DocumentL10nState GetState() { return mState; }; + + void MaybeRecordTelemetry(); + + bool mBlockingLayout = false; + + mozilla::TimeStamp mInitialTranslationStart; + static bool mIsFirstBrowserWindow; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_l10n_DocumentL10n_h diff --git a/dom/l10n/L10nMutations.cpp b/dom/l10n/L10nMutations.cpp new file mode 100644 index 0000000000..63e30c2d83 --- /dev/null +++ b/dom/l10n/L10nMutations.cpp @@ -0,0 +1,362 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "L10nMutations.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsRefreshDriver.h" +#include "DOMLocalization.h" +#include "mozilla/intl/Localization.h" +#include "nsThreadManager.h" + +using namespace mozilla; +using namespace mozilla::intl; +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(L10nMutations) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(L10nMutations) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElements) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingElementsHash) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(L10nMutations) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElements) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingElementsHash) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutations) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutations) +NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutations) + +L10nMutations::L10nMutations(DOMLocalization* aDOMLocalization) + : mDOMLocalization(aDOMLocalization) { + mObserving = true; +} + +L10nMutations::~L10nMutations() { + StopRefreshObserver(); + MOZ_ASSERT(!mDOMLocalization, + "DOMLocalization<-->L10nMutations cycle should be broken."); +} + +void L10nMutations::AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + if (!mObserving) { + return; + } + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::datal10nid || + aAttribute == nsGkAtoms::datal10nargs)) { + if (IsInRoots(aElement)) { + L10nElementChanged(aElement); + } + } +} + +void L10nMutations::ContentAppended(nsIContent* aChild) { + if (!mObserving) { + return; + } + + if (!IsInRoots(aChild)) { + return; + } + + Sequence> elements; + for (nsIContent* node = aChild; node; node = node->GetNextSibling()) { + if (node->IsElement()) { + DOMLocalization::GetTranslatables(*node, elements, IgnoreErrors()); + } + } + + for (auto& elem : elements) { + L10nElementChanged(elem); + } +} + +void L10nMutations::ContentInserted(nsIContent* aChild) { + if (!mObserving) { + return; + } + + if (!aChild->IsElement()) { + return; + } + Element* elem = aChild->AsElement(); + + if (!IsInRoots(elem)) { + return; + } + + Sequence> elements; + DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors()); + + for (auto& elem : elements) { + L10nElementChanged(elem); + } +} + +void L10nMutations::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (!mObserving || mPendingElements.IsEmpty()) { + return; + } + + Element* elem = Element::FromNode(*aChild); + if (!elem || !IsInRoots(elem)) { + return; + } + + Sequence> elements; + DOMLocalization::GetTranslatables(*aChild, elements, IgnoreErrors()); + + for (auto& elem : elements) { + if (mPendingElementsHash.EnsureRemoved(elem)) { + mPendingElements.RemoveElement(elem); + } + } + + if (!HasPendingMutations()) { + nsContentUtils::AddScriptRunner(NewRunnableMethod( + "MaybeFirePendingTranslationsFinished", this, + &L10nMutations::MaybeFirePendingTranslationsFinished)); + } +} + +void L10nMutations::L10nElementChanged(Element* aElement) { + const bool wasEmpty = mPendingElements.IsEmpty(); + + if (mPendingElementsHash.EnsureInserted(aElement)) { + mPendingElements.AppendElement(aElement); + } + + if (!wasEmpty) { + return; + } + + if (!mRefreshDriver) { + StartRefreshObserver(); + } + + if (!mBlockingLoad) { + Document* doc = GetDocument(); + if (doc && doc->GetReadyStateEnum() != Document::READYSTATE_COMPLETE) { + doc->BlockOnload(); + mBlockingLoad = true; + } + } + + if (mBlockingLoad && !mPendingBlockingLoadFlush) { + // We want to make sure we flush translations and don't block the load + // indefinitely (and, in fact, that we do it rather soon, even if the + // refresh driver is not ticking yet). + // + // In some platforms (mainly Wayland) the load of the main document + // causes vsync to start running and start ticking the refresh driver, + // so we can't rely on the refresh driver ticking yet. + RefPtr task = + NewRunnableMethod("FlushPendingTranslationsBeforeLoad", this, + &L10nMutations::FlushPendingTranslationsBeforeLoad); + nsThreadManager::get().DispatchDirectTaskToCurrentThread(task); + mPendingBlockingLoadFlush = true; + } +} + +void L10nMutations::PauseObserving() { mObserving = false; } + +void L10nMutations::ResumeObserving() { mObserving = true; } + +void L10nMutations::WillRefresh(mozilla::TimeStamp aTime) { + StopRefreshObserver(); + FlushPendingTranslations(); +} + +/** + * The handler for the `TranslateElements` promise used to turn + * a potential rejection into a console warning. + **/ +class L10nMutationFinalizationHandler final : public PromiseNativeHandler { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(L10nMutationFinalizationHandler) + + explicit L10nMutationFinalizationHandler(L10nMutations* aMutations, + nsIGlobalObject* aGlobal) + : mMutations(aMutations), mGlobal(aGlobal) {} + + MOZ_CAN_RUN_SCRIPT void Settled() { + if (RefPtr mutations = mMutations) { + mutations->PendingPromiseSettled(); + } + } + + MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) override { + Settled(); + } + + MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) override { + nsTArray errors{ + "[dom/l10n] Errors during l10n mutation frame."_ns, + }; + MaybeReportErrorsToGecko(errors, IgnoreErrors(), mGlobal); + Settled(); + } + + private: + ~L10nMutationFinalizationHandler() = default; + + RefPtr mMutations; + nsCOMPtr mGlobal; +}; + +NS_IMPL_CYCLE_COLLECTION(L10nMutationFinalizationHandler, mGlobal, mMutations) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(L10nMutationFinalizationHandler) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(L10nMutationFinalizationHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(L10nMutationFinalizationHandler) + +void L10nMutations::FlushPendingTranslationsBeforeLoad() { + MOZ_ASSERT(mPendingBlockingLoadFlush); + mPendingBlockingLoadFlush = false; + FlushPendingTranslations(); +} + +void L10nMutations::FlushPendingTranslations() { + if (!mDOMLocalization) { + return; + } + + nsTArray> elements; + for (auto& elem : mPendingElements) { + if (elem->HasAttr(nsGkAtoms::datal10nid)) { + elements.AppendElement(*elem); + } + } + + mPendingElementsHash.Clear(); + mPendingElements.Clear(); + + RefPtr promise = + mDOMLocalization->TranslateElements(elements, IgnoreErrors()); + if (promise && promise->State() == Promise::PromiseState::Pending) { + mPendingPromises++; + auto l10nMutationFinalizationHandler = + MakeRefPtr( + this, mDOMLocalization->GetParentObject()); + promise->AppendNativeHandler(l10nMutationFinalizationHandler); + } + + MaybeFirePendingTranslationsFinished(); +} + +void L10nMutations::PendingPromiseSettled() { + MOZ_DIAGNOSTIC_ASSERT(mPendingPromises); + mPendingPromises--; + MaybeFirePendingTranslationsFinished(); +} + +void L10nMutations::MaybeFirePendingTranslationsFinished() { + if (HasPendingMutations()) { + return; + } + + RefPtr doc = GetDocument(); + if (NS_WARN_IF(!doc)) { + return; + } + + if (mBlockingLoad) { + mBlockingLoad = false; + doc->UnblockOnload(false); + } + nsContentUtils::DispatchEventOnlyToChrome( + doc, ToSupports(doc), u"L10nMutationsFinished"_ns, CanBubble::eNo, + Cancelable::eNo, Composed::eNo, nullptr); +} + +void L10nMutations::Disconnect() { + StopRefreshObserver(); + mDOMLocalization = nullptr; +} + +Document* L10nMutations::GetDocument() const { + if (!mDOMLocalization) { + return nullptr; + } + auto* innerWindow = mDOMLocalization->GetParentObject()->AsInnerWindow(); + if (!innerWindow) { + return nullptr; + } + return innerWindow->GetExtantDoc(); +} + +void L10nMutations::StartRefreshObserver() { + if (!mDOMLocalization || mRefreshDriver) { + return; + } + if (Document* doc = GetDocument()) { + if (nsPresContext* ctx = doc->GetPresContext()) { + mRefreshDriver = ctx->RefreshDriver(); + } + } + + // If we can't start the refresh driver, it means + // that the presContext is not available yet. + // In that case, we'll trigger the flush of pending + // elements in Document::CreatePresShell. + if (mRefreshDriver) { + mRefreshDriver->AddRefreshObserver(this, FlushType::Style, + "L10n mutations"); + } else { + NS_WARNING("[l10n][mutations] Failed to start a refresh observer."); + } +} + +void L10nMutations::StopRefreshObserver() { + if (!mDOMLocalization) { + return; + } + + if (mRefreshDriver) { + mRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); + mRefreshDriver = nullptr; + } +} + +void L10nMutations::OnCreatePresShell() { + if (!mPendingElements.IsEmpty()) { + StartRefreshObserver(); + } +} + +bool L10nMutations::IsInRoots(nsINode* aNode) { + // If the root of the mutated element is in the light DOM, + // we know it must be covered by our observer directly. + // + // Otherwise, we need to check if its subtree root is the same + // as any of the `DOMLocalization::mRoots` subtree roots. + nsINode* root = aNode->SubtreeRoot(); + + // If element is in light DOM, it must be covered by one of + // the DOMLocalization roots to end up here. + MOZ_ASSERT_IF(!root->IsShadowRoot(), + mDOMLocalization->SubtreeRootInRoots(root)); + + return !root->IsShadowRoot() || mDOMLocalization->SubtreeRootInRoots(root); +} diff --git a/dom/l10n/L10nMutations.h b/dom/l10n/L10nMutations.h new file mode 100644 index 0000000000..afc08445e0 --- /dev/null +++ b/dom/l10n/L10nMutations.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_l10n_L10nMutations_h +#define mozilla_dom_l10n_L10nMutations_h + +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsRefreshObservers.h" +#include "nsStubMutationObserver.h" +#include "nsTHashSet.h" + +class nsRefreshDriver; + +namespace mozilla::dom { +class Document; +class DOMLocalization; +/** + * L10nMutations manage observing roots for localization + * changes and coalescing pending translations into + * batches - one per animation frame. + */ +class L10nMutations final : public nsStubMultiMutationObserver, + public nsARefreshObserver { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(L10nMutations, nsIMutationObserver) + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + explicit L10nMutations(DOMLocalization* aDOMLocalization); + + /** + * Pause root observation. + * This is useful for injecting already-translated + * content into an observed root, without causing + * superflues translation. + */ + void PauseObserving(); + + /** + * Resume root observation. + */ + void ResumeObserving(); + + /** + * Disconnect roots, stop refresh observer + * and break the cycle collection deadlock + * by removing the reference to mDOMLocalization. + */ + void Disconnect(); + + /** + * Called when PresShell gets created for the document. + * If there are already pending mutations, this + * will schedule the refresh driver to translate them. + */ + void OnCreatePresShell(); + + bool HasPendingMutations() const { + return !mPendingElements.IsEmpty() || mPendingPromises; + } + + MOZ_CAN_RUN_SCRIPT void PendingPromiseSettled(); + + private: + bool mObserving = false; + bool mBlockingLoad = false; + bool mPendingBlockingLoadFlush = false; + uint32_t mPendingPromises = 0; + RefPtr mRefreshDriver; + DOMLocalization* mDOMLocalization; + + // The hash is used to speed up lookups into mPendingElements, which we need + // to guarantee some consistent ordering of operations. + nsTHashSet> mPendingElementsHash; + nsTArray> mPendingElements; + + Document* GetDocument() const; + + MOZ_CAN_RUN_SCRIPT void WillRefresh(mozilla::TimeStamp aTime) override; + + void StartRefreshObserver(); + void StopRefreshObserver(); + void L10nElementChanged(Element* aElement); + MOZ_CAN_RUN_SCRIPT void FlushPendingTranslations(); + MOZ_CAN_RUN_SCRIPT void FlushPendingTranslationsBeforeLoad(); + MOZ_CAN_RUN_SCRIPT void MaybeFirePendingTranslationsFinished(); + + ~L10nMutations(); + bool IsInRoots(nsINode* aNode); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_l10n_L10nMutations_h__ diff --git a/dom/l10n/L10nOverlays.cpp b/dom/l10n/L10nOverlays.cpp new file mode 100644 index 0000000000..716479b381 --- /dev/null +++ b/dom/l10n/L10nOverlays.cpp @@ -0,0 +1,557 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "L10nOverlays.h" +#include "mozilla/dom/HTMLTemplateElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "HTMLSplitOnSpacesTokenizer.h" +#include "nsHtml5StringParser.h" +#include "nsTextNode.h" +#include "nsIParserUtils.h" + +using namespace mozilla::dom; +using namespace mozilla; + +/** + * Check if attribute is allowed for the given element. + * + * 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. + * + * `aExplicitlyAllowed` can be passed as a list of attributes explicitly allowed + * on this element. + */ +static bool IsAttrNameLocalizable( + const nsAtom* aAttrName, Element* aElement, + const nsTArray& aExplicitlyAllowed) { + if (!aExplicitlyAllowed.IsEmpty()) { + nsAutoString name; + aAttrName->ToString(name); + if (aExplicitlyAllowed.Contains(name)) { + return true; + } + } + + nsAtom* elemName = aElement->NodeInfo()->NameAtom(); + uint32_t nameSpace = aElement->NodeInfo()->NamespaceID(); + + if (nameSpace == kNameSpaceID_XHTML) { + // Is it a globally safe attribute? + if (aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::aria_label || + aAttrName == nsGkAtoms::aria_description) { + return true; + } + + // Is it allowed on this element? + if (elemName == nsGkAtoms::a) { + return aAttrName == nsGkAtoms::download; + } + if (elemName == nsGkAtoms::area) { + return aAttrName == nsGkAtoms::download || aAttrName == nsGkAtoms::alt; + } + if (elemName == nsGkAtoms::input) { + // Special case for value on HTML inputs with type button, reset, submit + if (aAttrName == nsGkAtoms::value) { + HTMLInputElement* input = HTMLInputElement::FromNode(aElement); + if (input) { + auto type = input->ControlType(); + if (type == FormControlType::InputSubmit || + type == FormControlType::InputButton || + type == FormControlType::InputReset) { + return true; + } + } + } + return aAttrName == nsGkAtoms::alt || aAttrName == nsGkAtoms::placeholder; + } + if (elemName == nsGkAtoms::menuitem) { + return aAttrName == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::menu) { + return aAttrName == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::optgroup) { + return aAttrName == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::option) { + return aAttrName == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::track) { + return aAttrName == nsGkAtoms::label; + } + if (elemName == nsGkAtoms::img) { + return aAttrName == nsGkAtoms::alt; + } + if (elemName == nsGkAtoms::textarea) { + return aAttrName == nsGkAtoms::placeholder; + } + if (elemName == nsGkAtoms::th) { + return aAttrName == nsGkAtoms::abbr; + } + + } else if (nameSpace == kNameSpaceID_XUL) { + // Is it a globally safe attribute? + if (aAttrName == nsGkAtoms::accesskey || + aAttrName == nsGkAtoms::aria_label || aAttrName == nsGkAtoms::label || + aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::tooltiptext) { + return true; + } + + // Is it allowed on this element? + if (elemName == nsGkAtoms::description) { + return aAttrName == nsGkAtoms::value; + } + if (elemName == nsGkAtoms::key) { + return aAttrName == nsGkAtoms::key || aAttrName == nsGkAtoms::keycode; + } + if (elemName == nsGkAtoms::label) { + return aAttrName == nsGkAtoms::value; + } + } + + return false; +} + +already_AddRefed L10nOverlays::CreateTextNodeFromTextContent( + Element* aElement, ErrorResult& aRv) { + nsAutoString content; + aElement->GetTextContent(content, aRv); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return aElement->OwnerDoc()->CreateTextNode(content); +} + +class AttributeNameValueComparator { + public: + bool Equals(const AttributeNameValue& aAttribute, + const nsAttrName* aAttrName) const { + return aAttrName->Equals(NS_ConvertUTF8toUTF16(aAttribute.mName)); + } +}; + +void L10nOverlays::OverlayAttributes( + const Nullable>& aTranslation, + Element* aToElement, ErrorResult& aRv) { + nsTArray explicitlyAllowed; + + { + nsAutoString l10nAttrs; + if (aToElement->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nattrs, + l10nAttrs)) { + HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ','); + while (tokenizer.hasMoreTokens()) { + const nsAString& token = tokenizer.nextToken(); + if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) { + explicitlyAllowed.AppendElement(token); + } + } + } + } + + uint32_t i = aToElement->GetAttrCount(); + while (i > 0) { + const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1); + + if (IsAttrNameLocalizable(attrName->LocalName(), aToElement, + explicitlyAllowed) && + (aTranslation.IsNull() || + !aTranslation.Value().Contains(attrName, + AttributeNameValueComparator()))) { + RefPtr localName = attrName->LocalName(); + aToElement->UnsetAttr(localName, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + i--; + } + + if (aTranslation.IsNull()) { + return; + } + + for (auto& attribute : aTranslation.Value()) { + RefPtr nameAtom = NS_Atomize(attribute.mName); + if (IsAttrNameLocalizable(nameAtom, aToElement, explicitlyAllowed)) { + NS_ConvertUTF8toUTF16 value(attribute.mValue); + if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value, + eCaseMatters)) { + aToElement->SetAttr(nameAtom, value, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } + } +} + +void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement, + ErrorResult& aRv) { + Nullable> attributes; + uint32_t attrCount = aFromElement->GetAttrCount(); + + if (attrCount == 0) { + attributes.SetNull(); + } else { + Sequence sequence; + + uint32_t i = 0; + while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) { + AttributeNameValue* attr = sequence.AppendElement(fallible); + MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None), + "No namespaced attributes allowed."); + info.mName->LocalName()->ToUTF8String(attr->mName); + + nsAutoString value; + info.mValue->ToString(value); + attr->mValue.Assign(NS_ConvertUTF16toUTF8(value)); + } + + attributes.SetValue(sequence); + } + + return OverlayAttributes(attributes, aToElement, aRv); +} + +void L10nOverlays::ShallowPopulateUsing(Element* aFromElement, + Element* aToElement, ErrorResult& aRv) { + nsAutoString content; + aFromElement->GetTextContent(content, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aToElement->SetTextContent(content, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + OverlayAttributes(aFromElement, aToElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +already_AddRefed L10nOverlays::GetNodeForNamedElement( + Element* aSourceElement, Element* aTranslatedChild, + nsTArray& aErrors, ErrorResult& aRv) { + nsAutoString childName; + aTranslatedChild->GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nname, + childName); + RefPtr sourceChild = nullptr; + + nsINodeList* childNodes = aSourceElement->ChildNodes(); + for (uint32_t i = 0; i < childNodes->Length(); i++) { + nsINode* childNode = childNodes->Item(i); + + if (!childNode->IsElement()) { + continue; + } + Element* childElement = childNode->AsElement(); + + if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname, + childName, eCaseMatters)) { + sourceChild = childElement; + break; + } + } + + if (!sourceChild) { + L10nOverlaysError error; + error.mCode.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING); + error.mL10nName.Construct(childName); + aErrors.AppendElement(error); + return CreateTextNodeFromTextContent(aTranslatedChild, aRv); + } + + nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom(); + nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom(); + if (sourceChildName != translatedChildName && + // Create a specific exception for img vs. image mismatches, + // see bug 1543493 + !(translatedChildName == nsGkAtoms::img && + sourceChildName == nsGkAtoms::image)) { + L10nOverlaysError error; + error.mCode.Construct( + L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH); + error.mL10nName.Construct(childName); + error.mTranslatedElementName.Construct( + aTranslatedChild->NodeInfo()->LocalName()); + error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + return CreateTextNodeFromTextContent(aTranslatedChild, aRv); + } + + aSourceElement->RemoveChild(*sourceChild, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + RefPtr clone = sourceChild->CloneNode(false, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return clone.forget(); +} + +bool L10nOverlays::IsElementAllowed(Element* aElement) { + uint32_t nameSpace = aElement->NodeInfo()->NamespaceID(); + if (nameSpace != kNameSpaceID_XHTML) { + return false; + } + + nsAtom* nameAtom = aElement->NodeInfo()->NameAtom(); + + return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong || + nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s || + nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q || + nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr || + nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time || + nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var || + nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd || + nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup || + nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b || + nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark || + nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo || + nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br || + nameAtom == nsGkAtoms::wbr; +} + +already_AddRefed L10nOverlays::CreateSanitizedElement( + Element* aElement, ErrorResult& aRv) { + // Start with an empty element of the same type to remove nested children + // and non-localizable attributes defined by the translation. + + nsAutoString nameSpaceURI; + aElement->NodeInfo()->GetNamespaceURI(nameSpaceURI); + ElementCreationOptionsOrString options; + RefPtr clone = aElement->OwnerDoc()->CreateElementNS( + nameSpaceURI, aElement->NodeInfo()->LocalName(), options, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + ShallowPopulateUsing(aElement, clone, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return clone.forget(); +} + +void L10nOverlays::OverlayChildNodes(DocumentFragment* aFromFragment, + Element* aToElement, + nsTArray& aErrors, + ErrorResult& aRv) { + nsINodeList* childNodes = aFromFragment->ChildNodes(); + for (uint32_t i = 0; i < childNodes->Length(); i++) { + nsINode* childNode = childNodes->Item(i); + + if (!childNode->IsElement()) { + // Keep the translated text node. + continue; + } + + RefPtr childElement = childNode->AsElement(); + + if (childElement->HasAttr(kNameSpaceID_None, nsGkAtoms::datal10nname)) { + RefPtr sanitized = + GetNodeForNamedElement(aToElement, childElement, aErrors, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + aFromFragment->ReplaceChild(*sanitized, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + continue; + } + + if (IsElementAllowed(childElement)) { + RefPtr sanitized = CreateSanitizedElement(childElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + aFromFragment->ReplaceChild(*sanitized, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + continue; + } + + L10nOverlaysError error; + error.mCode.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE); + error.mTranslatedElementName.Construct( + childElement->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + + // If all else fails, replace the element with its text content. + RefPtr textNode = CreateTextNodeFromTextContent(childElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aFromFragment->ReplaceChild(*textNode, *childNode, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + while (aToElement->HasChildren()) { + nsIContent* child = aToElement->GetLastChild(); +#ifdef DEBUG + if (child->IsElement()) { + if (child->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::datal10nid)) { + L10nOverlaysError error; + error.mCode.Construct( + L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED); + nsAutoString id; + child->AsElement()->GetAttr(nsGkAtoms::datal10nid, id); + error.mL10nName.Construct(id); + error.mTranslatedElementName.Construct( + aToElement->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + } else if (child->AsElement()->ChildElementCount() > 0) { + L10nOverlaysError error; + error.mCode.Construct( + L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM); + nsAutoString id; + aToElement->GetAttr(nsGkAtoms::datal10nid, id); + error.mL10nName.Construct(id); + error.mTranslatedElementName.Construct( + aToElement->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + } + } +#endif + aToElement->RemoveChildNode(child, true); + } + aToElement->AppendChild(*aFromFragment, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +void L10nOverlays::TranslateElement( + const GlobalObject& aGlobal, Element& aElement, + const L10nMessage& aTranslation, + Nullable>& aErrors) { + nsTArray errors; + + ErrorResult rv; + + TranslateElement(aElement, aTranslation, errors, rv); + if (NS_WARN_IF(rv.Failed())) { + L10nOverlaysError error; + error.mCode.Construct(L10nOverlays_Binding::ERROR_UNKNOWN); + errors.AppendElement(error); + } + if (!errors.IsEmpty()) { + aErrors.SetValue(std::move(errors)); + } +} + +bool L10nOverlays::ContainsMarkup(const nsACString& aStr) { + // We use our custom ContainsMarkup rather than the + // one from FragmentOrElement.cpp, because we don't + // want to trigger HTML parsing on every `Preferences & Options` + // type of string. + const char* start = aStr.BeginReading(); + const char* end = aStr.EndReading(); + + while (start != end) { + char c = *start; + if (c == '<') { + return true; + } + ++start; + + if (c == '&' && start != end) { + c = *start; + if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z')) { + return true; + } + ++start; + } + } + + return false; +} + +void L10nOverlays::TranslateElement(Element& aElement, + const L10nMessage& aTranslation, + nsTArray& aErrors, + ErrorResult& aRv) { + if (!aTranslation.mValue.IsVoid()) { + NodeInfo* nodeInfo = aElement.NodeInfo(); + if (nodeInfo->NameAtom() == nsGkAtoms::title && + nodeInfo->NamespaceID() == kNameSpaceID_XHTML) { + // A special case for the HTML title element whose content must be text. + aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else if (!ContainsMarkup(aTranslation.mValue)) { +#ifdef DEBUG + if (aElement.ChildElementCount() > 0) { + L10nOverlaysError error; + error.mCode.Construct( + L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM); + nsAutoString id; + aElement.GetAttr(nsGkAtoms::datal10nid, id); + error.mL10nName.Construct(id); + error.mTranslatedElementName.Construct( + aElement.GetLastElementChild()->NodeInfo()->LocalName()); + aErrors.AppendElement(error); + } +#endif + // If the translation doesn't contain any markup skip the overlay logic. + aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + // Else parse the translation's HTML into a DocumentFragment, + // sanitize it and replace the element's content. + RefPtr fragment = + new (aElement.OwnerDoc()->NodeInfoManager()) + DocumentFragment(aElement.OwnerDoc()->NodeInfoManager()); + // Note: these flags should be no less restrictive than the ones in + // nsContentUtils::ParseFragmentHTML . + // We supply the flags here because otherwise the parsing of HTML can + // trip DEBUG-only crashes, see bug 1809902 for details. + auto sanitizationFlags = nsIParserUtils::SanitizerDropForms | + nsIParserUtils::SanitizerLogRemovals; + nsContentUtils::ParseFragmentHTML( + NS_ConvertUTF8toUTF16(aTranslation.mValue), fragment, + nsGkAtoms::_template, kNameSpaceID_XHTML, false, true, + sanitizationFlags); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + OverlayChildNodes(fragment, &aElement, aErrors, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } + + // Even if the translation doesn't define any localizable attributes, run + // overlayAttributes to remove any localizable attributes set by previous + // translations. + OverlayAttributes(aTranslation.mAttributes, &aElement, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} diff --git a/dom/l10n/L10nOverlays.h b/dom/l10n/L10nOverlays.h new file mode 100644 index 0000000000..95b1714761 --- /dev/null +++ b/dom/l10n/L10nOverlays.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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_l10n_L10nOverlays_h +#define mozilla_dom_l10n_L10nOverlays_h + +#include "mozilla/dom/L10nOverlaysBinding.h" +#include "mozilla/dom/LocalizationBinding.h" + +class nsINode; + +namespace mozilla::dom { + +class DocumentFragment; +class Element; + +class L10nOverlays { + public: + /** + * Translate an element. + * + * Translate the element's text content and attributes. Some HTML markup is + * allowed in the translation. The element's children with the data-l10n-name + * attribute will be treated as arguments to the translation. If the + * translation defines the same children, their attributes and text contents + * will be used for translating the matching source child. + */ + static void TranslateElement(const GlobalObject& aGlobal, Element& aElement, + const L10nMessage& aTranslation, + Nullable>& aErrors); + static void TranslateElement(Element& aElement, + const L10nMessage& aTranslation, + nsTArray& aErrors, + ErrorResult& aRv); + + private: + /** + * Create a text node from text content of an Element. + */ + static already_AddRefed CreateTextNodeFromTextContent( + Element* aElement, ErrorResult& aRv); + + /** + * Transplant localizable attributes of an element to another element. + * + * Any localizable attributes already set on the target element will be + * cleared. + */ + static void OverlayAttributes( + const Nullable>& aTranslation, + Element* aToElement, ErrorResult& aRv); + static void OverlayAttributes(Element* aFromElement, Element* aToElement, + ErrorResult& aRv); + + /** + * Helper to set textContent and localizable attributes on an element. + */ + static void ShallowPopulateUsing(Element* aFromElement, Element* aToElement, + ErrorResult& aRv); + + /** + * Sanitize a child element created by the translation. + * + * Try to find a corresponding child in sourceElement and use it as the base + * for the sanitization. This will preserve functional attributes defined on + * the child element in the source HTML. + */ + static already_AddRefed GetNodeForNamedElement( + Element* aSourceElement, Element* aTranslatedChild, + nsTArray& aErrors, ErrorResult& aRv); + + /** + * Check if element is allowed in the translation. + * + * This method is used by the sanitizer when the translation markup contains + * an element which is not present in the source code. + */ + static bool IsElementAllowed(Element* aElement); + + /** + * Sanitize an allowed element. + * + * Text-level elements allowed in translations may only use safe attributes + * and will have any nested markup stripped to text content. + */ + static already_AddRefed CreateSanitizedElement(Element* aElement, + ErrorResult& aRv); + + /** + * Replace child nodes of an element with child nodes of another element. + * + * The contents of the target element will be cleared and fully replaced with + * sanitized contents of the source element. + */ + static void OverlayChildNodes(DocumentFragment* aFromFragment, + Element* aToElement, + nsTArray& aErrors, + ErrorResult& aRv); + + /** + * A helper used to test if the string contains HTML markup. + */ + static bool ContainsMarkup(const nsACString& aStr); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/l10n/components.conf b/dom/l10n/components.conf new file mode 100644 index 0000000000..cbb689e776 --- /dev/null +++ b/dom/l10n/components.conf @@ -0,0 +1,14 @@ +Classes = [ + { + 'cid': '{a2017fd2-7d8d-11e9-b492-ab187f765b54}', + 'contract_ids': ['@mozilla.org/dom/l10n/localization;1'], + 'type': 'mozilla::dom::DOMLocalization', + 'headers': ['/dom/l10n/DOMLocalization.h'], + }, + { + 'cid': '{8d85597c-3a92-11e9-9ffc-73d225b2d53f}', + 'contract_ids': ['@mozilla.org/dom/l10n/overlays;1'], + 'type': 'mozilla::dom::L10nOverlays', + 'headers': ['/dom/l10n/L10nOverlays.h'], + }, +] diff --git a/dom/l10n/moz.build b/dom/l10n/moz.build new file mode 100644 index 0000000000..0beb3d668e --- /dev/null +++ b/dom/l10n/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Internationalization") + +EXPORTS.mozilla.dom += [ + "DocumentL10n.h", + "DOMLocalization.h", + "L10nMutations.h", + "L10nOverlays.h", +] + +UNIFIED_SOURCES += [ + "DocumentL10n.cpp", + "DOMLocalization.cpp", + "L10nMutations.cpp", + "L10nOverlays.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", +] + +FINAL_LIBRARY = "xul" + +MOCHITEST_MANIFESTS += ["tests/mochitest/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["tests/mochitest/chrome.ini"] +BROWSER_CHROME_MANIFESTS += ["tests/mochitest/browser.ini"] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] diff --git a/dom/l10n/tests/gtest/TestL10nOverlays.cpp b/dom/l10n/tests/gtest/TestL10nOverlays.cpp new file mode 100644 index 0000000000..c6c9da9939 --- /dev/null +++ b/dom/l10n/tests/gtest/TestL10nOverlays.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; 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/. */ + +#include "gtest/gtest.h" +#include "mozilla/dom/L10nOverlays.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/L10nOverlaysBinding.h" +#include "mozilla/dom/Element.h" +#include "mozilla/NullPrincipal.h" +#include "nsNetUtil.h" + +using mozilla::NullPrincipal; +using namespace mozilla::dom; + +static already_AddRefed SetUpDocument() { + nsCOMPtr uri; + NS_NewURI(getter_AddRefs(uri), "about:blank"); + nsCOMPtr principal = + NullPrincipal::CreateWithoutOriginAttributes(); + nsCOMPtr document; + nsresult rv = NS_NewDOMDocument(getter_AddRefs(document), + u""_ns, // aNamespaceURI + u""_ns, // aQualifiedName + nullptr, // aDoctype + uri, uri, principal, + false, // aLoadedAsData + nullptr, // aEventObject + DocumentFlavorHTML); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return document.forget(); +} + +/** + * This test verifies that the basic C++ DOM L10nOverlays API + * works correctly. + */ +TEST(DOM_L10n_Overlays, Initial) +{ + mozilla::ErrorResult rv; + + // 1. Set up an HTML document. + nsCOMPtr doc = SetUpDocument(); + + // 2. Create a simple Element with a child. + // + //
+ // + //
+ // + RefPtr elem = doc->CreateHTMLElement(nsGkAtoms::div); + RefPtr span = doc->CreateHTMLElement(nsGkAtoms::a); + span->SetAttribute(u"data-l10n-name"_ns, u"link"_ns, rv); + span->SetAttribute(u"href"_ns, u"https://www.mozilla.org"_ns, rv); + elem->AppendChild(*span, rv); + + // 3. Create an L10nMessage with a translation for the element. + L10nMessage translation; + translation.mValue.AssignLiteral( + "Hello World."); + + // 4. Translate the element. + nsTArray errors; + L10nOverlays::TranslateElement(*elem, translation, errors, rv); + + nsAutoString textContent; + elem->GetInnerHTML(textContent, rv); + + // 5. Verify that the innerHTML matches the expectations. + ASSERT_STREQ(NS_ConvertUTF16toUTF8(textContent).get(), + "Hello World."); +} diff --git a/dom/l10n/tests/gtest/moz.build b/dom/l10n/tests/gtest/moz.build new file mode 100644 index 0000000000..0e1e2173a6 --- /dev/null +++ b/dom/l10n/tests/gtest/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "TestL10nOverlays.cpp", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/l10n/tests/mochitest/browser.ini b/dom/l10n/tests/mochitest/browser.ini new file mode 100644 index 0000000000..966be56661 --- /dev/null +++ b/dom/l10n/tests/mochitest/browser.ini @@ -0,0 +1,6 @@ +[document_l10n/non-system-principal/browser_resource_uri.js] +support-files = + document_l10n/non-system-principal/test.html + document_l10n/non-system-principal/localization/test.ftl +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure diff --git a/dom/l10n/tests/mochitest/chrome.ini b/dom/l10n/tests/mochitest/chrome.ini new file mode 100644 index 0000000000..68f3d89d40 --- /dev/null +++ b/dom/l10n/tests/mochitest/chrome.ini @@ -0,0 +1,47 @@ +[l10n_overlays/test_attributes.html] +[l10n_overlays/test_functional_children.html] +[l10n_overlays/test_text_children.html] +[l10n_overlays/test_extra_text_markup.html] +[l10n_overlays/test_l10n_overlays.xhtml] +[l10n_overlays/test_same_id.html] +[l10n_overlays/test_same_id_args.html] +[l10n_overlays/test_title.html] + +[l10n_mutations/test_append_content_post_dcl.html] +[l10n_mutations/test_append_content_pre_dcl.html] +[l10n_mutations/test_append_fragment_post_dcl.html] +[l10n_mutations/test_disconnectedRoot_webcomponent.html] +[l10n_mutations/test_set_attributes.html] +[l10n_mutations/test_pause_observing.html] +[l10n_mutations/test_template.html] +[l10n_mutations/test_remove_element.html] +[l10n_mutations/test_remove_fragment.html] + +[dom_localization/test_attr_sanitized.html] +[dom_localization/test_getAttributes.html] +[dom_localization/test_setAttributes.html] +[dom_localization/test_translateElements.html] +[dom_localization/test_translateFragment.html] +[dom_localization/test_connectRoot.html] +[dom_localization/test_connectRoot_webcomponent.html] +[dom_localization/test_disconnectRoot.html] +[dom_localization/test_repeated_l10nid.html] +[dom_localization/test_translateRoots.html] +[dom_localization/test_l10n_mutations.html] +[dom_localization/test_overlay.html] +[dom_localization/test_overlay_repeated.html] +[dom_localization/test_overlay_missing_all.html] +[dom_localization/test_overlay_missing_children.html] +[dom_localization/test_overlay_sanitized.html] +[dom_localization/test_domloc.xhtml] + + +[document_l10n/test_docl10n.xhtml] +[document_l10n/test_docl10n.html] +[document_l10n/test_docl10n_sync.html] +[document_l10n/test_docl10n_ready_rejected.html] +[document_l10n/test_docl10n_removeResourceIds.html] +[document_l10n/test_docl10n_lazy.html] +[document_l10n/test_connectRoot_webcomponent.html] +[document_l10n/test_connectRoot_webcomponent_lazy.html] +[document_l10n/test_telemetry.html] diff --git a/dom/l10n/tests/mochitest/document_l10n/README.txt b/dom/l10n/tests/mochitest/document_l10n/README.txt new file mode 100644 index 0000000000..b798a5039a --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/README.txt @@ -0,0 +1,3 @@ +Tests in this directory cover support for DocumentL10n +WebIDL API across different use cases such as +processes, principals and so on. diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt new file mode 100644 index 0000000000..d0cc074166 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/README.txt @@ -0,0 +1,3 @@ +Tests in this directory cover the functionality +of DocumentL10n WebIDL API in non-system-principal +scenario. diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js new file mode 100644 index 0000000000..a658e88bec --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/browser_resource_uri.js @@ -0,0 +1,109 @@ +let uri = + "chrome://mochitests/content/browser/dom/l10n/tests/mochitest//document_l10n/non-system-principal/"; +let protocol = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +protocol.setSubstitution("l10n-test", Services.io.newURI(uri)); + +// Since we want the mock source to work with all locales, we're going +// to register it for currently used locales, and we'll put the path that +// doesn't use the `{locale}` component to make it work irrelevant of +// what locale the mochitest is running in. +// +// Notice: we're using a `chrome://` protocol here only for convenience reasons. +// Real sources should use `resource://` protocol. +let locales = Services.locale.appLocalesAsBCP47; + +// This source is actually using a real `FileSource` instead of a mocked one, +// because we want to test that fetching real I/O out of the `uri` works in non-system-principal. +let source = new L10nFileSource("test", "app", locales, `${uri}localization/`); +L10nRegistry.getInstance().registerSources([source]); + +registerCleanupFunction(() => { + protocol.setSubstitution("l10n-test", null); + L10nRegistry.getInstance().removeSources(["test"]); + SpecialPowers.pushPrefEnv({ + set: [["dom.ipc.processPrelaunch.enabled", true]], + }); +}); + +const kChildPage = getRootDirectory(gTestPath) + "test.html"; + +const kAboutPagesRegistered = Promise.all([ + BrowserTestUtils.registerAboutPage( + registerCleanupFunction, + "test-about-l10n-child", + kChildPage, + Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT | + Ci.nsIAboutModule.ALLOW_SCRIPT + ), +]); + +add_task(async () => { + // Bug 1640333 - windows fails (sometimes) to ever get document.l10n.ready + // if e10s process caching is enabled + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processPrelaunch.enabled", false], + ["dom.security.skip_about_page_has_csp_assert", true], + ], + }); + await kAboutPagesRegistered; + await BrowserTestUtils.withNewTab( + "about:test-about-l10n-child", + async browser => { + await SpecialPowers.spawn(browser, [], async function () { + let document = content.document; + let window = document.defaultView; + + await document.testsReadyPromise; + + let principal = SpecialPowers.wrap(document).nodePrincipal; + is( + principal.spec, + "about:test-about-l10n-child", + "correct content principal" + ); + + let desc = document.getElementById("main-desc"); + + // We can test here for a particular value because we're + // using a mock file source which is locale independent. + // + // If you're writing a test that verifies that a UI + // widget got real localization, you should not rely on + // the particular value, but rather on the content not + // being empty (to keep the test pass in non-en-US locales). + is(desc.textContent, "This is a mock page title"); + + // Test for l10n.getAttributes + let label = document.getElementById("label1"); + let l10nArgs = document.l10n.getAttributes(label); + is(l10nArgs.id, "subtitle"); + is(l10nArgs.args.name, "Firefox"); + + // Test for manual value formatting + let customMsg = document.getElementById("customMessage").textContent; + is(customMsg, "This is a custom message formatted from JS."); + + // Since we applied the `data-l10n-id` attribute + // on `label` in this microtask, we have to wait for + // the next paint to verify that the MutationObserver + // applied the translation. + await new Promise(resolve => { + let verifyL10n = () => { + if (!label.textContent.includes("Firefox")) { + window.requestAnimationFrame(verifyL10n); + } else { + resolve(); + } + }; + + window.requestAnimationFrame(verifyL10n); + }); + }); + } + ); +}); diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl new file mode 100644 index 0000000000..a5da5a8f00 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/localization/test.ftl @@ -0,0 +1,4 @@ +page-title = This is a mock page title +subtitle = This is a label for { $name } + +custom-message = This is a custom message formatted from JS. diff --git a/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html new file mode 100644 index 0000000000..5d91f3da46 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/non-system-principal/test.html @@ -0,0 +1,37 @@ + + + + + Test DocumentL10n in HTML environment + + + + +

+ +

+

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html new file mode 100644 index 0000000000..3f2def3547 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent.html @@ -0,0 +1,90 @@ + + + + + Test Web Component connecting into Document's l10n + + + + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html new file mode 100644 index 0000000000..b74bbc00c8 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_connectRoot_webcomponent_lazy.html @@ -0,0 +1,98 @@ + + + + + Test Web Component connecting into Document's l10n + + + + + + + + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html new file mode 100644 index 0000000000..12ff623f5c --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.html @@ -0,0 +1,66 @@ + + + + + Test DocumentL10n in HTML environment + + + + + + +

+ +

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml new file mode 100644 index 0000000000..2d51d8689e --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n.xhtml @@ -0,0 +1,60 @@ + + + + + + + + + Test DocumentL10n in HTML environment + + + + + + +

+ +

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html new file mode 100644 index 0000000000..6c3ddb73ed --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_lazy.html @@ -0,0 +1,44 @@ + + + + + Test Lazy DocumentL10n in HTML environment + + + + + +

+ +

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html new file mode 100644 index 0000000000..63e18f802c --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_ready_rejected.html @@ -0,0 +1,29 @@ + + + + + Test mozIDOMLocalization.ready rejected state + + + + + + +

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html new file mode 100644 index 0000000000..8ccaa04614 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_removeResourceIds.html @@ -0,0 +1,59 @@ + + + + + Test DocumentL10n::RemoveResourceIds + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html new file mode 100644 index 0000000000..ea44d1afe1 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_docl10n_sync.html @@ -0,0 +1,54 @@ + + + + + Test DocumentL10n in HTML environment + + + + + + +

+ +

+ + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html b/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html new file mode 100644 index 0000000000..b528ebc7ea --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_telemetry.html @@ -0,0 +1,83 @@ + + + + + Test DocumentL10n Telemetry + + + + + + + diff --git a/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html b/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html new file mode 100644 index 0000000000..4f4b29c500 --- /dev/null +++ b/dom/l10n/tests/mochitest/document_l10n/test_unpriv_iframe.html @@ -0,0 +1,26 @@ + + + + + Ensure unprivilaged document cannot access document.l10n in an iframe + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html b/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html new file mode 100644 index 0000000000..a9244c9891 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_attr_sanitized.html @@ -0,0 +1,49 @@ + + + + + Test DOMLocalization's attr sanitization functionality + + + + + +

+

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html new file mode 100644 index 0000000000..83b83757f1 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot.html @@ -0,0 +1,45 @@ + + + + + Test DOMLocalization.prototype.connectRoot + + + + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html new file mode 100644 index 0000000000..1254bd814f --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_connectRoot_webcomponent.html @@ -0,0 +1,72 @@ + + + + + Test DOMLocalization.prototype.connectRoot with Web Components + + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html b/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html new file mode 100644 index 0000000000..fd7344c88e --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_disconnectRoot.html @@ -0,0 +1,60 @@ + + + + + Test DOMLocalization.prototype.disconnectRoot + + + + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml b/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml new file mode 100644 index 0000000000..a4d4fcc506 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_domloc.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html b/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html new file mode 100644 index 0000000000..11996d132a --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_getAttributes.html @@ -0,0 +1,49 @@ + + + + + Test DOMLocalization.prototype.getAttributes + + + + + +

+

+

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html b/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html new file mode 100644 index 0000000000..278baa15dd --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_l10n_mutations.html @@ -0,0 +1,57 @@ + + + + + Test DOMLocalization's MutationObserver + + + + + +

+

+
+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay.html new file mode 100644 index 0000000000..2c8c219bb2 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay.html @@ -0,0 +1,60 @@ + + + + + Test DOMLocalization's DOMOverlay functionality + + + + + +

+

+ +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html new file mode 100644 index 0000000000..c6f285aa38 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_all.html @@ -0,0 +1,37 @@ + + + + + Test DOMLocalization's DOMOverlay functionality + + + + + +

+ + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html new file mode 100644 index 0000000000..9e4fec1ce2 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_missing_children.html @@ -0,0 +1,53 @@ + + + + + Test DOMLocalization's DOMOverlay functionality + + + + + +

+ + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html new file mode 100644 index 0000000000..a169c27fa0 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_repeated.html @@ -0,0 +1,50 @@ + + + + + Test DOMLocalization's DOMOverlay functionality + + + + + +

+ + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html b/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html new file mode 100644 index 0000000000..fcc201edb6 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_overlay_sanitized.html @@ -0,0 +1,52 @@ + + + + + Test DOMLocalization's DOMOverlay functionality + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html b/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html new file mode 100644 index 0000000000..64d585f0a0 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_repeated_l10nid.html @@ -0,0 +1,60 @@ + + + + + Test DOMLocalization's matching l10nIds functionality + + + + + +

+

+ +

+ +

+ +

+ +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html b/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html new file mode 100644 index 0000000000..508a15dcd3 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_setAttributes.html @@ -0,0 +1,79 @@ + + + + + Test DOMLocalization.prototype.setAttributes and DOMLocalization.prototype.setArgs + + + + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html b/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html new file mode 100644 index 0000000000..7893309ce4 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_translateElements.html @@ -0,0 +1,47 @@ + + + + + Test DOMLocalization.prototype.translateElements + + + + + +

+ + + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html b/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html new file mode 100644 index 0000000000..c5ccfd996a --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_translateFragment.html @@ -0,0 +1,48 @@ + + + + + Test DOMLocalization.prototype.translateFragment + + + + + +

+ + diff --git a/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html b/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html new file mode 100644 index 0000000000..58bfb044b0 --- /dev/null +++ b/dom/l10n/tests/mochitest/dom_localization/test_translateRoots.html @@ -0,0 +1,56 @@ + + + + + Test DOMLocalization.prototype.translateRoots + + + + + +
+

+
+
+

+
+ + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html new file mode 100644 index 0000000000..e3a9819728 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_post_dcl.html @@ -0,0 +1,30 @@ + + + + + Test L10n Mutations for ContentAppended after DOMContentLoaded + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html new file mode 100644 index 0000000000..825464e7b8 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_content_pre_dcl.html @@ -0,0 +1,28 @@ + + + + + Test L10n Mutations for ContentAppended before DOMContentLoaded + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html b/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html new file mode 100644 index 0000000000..8efb3203c6 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_append_fragment_post_dcl.html @@ -0,0 +1,39 @@ + + + + + Test L10n Mutations for appending a fragment after DOMContentLoaded + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html b/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html new file mode 100644 index 0000000000..bb9d9fc24d --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_disconnectedRoot_webcomponent.html @@ -0,0 +1,148 @@ + + + + + Test DOMLocalization.prototype.connectRoot with Web Components + + + + + + +

+ + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html b/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html new file mode 100644 index 0000000000..d225153418 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_pause_observing.html @@ -0,0 +1,44 @@ + + + + + Test L10n Mutations for Pause/Resume Observing + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html new file mode 100644 index 0000000000..347e858d52 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_element.html @@ -0,0 +1,68 @@ + + + + + Test L10n Mutations for removing element + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html new file mode 100644 index 0000000000..4f4d0fe1d8 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_remove_fragment.html @@ -0,0 +1,67 @@ + + + + + Test L10n Mutations for removing fragment + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html b/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html new file mode 100644 index 0000000000..762353a06c --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_set_attributes.html @@ -0,0 +1,37 @@ + + + + + Test L10n Mutations for AttributeChange after DOMContentLoaded + + + + + + + +
+
+ + diff --git a/dom/l10n/tests/mochitest/l10n_mutations/test_template.html b/dom/l10n/tests/mochitest/l10n_mutations/test_template.html new file mode 100644 index 0000000000..d02ecfcd4d --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_mutations/test_template.html @@ -0,0 +1,37 @@ + + + + + Test L10n Mutations in Template elements + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html b/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html new file mode 100644 index 0000000000..3d1f6048b2 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_attributes.html @@ -0,0 +1,86 @@ + + + + + Test L10nOverlays Top-level attributes + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html b/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html new file mode 100644 index 0000000000..5f60599dfc --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_extra_text_markup.html @@ -0,0 +1,136 @@ + + + + + Test L10nOverlays Localized text markup + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html b/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html new file mode 100644 index 0000000000..dba5b6e633 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_functional_children.html @@ -0,0 +1,344 @@ + + + + + Test L10nOverlays functional children test + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml b/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml new file mode 100644 index 0000000000..494958c573 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_l10n_overlays.xhtml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html new file mode 100644 index 0000000000..ecaefbd68d --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id.html @@ -0,0 +1,57 @@ + + + + + Test Amount of mutations generated from DOM Overlays + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html new file mode 100644 index 0000000000..e43c394970 --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_same_id_args.html @@ -0,0 +1,57 @@ + + + + + Test Amount of mutations generated from DOM Overlays + + + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html b/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html new file mode 100644 index 0000000000..1c2fab7ade --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_text_children.html @@ -0,0 +1,74 @@ + + + + + Test L10nOverlays Text-semantic argument elements + + + + + + + diff --git a/dom/l10n/tests/mochitest/l10n_overlays/test_title.html b/dom/l10n/tests/mochitest/l10n_overlays/test_title.html new file mode 100644 index 0000000000..4571589b8e --- /dev/null +++ b/dom/l10n/tests/mochitest/l10n_overlays/test_title.html @@ -0,0 +1,60 @@ + + + + + Test L10nOverlays Special treatment of the title element + + + + + + + diff --git a/dom/l10n/tests/mochitest/mochitest.ini b/dom/l10n/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..82af57c47c --- /dev/null +++ b/dom/l10n/tests/mochitest/mochitest.ini @@ -0,0 +1 @@ +[document_l10n/test_unpriv_iframe.html] -- cgit v1.2.3