зеркало из https://github.com/mozilla/gecko-dev.git
701 строка
23 KiB
C++
701 строка
23 KiB
C++
/* -*- 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 "json/json.h"
|
|
#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> DOMLocalization::Constructor(
|
|
const GlobalObject& aGlobal,
|
|
const Sequence<dom::OwningUTF8StringOrResourceId>& aResourceIds,
|
|
bool aIsSync, const Optional<NonNull<L10nRegistry>>& aRegistry,
|
|
const Optional<Sequence<nsCString>>& aLocales, ErrorResult& aRv) {
|
|
auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)};
|
|
Maybe<nsTArray<nsCString>> locales;
|
|
|
|
if (aLocales.WasPassed()) {
|
|
locales.emplace();
|
|
locales->SetCapacity(aLocales.Value().Length());
|
|
for (const auto& locale : aLocales.Value()) {
|
|
locales->AppendElement(locale);
|
|
}
|
|
}
|
|
|
|
RefPtr<const ffi::LocalizationRc> 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<nsIGlobalObject> 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<JSObject*> 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<nsIGlobalObject> 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<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv) {
|
|
if (aArgs.WasPassed() && aArgs.Value()) {
|
|
nsAutoString data;
|
|
JS::Rooted<JS::Value> 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(nsGkAtoms::datal10nid, l10nId)) {
|
|
CopyUTF16toUTF8(l10nId, aResult.mId);
|
|
}
|
|
|
|
if (aElement.GetAttr(nsGkAtoms::datal10nargs, l10nArgs)) {
|
|
ConvertStringToL10nArgs(l10nArgs, aResult.mArgs.SetValue(), aRv);
|
|
}
|
|
}
|
|
|
|
void DOMLocalization::SetArgs(JSContext* aCx, Element& aElement,
|
|
const Optional<JS::Handle<JSObject*>>& aArgs,
|
|
ErrorResult& aRv) {
|
|
if (aArgs.WasPassed() && aArgs.Value()) {
|
|
nsAutoString data;
|
|
JS::Rooted<JS::Value> 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<Promise> DOMLocalization::TranslateFragment(nsINode& aNode,
|
|
ErrorResult& aRv) {
|
|
Sequence<OwningNonNull<Element>> 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<nsCOMPtr<Element>>& Elements() { return mElements; }
|
|
|
|
void SetReturnValuePromise(Promise* aReturnValuePromise) {
|
|
mReturnValuePromise = aReturnValuePromise;
|
|
}
|
|
|
|
virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
|
|
ErrorResult& aRv) override {
|
|
ErrorResult rv;
|
|
|
|
nsTArray<Nullable<L10nMessage>> l10nData;
|
|
if (aValue.isObject()) {
|
|
JS::ForOfIterator iter(aCx);
|
|
if (!iter.init(aValue, JS::ForOfIterator::AllowNonIterable)) {
|
|
mReturnValuePromise->MaybeRejectWithUndefined();
|
|
return;
|
|
}
|
|
if (!iter.valueIsIterable()) {
|
|
mReturnValuePromise->MaybeRejectWithUndefined();
|
|
return;
|
|
}
|
|
|
|
JS::Rooted<JS::Value> temp(aCx);
|
|
while (true) {
|
|
bool done;
|
|
if (!iter.next(&temp, &done)) {
|
|
mReturnValuePromise->MaybeRejectWithUndefined();
|
|
return;
|
|
}
|
|
|
|
if (done) {
|
|
break;
|
|
}
|
|
|
|
Nullable<L10nMessage>* 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<JS::Value> aValue,
|
|
ErrorResult& aRv) override {
|
|
mReturnValuePromise->MaybeRejectWithClone(aCx, aValue);
|
|
}
|
|
|
|
private:
|
|
~ElementTranslationHandler() = default;
|
|
|
|
nsTArray<nsCOMPtr<Element>> mElements;
|
|
RefPtr<DOMLocalization> mDOMLocalization;
|
|
RefPtr<Promise> mReturnValuePromise;
|
|
RefPtr<nsXULPrototypeDocument> 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<Promise> DOMLocalization::TranslateElements(
|
|
const nsTArray<OwningNonNull<Element>>& aElements, ErrorResult& aRv) {
|
|
return TranslateElements(aElements, nullptr, aRv);
|
|
}
|
|
|
|
already_AddRefed<Promise> DOMLocalization::TranslateElements(
|
|
const nsTArray<OwningNonNull<Element>>& aElements,
|
|
nsXULPrototypeDocument* aProto, ErrorResult& aRv) {
|
|
Sequence<OwningUTF8StringOrL10nIdArgs> l10nKeys;
|
|
RefPtr<ElementTranslationHandler> nativeHandler =
|
|
new ElementTranslationHandler(this, aProto);
|
|
nsTArray<nsCOMPtr<Element>>& 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 = Promise::Create(mGlobal, aRv);
|
|
if (NS_WARN_IF(aRv.Failed())) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (IsSync()) {
|
|
nsTArray<Nullable<L10nMessage>> 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<Promise> 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<JS::Value> aValue,
|
|
ErrorResult& aRv) override {
|
|
DOMLocalization::SetRootInfo(mRoot);
|
|
}
|
|
|
|
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
|
|
ErrorResult& aRv) override {}
|
|
|
|
private:
|
|
~L10nRootTranslationHandler() = default;
|
|
|
|
RefPtr<Element> 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<Promise> DOMLocalization::TranslateRoots(ErrorResult& aRv) {
|
|
nsTArray<RefPtr<Promise>> promises;
|
|
|
|
for (nsINode* root : mRoots) {
|
|
RefPtr<Promise> 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<L10nRootTranslationHandler> 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<OwningNonNull<Element>>& 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(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<nsCOMPtr<Element>>& aElements,
|
|
nsTArray<Nullable<L10nMessage>>& aTranslations,
|
|
nsXULPrototypeDocument* aProto, ErrorResult& aRv) {
|
|
if (aElements.Length() != aTranslations.Length()) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
PauseObserving();
|
|
|
|
bool hasMissingTranslation = false;
|
|
|
|
nsTArray<L10nOverlaysError> 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(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> 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<L10nOverlaysError>& 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()->GetAsInnerWindow();
|
|
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;
|
|
}
|
|
|
|
Json::Value args;
|
|
Json::Reader jsonReader;
|
|
|
|
if (!jsonReader.parse(NS_ConvertUTF16toUTF8(aInput).get(), args, false)) {
|
|
nsTArray<nsCString> errors{
|
|
"[dom/l10n] Failed to parse l10n-args JSON: "_ns +
|
|
NS_ConvertUTF16toUTF8(aInput),
|
|
};
|
|
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
|
|
return;
|
|
}
|
|
|
|
if (!args.isObject()) {
|
|
nsTArray<nsCString> errors{
|
|
"[dom/l10n] Failed to parse l10n-args JSON: "_ns +
|
|
NS_ConvertUTF16toUTF8(aInput),
|
|
};
|
|
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
|
|
return;
|
|
}
|
|
|
|
for (Json::ValueConstIterator iter = args.begin(); iter != args.end();
|
|
++iter) {
|
|
L10nArgs::EntryType* newEntry = aRetVal.Entries().AppendElement(fallible);
|
|
if (!newEntry) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
newEntry->mKey = iter.name().c_str();
|
|
if (iter->isString()) {
|
|
newEntry->mValue.SetValue().RawSetAsUTF8String().Assign(
|
|
iter->asString().c_str(), iter->asString().length());
|
|
} else if (iter->isDouble()) {
|
|
newEntry->mValue.SetValue().RawSetAsDouble() = iter->asDouble();
|
|
} else if (iter->isBool()) {
|
|
if (iter->asBool()) {
|
|
newEntry->mValue.SetValue().RawSetAsUTF8String().Assign("true");
|
|
} else {
|
|
newEntry->mValue.SetValue().RawSetAsUTF8String().Assign("false");
|
|
}
|
|
} else if (iter->isNull()) {
|
|
newEntry->mValue.SetNull();
|
|
} else {
|
|
nsTArray<nsCString> errors{
|
|
"[dom/l10n] Failed to convert l10n-args JSON: "_ns +
|
|
NS_ConvertUTF16toUTF8(aInput),
|
|
};
|
|
MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
|
|
}
|
|
}
|
|
}
|