Backed out changeset 5669c20b0617 (bug 1903669) for causing crashtest failures on...
[gecko.git] / intl / l10n / Localization.cpp
blob0d6f1f98822879390a551e3bfe7533574eeaf02f
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "Localization.h"
8 #include "nsIObserverService.h"
9 #include "mozilla/BasePrincipal.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/Services.h"
12 #include "mozilla/dom/PromiseNativeHandler.h"
14 #define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed"
15 #define L10N_PSEUDO_PREF "intl.l10n.pseudo"
17 using namespace mozilla;
18 using namespace mozilla::dom;
19 using namespace mozilla::intl;
21 static const char* kObservedPrefs[] = {L10N_PSEUDO_PREF, nullptr};
23 static nsTArray<ffi::L10nKey> ConvertFromL10nKeys(
24 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys) {
25 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
27 for (const auto& entry : aKeys) {
28 if (entry.IsUTF8String()) {
29 const auto& id = entry.GetAsUTF8String();
30 ffi::L10nKey* key = l10nKeys.AppendElement();
31 key->id = &id;
32 } else {
33 const auto& e = entry.GetAsL10nIdArgs();
34 ffi::L10nKey* key = l10nKeys.AppendElement();
35 key->id = &e.mId;
36 if (!e.mArgs.IsNull()) {
37 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
42 return l10nKeys;
45 [[nodiscard]] static bool ConvertToAttributeNameValue(
46 const nsTArray<ffi::L10nAttribute>& aAttributes,
47 FallibleTArray<AttributeNameValue>& aValues) {
48 if (!aValues.SetCapacity(aAttributes.Length(), fallible)) {
49 return false;
51 for (const auto& attr : aAttributes) {
52 auto* cvtAttr = aValues.AppendElement(fallible);
53 MOZ_ASSERT(cvtAttr, "SetCapacity didn't set enough capacity somehow?");
54 cvtAttr->mName = attr.name;
55 cvtAttr->mValue = attr.value;
57 return true;
60 [[nodiscard]] static bool ConvertToL10nMessages(
61 const nsTArray<ffi::OptionalL10nMessage>& aMessages,
62 nsTArray<Nullable<L10nMessage>>& aOut) {
63 if (!aOut.SetCapacity(aMessages.Length(), fallible)) {
64 return false;
67 for (const auto& entry : aMessages) {
68 Nullable<L10nMessage>* msg = aOut.AppendElement(fallible);
69 MOZ_ASSERT(msg, "SetCapacity didn't set enough capacity somehow?");
71 if (!entry.is_present) {
72 continue;
75 L10nMessage& m = msg->SetValue();
76 if (!entry.message.value.IsVoid()) {
77 m.mValue = entry.message.value;
79 if (!entry.message.attributes.IsEmpty()) {
80 auto& value = m.mAttributes.SetValue();
81 if (!ConvertToAttributeNameValue(entry.message.attributes, value)) {
82 return false;
87 return true;
90 NS_IMPL_CYCLE_COLLECTING_ADDREF(Localization)
91 NS_IMPL_CYCLE_COLLECTING_RELEASE(Localization)
92 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization)
93 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
94 NS_INTERFACE_MAP_ENTRY(nsIObserver)
95 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
96 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
97 NS_INTERFACE_MAP_END
98 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK(Localization, mGlobal)
100 /* static */
101 already_AddRefed<Localization> Localization::Create(
102 const nsTArray<nsCString>& aResourceIds, bool aIsSync) {
103 return MakeAndAddRef<Localization>(aResourceIds, aIsSync);
106 /* static */
107 already_AddRefed<Localization> Localization::Create(
108 const nsTArray<ffi::GeckoResourceId>& aResourceIds, bool aIsSync) {
109 return MakeAndAddRef<Localization>(aResourceIds, aIsSync);
112 Localization::Localization(const nsTArray<nsCString>& aResIds, bool aIsSync) {
113 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResIds)};
114 ffi::localization_new(&ffiResourceIds, aIsSync, nullptr,
115 getter_AddRefs(mRaw));
117 RegisterObservers();
120 Localization::Localization(const nsTArray<ffi::GeckoResourceId>& aResIds,
121 bool aIsSync) {
122 ffi::localization_new(&aResIds, aIsSync, nullptr, getter_AddRefs(mRaw));
124 RegisterObservers();
127 Localization::Localization(nsIGlobalObject* aGlobal,
128 const nsTArray<nsCString>& aResIds, bool aIsSync)
129 : mGlobal(aGlobal) {
130 nsTArray<ffi::GeckoResourceId> resourceIds{
131 L10nRegistry::ResourceIdsToFFI(aResIds)};
132 ffi::localization_new(&resourceIds, aIsSync, nullptr, getter_AddRefs(mRaw));
134 RegisterObservers();
137 Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync)
138 : mGlobal(aGlobal) {
139 nsTArray<ffi::GeckoResourceId> resIds;
140 ffi::localization_new(&resIds, aIsSync, nullptr, getter_AddRefs(mRaw));
142 RegisterObservers();
145 Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync,
146 const ffi::LocalizationRc* aRaw)
147 : mGlobal(aGlobal), mRaw(aRaw) {
148 RegisterObservers();
151 already_AddRefed<Localization> Localization::Constructor(
152 const GlobalObject& aGlobal,
153 const Sequence<OwningUTF8StringOrResourceId>& aResourceIds, bool aIsSync,
154 const Optional<NonNull<L10nRegistry>>& aRegistry,
155 const Optional<Sequence<nsCString>>& aLocales, ErrorResult& aRv) {
156 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)};
157 Maybe<nsTArray<nsCString>> locales;
159 if (aLocales.WasPassed()) {
160 locales.emplace();
161 locales->SetCapacity(aLocales.Value().Length());
162 for (const auto& locale : aLocales.Value()) {
163 locales->AppendElement(locale);
167 RefPtr<const ffi::LocalizationRc> raw;
169 bool result = ffi::localization_new_with_locales(
170 &ffiResourceIds, aIsSync,
171 aRegistry.WasPassed() ? aRegistry.Value().Raw() : nullptr,
172 locales.ptrOr(nullptr), getter_AddRefs(raw));
174 if (!result) {
175 aRv.ThrowInvalidStateError(
176 "Failed to create the Localization. Check the locales arguments.");
177 return nullptr;
180 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
182 return do_AddRef(new Localization(global, aIsSync, raw));
185 JSObject* Localization::WrapObject(JSContext* aCx,
186 JS::Handle<JSObject*> aGivenProto) {
187 return Localization_Binding::Wrap(aCx, this, aGivenProto);
190 Localization::~Localization() = default;
192 NS_IMETHODIMP
193 Localization::Observe(nsISupports* aSubject, const char* aTopic,
194 const char16_t* aData) {
195 if (!strcmp(aTopic, INTL_APP_LOCALES_CHANGED)) {
196 OnChange();
197 } else {
198 MOZ_ASSERT(!strcmp("nsPref:changed", aTopic));
199 nsDependentString pref(aData);
200 if (pref.EqualsLiteral(L10N_PSEUDO_PREF)) {
201 OnChange();
205 return NS_OK;
208 void Localization::RegisterObservers() {
209 DebugOnly<nsresult> rv = Preferences::AddWeakObservers(this, kObservedPrefs);
210 MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
212 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
213 if (obs) {
214 obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true);
218 void Localization::OnChange() { ffi::localization_on_change(mRaw.get()); }
220 void Localization::AddResourceId(const ffi::GeckoResourceId& aResourceId) {
221 ffi::localization_add_res_id(mRaw.get(), &aResourceId);
223 void Localization::AddResourceId(const nsCString& aResourceId) {
224 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)};
225 AddResourceId(ffiResourceId);
227 void Localization::AddResourceId(
228 const dom::OwningUTF8StringOrResourceId& aResourceId) {
229 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)};
230 AddResourceId(ffiResourceId);
233 uint32_t Localization::RemoveResourceId(
234 const ffi::GeckoResourceId& aResourceId) {
235 return ffi::localization_remove_res_id(mRaw.get(), &aResourceId);
237 uint32_t Localization::RemoveResourceId(const nsCString& aResourceId) {
238 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)};
239 return RemoveResourceId(ffiResourceId);
241 uint32_t Localization::RemoveResourceId(
242 const dom::OwningUTF8StringOrResourceId& aResourceId) {
243 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)};
244 return RemoveResourceId(ffiResourceId);
247 void Localization::AddResourceIds(
248 const nsTArray<dom::OwningUTF8StringOrResourceId>& aResourceIds) {
249 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)};
250 ffi::localization_add_res_ids(mRaw.get(), &ffiResourceIds);
253 uint32_t Localization::RemoveResourceIds(
254 const nsTArray<dom::OwningUTF8StringOrResourceId>& aResourceIds) {
255 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)};
256 return ffi::localization_remove_res_ids(mRaw.get(), &ffiResourceIds);
259 already_AddRefed<Promise> Localization::FormatValue(
260 const nsACString& aId, const Optional<L10nArgs>& aArgs, ErrorResult& aRv) {
261 nsTArray<ffi::L10nArg> l10nArgs;
262 nsTArray<nsCString> errors;
264 if (aArgs.WasPassed()) {
265 const L10nArgs& args = aArgs.Value();
266 FluentBundle::ConvertArgs(args, l10nArgs);
268 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
270 ffi::localization_format_value(
271 mRaw.get(), &aId, &l10nArgs, promise,
272 [](const Promise* aPromise, const nsACString* aValue,
273 const nsTArray<nsCString>* aErrors) {
274 Promise* promise = const_cast<Promise*>(aPromise);
276 ErrorResult rv;
277 if (MaybeReportErrorsToGecko(*aErrors, rv,
278 promise->GetParentObject())) {
279 promise->MaybeReject(std::move(rv));
280 } else {
281 promise->MaybeResolve(aValue);
285 return MaybeWrapPromise(promise);
288 already_AddRefed<Promise> Localization::FormatValues(
289 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) {
290 nsTArray<ffi::L10nKey> l10nKeys = ConvertFromL10nKeys(aKeys);
292 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
293 if (aRv.Failed()) {
294 return nullptr;
297 ffi::localization_format_values(
298 mRaw.get(), &l10nKeys, promise,
299 // callback function which will be invoked by the rust code, passing the
300 // promise back in.
301 [](const Promise* aPromise, const nsTArray<nsCString>* aValues,
302 const nsTArray<nsCString>* aErrors) {
303 Promise* promise = const_cast<Promise*>(aPromise);
305 ErrorResult rv;
306 if (MaybeReportErrorsToGecko(*aErrors, rv,
307 promise->GetParentObject())) {
308 promise->MaybeReject(std::move(rv));
309 } else {
310 promise->MaybeResolve(*aValues);
314 return MaybeWrapPromise(promise);
317 already_AddRefed<Promise> Localization::FormatMessages(
318 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) {
319 auto l10nKeys = ConvertFromL10nKeys(aKeys);
321 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
322 if (aRv.Failed()) {
323 return nullptr;
326 ffi::localization_format_messages(
327 mRaw.get(), &l10nKeys, promise,
328 // callback function which will be invoked by the rust code, passing the
329 // promise back in.
330 [](const Promise* aPromise,
331 const nsTArray<ffi::OptionalL10nMessage>* aRaw,
332 const nsTArray<nsCString>* aErrors) {
333 Promise* promise = const_cast<Promise*>(aPromise);
335 ErrorResult rv;
336 if (MaybeReportErrorsToGecko(*aErrors, rv,
337 promise->GetParentObject())) {
338 promise->MaybeReject(std::move(rv));
339 } else {
340 nsTArray<Nullable<L10nMessage>> messages;
341 if (!ConvertToL10nMessages(*aRaw, messages)) {
342 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
343 } else {
344 promise->MaybeResolve(std::move(messages));
349 return MaybeWrapPromise(promise);
352 void Localization::FormatValueSync(const nsACString& aId,
353 const Optional<L10nArgs>& aArgs,
354 nsACString& aRetVal, ErrorResult& aRv) {
355 nsTArray<ffi::L10nArg> l10nArgs;
356 nsTArray<nsCString> errors;
358 if (aArgs.WasPassed()) {
359 const L10nArgs& args = aArgs.Value();
360 FluentBundle::ConvertArgs(args, l10nArgs);
363 bool rv = ffi::localization_format_value_sync(mRaw.get(), &aId, &l10nArgs,
364 &aRetVal, &errors);
366 if (rv) {
367 MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
368 } else {
369 aRv.ThrowInvalidStateError(
370 "Can't use formatValueSync when state is async.");
374 void Localization::FormatValuesSync(
375 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
376 nsTArray<nsCString>& aRetVal, ErrorResult& aRv) {
377 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
378 nsTArray<nsCString> errors;
380 for (const auto& entry : aKeys) {
381 if (entry.IsUTF8String()) {
382 const auto& id = entry.GetAsUTF8String();
383 nsTArray<ffi::L10nArg> l10nArgs;
384 ffi::L10nKey* key = l10nKeys.AppendElement();
385 key->id = &id;
386 } else {
387 const auto& e = entry.GetAsL10nIdArgs();
388 nsTArray<ffi::L10nArg> l10nArgs;
389 ffi::L10nKey* key = l10nKeys.AppendElement();
390 key->id = &e.mId;
391 if (!e.mArgs.IsNull()) {
392 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
397 bool rv = ffi::localization_format_values_sync(mRaw.get(), &l10nKeys,
398 &aRetVal, &errors);
400 if (rv) {
401 MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
402 } else {
403 aRv.ThrowInvalidStateError(
404 "Can't use formatValuesSync when state is async.");
408 void Localization::FormatMessagesSync(
409 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys,
410 nsTArray<Nullable<L10nMessage>>& aRetVal, ErrorResult& aRv) {
411 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length());
412 nsTArray<nsCString> errors;
414 for (const auto& entry : aKeys) {
415 if (entry.IsUTF8String()) {
416 const auto& id = entry.GetAsUTF8String();
417 nsTArray<ffi::L10nArg> l10nArgs;
418 ffi::L10nKey* key = l10nKeys.AppendElement();
419 key->id = &id;
420 } else {
421 const auto& e = entry.GetAsL10nIdArgs();
422 nsTArray<ffi::L10nArg> l10nArgs;
423 ffi::L10nKey* key = l10nKeys.AppendElement();
424 key->id = &e.mId;
425 if (!e.mArgs.IsNull()) {
426 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args);
431 nsTArray<ffi::OptionalL10nMessage> result(l10nKeys.Length());
433 bool rv = ffi::localization_format_messages_sync(mRaw.get(), &l10nKeys,
434 &result, &errors);
436 if (!rv) {
437 return aRv.ThrowInvalidStateError(
438 "Can't use formatMessagesSync when state is async.");
440 MaybeReportErrorsToGecko(errors, aRv, GetParentObject());
441 if (aRv.Failed()) {
442 return;
444 if (!ConvertToL10nMessages(result, aRetVal)) {
445 return aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
449 void Localization::SetAsync() { ffi::localization_set_async(mRaw.get()); }
450 bool Localization::IsSync() { return ffi::localization_is_sync(mRaw.get()); }
453 * PromiseResolver is a PromiseNativeHandler used
454 * by MaybeWrapPromise method.
456 class PromiseResolver final : public PromiseNativeHandler {
457 public:
458 NS_DECL_ISUPPORTS
460 explicit PromiseResolver(Promise* aPromise) : mPromise(aPromise) {}
461 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
462 ErrorResult& aRv) override;
463 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
464 ErrorResult& aRv) override;
466 protected:
467 virtual ~PromiseResolver();
469 RefPtr<Promise> mPromise;
472 NS_INTERFACE_MAP_BEGIN(PromiseResolver)
473 NS_INTERFACE_MAP_ENTRY(nsISupports)
474 NS_INTERFACE_MAP_END
476 NS_IMPL_ADDREF(PromiseResolver)
477 NS_IMPL_RELEASE(PromiseResolver)
479 void PromiseResolver::ResolvedCallback(JSContext* aCx,
480 JS::Handle<JS::Value> aValue,
481 ErrorResult& aRv) {
482 mPromise->MaybeResolveWithClone(aCx, aValue);
485 void PromiseResolver::RejectedCallback(JSContext* aCx,
486 JS::Handle<JS::Value> aValue,
487 ErrorResult& aRv) {
488 mPromise->MaybeRejectWithClone(aCx, aValue);
491 PromiseResolver::~PromiseResolver() { mPromise = nullptr; }
494 * MaybeWrapPromise is a helper method used by Localization
495 * API methods to clone the value returned by a promise
496 * into a new context.
498 * This allows for a promise from a privileged context
499 * to be returned into an unprivileged document.
501 * This method is only used for promises that carry values.
503 already_AddRefed<Promise> Localization::MaybeWrapPromise(
504 Promise* aInnerPromise) {
505 MOZ_ASSERT(aInnerPromise->State() == Promise::PromiseState::Pending);
506 // For system principal we don't need to wrap the
507 // result promise at all.
508 nsIPrincipal* principal = mGlobal->PrincipalOrNull();
509 if (principal && principal->IsSystemPrincipal()) {
510 return do_AddRef(aInnerPromise);
513 IgnoredErrorResult result;
514 RefPtr<Promise> docPromise = Promise::Create(mGlobal, result);
515 if (NS_WARN_IF(result.Failed())) {
516 return nullptr;
519 auto resolver = MakeRefPtr<PromiseResolver>(docPromise);
520 aInnerPromise->AppendNativeHandler(resolver);
521 return docPromise.forget();