Bug 1560342 - Replace generic object argument with a specific L10nArgs object for passing l10n arguments. r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D35586

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Zibi Braniecki 2019-06-25 19:43:50 +00:00
parent ae32fa44eb
commit 224a1bb5c4
7 changed files with 116 additions and 34 deletions

View File

@ -232,6 +232,10 @@ DOMInterfaces = {
'implicitJSContext': [ 'filename', 'lineNumber', 'stack' ],
},
'DOMLocalization': {
'implicitJSContext': [ 'getAttributes' ],
},
'DOMMatrixReadOnly': {
'headerFile': 'mozilla/dom/DOMMatrix.h',
},
@ -500,10 +504,10 @@ DOMInterfaces = {
},
'Localization': {
'implicitJSContext': [ 'formatValue', 'formatValues', 'formatMessages' ],
'nativeType': 'mozilla::intl::Localization',
},
'MatchGlob': {
'nativeType': 'mozilla::extensions::MatchGlob',
},

View File

@ -4,21 +4,31 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* An object used to carry localization information from and to an
* Element.
* L10nKey is an object used to carry localization tuple for message
* translation.
*
* Fields:
* id - identifier of a message used to localize the element.
* args - an optional map of arguments used to format the message.
* The argument will be converted to/from JSON, so can
* handle any value that JSON can, but in practice, the API
* will only use a string or a number for now.
* id - identifier of a message.
* args - an optional record of arguments used to format the message.
* The argument will be converted to/from JSON, and the API
* will only handle strings and numbers.
*/
typedef record<DOMString, (DOMString or double)?> L10nArgs;
dictionary L10nKey {
DOMString? id = null;
object? args = null;
L10nArgs? args = null;
};
/**
* L10nMessage is a compound translation unit from Fluent which
* encodes the value and (optionall) a list of attributes used
* to translate a given widget.
*
* Most simple imperative translations will only use the `value`,
* but when building a Message for a UI widget, a combination
* of a value and attributes will be used.
*/
dictionary AttributeNameValue {
required DOMString name;
required DOMString value;
@ -29,6 +39,11 @@ dictionary L10nMessage {
sequence<AttributeNameValue>? attributes = null;
};
/**
* A callback function which takes a list of locales and a list
* of localization resources and produces an iterator over
* FluentBundle objects used for localization with fallbacks.
*/
callback GenerateMessages = Promise<any> (sequence<DOMString> aAppLocales, sequence<DOMString> aResourceIds);
/**
@ -82,7 +97,7 @@ interface Localization {
* let value = await document.l10n.formatValue("unread-emails", {count: 5});
* assert.equal(value, "You have 5 unread emails");
*/
[NewObject] Promise<DOMString> formatValue(DOMString aId, optional object aArgs);
[NewObject] Promise<DOMString> formatValue(DOMString aId, optional L10nArgs aArgs);
/**
* Formats values of a list of messages with given ids.
@ -122,3 +137,10 @@ interface Localization {
*/
[NewObject] Promise<sequence<L10nMessage>> formatMessages(sequence<L10nKey> aKeys);
};
/**
* A helper dict for converting between JS Value and L10nArgs.
*/
dictionary L10nArgsHelperDict {
required L10nArgs args;
};

View File

@ -148,16 +148,7 @@ void DOMLocalization::GetAttributes(JSContext* aCx, Element& aElement,
}
if (aElement.GetAttr(kNameSpaceID_None, nsGkAtoms::datal10nargs, l10nArgs)) {
JS::Rooted<JS::Value> json(aCx);
if (!JS_ParseJSON(aCx, l10nArgs.get(), l10nArgs.Length(), &json)) {
aRv.NoteJSContextException(aCx);
return;
}
if (!json.isObject()) {
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
return;
}
aResult.mArgs = &json.toObject();
ConvertStringToL10nArgs(aCx, l10nArgs, aResult.mArgs.SetValue(), aRv);
}
}
@ -288,7 +279,7 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
return nullptr;
}
AutoEntryScript aes(mGlobal, "DOMLocalization GetAttributes");
AutoEntryScript aes(mGlobal, "DOMLocalization TranslateElements");
JSContext* cx = aes.cx();
for (auto& domElement : aElements) {
@ -303,8 +294,7 @@ already_AddRefed<Promise> DOMLocalization::TranslateElements(
}
GetAttributes(cx, *domElement, *key, aRv);
if (aRv.Failed()) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
@ -534,3 +524,29 @@ void DOMLocalization::ReportL10nOverlaysErrors(
}
}
}
void DOMLocalization::ConvertStringToL10nArgs(JSContext* aCx,
const nsString& aInput,
intl::L10nArgs& aRetVal,
ErrorResult& aRv) {
// 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(NS_LITERAL_STRING("{\"args\": ") + aInput +
NS_LITERAL_STRING("}"))) {
aRv.Throw(NS_ERROR_UNEXPECTED);
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;
}
}

View File

@ -83,6 +83,8 @@ class DOMLocalization : public intl::Localization {
void DisconnectMutations();
void DisconnectRoots();
void ReportL10nOverlaysErrors(nsTArray<L10nOverlaysError>& aErrors);
void ConvertStringToL10nArgs(JSContext* aCx, const nsString& aInput,
intl::L10nArgs& aRetVal, ErrorResult& aRv);
RefPtr<L10nMutations> mMutations;
nsTHashtable<nsRefPtrHashKey<Element>> mRoots;

View File

@ -171,19 +171,22 @@ uint32_t Localization::RemoveResourceIds(
}
already_AddRefed<Promise> Localization::FormatValue(
JSContext* aCx, const nsAString& aId,
const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv) {
JSContext* aCx, const nsAString& aId, const Optional<L10nArgs>& aArgs,
ErrorResult& aRv) {
JS::Rooted<JS::Value> args(aCx);
if (aArgs.WasPassed()) {
args = JS::ObjectValue(*aArgs.Value());
ConvertL10nArgsToJSValue(aCx, aArgs.Value(), &args, aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
} else {
args = JS::UndefinedValue();
}
RefPtr<Promise> promise;
nsresult rv = mLocalization->FormatValue(aId, args, getter_AddRefs(promise));
if (NS_FAILED(rv)) {
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
return nullptr;
}
@ -205,7 +208,7 @@ already_AddRefed<Promise> Localization::FormatValues(
RefPtr<Promise> promise;
aRv = mLocalization->FormatValues(jsKeys, getter_AddRefs(promise));
if (aRv.Failed()) {
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
@ -227,7 +230,7 @@ already_AddRefed<Promise> Localization::FormatMessages(
RefPtr<Promise> promise;
aRv = mLocalization->FormatMessages(jsKeys, getter_AddRefs(promise));
if (aRv.Failed()) {
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
@ -302,3 +305,33 @@ already_AddRefed<Promise> Localization::MaybeWrapPromise(
aInnerPromise->AppendNativeHandler(resolver);
return docPromise.forget();
}
void Localization::ConvertL10nArgsToJSValue(
JSContext* aCx, const L10nArgs& aArgs, JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv) {
// This method uses a temporary dictionary to automate
// converting an IDL Record to a JS Value via a dictionary.
//
// Once we get ToJSValue for Record, we'll switch to that.
L10nArgsHelperDict helperDict;
for (auto& entry : aArgs.Entries()) {
L10nArgs::EntryType* newEntry =
helperDict.mArgs.Entries().AppendElement(fallible);
if (!newEntry) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
newEntry->mKey = entry.mKey;
newEntry->mValue = entry.mValue;
}
JS::Rooted<JS::Value> jsVal(aCx);
if (!ToJSValue(aCx, helperDict, &jsVal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
JS::Rooted<JSObject*> jsObj(aCx, &jsVal.toObject());
if (!JS_GetProperty(aCx, jsObj, "args", aRetVal)) {
aRv.Throw(NS_ERROR_UNEXPECTED);
return;
}
}

View File

@ -16,6 +16,8 @@ using namespace mozilla::dom;
namespace mozilla {
namespace intl {
typedef Record<nsString, Nullable<OwningStringOrDouble>> L10nArgs;
class Localization : public nsIObserver,
public nsSupportsWeakReference,
public nsWrapperCache {
@ -49,9 +51,9 @@ class Localization : public nsIObserver,
uint32_t RemoveResourceIds(const nsTArray<nsString>& aResourceIds);
already_AddRefed<Promise> FormatValue(
JSContext* aCx, const nsAString& aId,
const Optional<JS::Handle<JSObject*>>& aArgs, ErrorResult& aRv);
already_AddRefed<Promise> FormatValue(JSContext* aCx, const nsAString& aId,
const Optional<L10nArgs>& aArgs,
ErrorResult& aRv);
already_AddRefed<Promise> FormatValues(JSContext* aCx,
const Sequence<L10nKey>& aKeys,
@ -66,6 +68,9 @@ class Localization : public nsIObserver,
void RegisterObservers();
void OnChange();
already_AddRefed<Promise> MaybeWrapPromise(Promise* aInnerPromise);
void ConvertL10nArgsToJSValue(JSContext* aCx, const L10nArgs& aArgs,
JS::MutableHandle<JS::Value> aRetVal,
ErrorResult& aRv);
nsCOMPtr<nsIGlobalObject> mGlobal;
nsCOMPtr<mozILocalization> mLocalization;