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();
33 const auto& e
= entry
.GetAsL10nIdArgs();
34 ffi::L10nKey
* key
= l10nKeys
.AppendElement();
36 if (!e
.mArgs
.IsNull()) {
37 FluentBundle::ConvertArgs(e
.mArgs
.Value(), key
->args
);
45 [[nodiscard
]] static bool ConvertToAttributeNameValue(
46 const nsTArray
<ffi::L10nAttribute
>& aAttributes
,
47 FallibleTArray
<AttributeNameValue
>& aValues
) {
48 if (!aValues
.SetCapacity(aAttributes
.Length(), fallible
)) {
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
;
60 [[nodiscard
]] static bool ConvertToL10nMessages(
61 const nsTArray
<ffi::OptionalL10nMessage
>& aMessages
,
62 nsTArray
<Nullable
<L10nMessage
>>& aOut
) {
63 if (!aOut
.SetCapacity(aMessages
.Length(), fallible
)) {
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
) {
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
)) {
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
)
98 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK(Localization
, mGlobal
)
101 already_AddRefed
<Localization
> Localization::Create(
102 const nsTArray
<nsCString
>& aResourceIds
, bool aIsSync
) {
103 return MakeAndAddRef
<Localization
>(aResourceIds
, aIsSync
);
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
));
120 Localization::Localization(const nsTArray
<ffi::GeckoResourceId
>& aResIds
,
122 ffi::localization_new(&aResIds
, aIsSync
, nullptr, getter_AddRefs(mRaw
));
127 Localization::Localization(nsIGlobalObject
* aGlobal
,
128 const nsTArray
<nsCString
>& aResIds
, bool aIsSync
)
130 nsTArray
<ffi::GeckoResourceId
> resourceIds
{
131 L10nRegistry::ResourceIdsToFFI(aResIds
)};
132 ffi::localization_new(&resourceIds
, aIsSync
, nullptr, getter_AddRefs(mRaw
));
137 Localization::Localization(nsIGlobalObject
* aGlobal
, bool aIsSync
)
139 nsTArray
<ffi::GeckoResourceId
> resIds
;
140 ffi::localization_new(&resIds
, aIsSync
, nullptr, getter_AddRefs(mRaw
));
145 Localization::Localization(nsIGlobalObject
* aGlobal
, bool aIsSync
,
146 const ffi::LocalizationRc
* aRaw
)
147 : mGlobal(aGlobal
), mRaw(aRaw
) {
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()) {
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
));
175 aRv
.ThrowInvalidStateError(
176 "Failed to create the Localization. Check the locales arguments.");
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;
193 Localization::Observe(nsISupports
* aSubject
, const char* aTopic
,
194 const char16_t
* aData
) {
195 if (!strcmp(aTopic
, INTL_APP_LOCALES_CHANGED
)) {
198 MOZ_ASSERT(!strcmp("nsPref:changed", aTopic
));
199 nsDependentString
pref(aData
);
200 if (pref
.EqualsLiteral(L10N_PSEUDO_PREF
)) {
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();
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
);
277 if (MaybeReportErrorsToGecko(*aErrors
, rv
,
278 promise
->GetParentObject())) {
279 promise
->MaybeReject(std::move(rv
));
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
);
297 ffi::localization_format_values(
298 mRaw
.get(), &l10nKeys
, promise
,
299 // callback function which will be invoked by the rust code, passing the
301 [](const Promise
* aPromise
, const nsTArray
<nsCString
>* aValues
,
302 const nsTArray
<nsCString
>* aErrors
) {
303 Promise
* promise
= const_cast<Promise
*>(aPromise
);
306 if (MaybeReportErrorsToGecko(*aErrors
, rv
,
307 promise
->GetParentObject())) {
308 promise
->MaybeReject(std::move(rv
));
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
);
326 ffi::localization_format_messages(
327 mRaw
.get(), &l10nKeys
, promise
,
328 // callback function which will be invoked by the rust code, passing the
330 [](const Promise
* aPromise
,
331 const nsTArray
<ffi::OptionalL10nMessage
>* aRaw
,
332 const nsTArray
<nsCString
>* aErrors
) {
333 Promise
* promise
= const_cast<Promise
*>(aPromise
);
336 if (MaybeReportErrorsToGecko(*aErrors
, rv
,
337 promise
->GetParentObject())) {
338 promise
->MaybeReject(std::move(rv
));
340 nsTArray
<Nullable
<L10nMessage
>> messages
;
341 if (!ConvertToL10nMessages(*aRaw
, messages
)) {
342 promise
->MaybeReject(NS_ERROR_OUT_OF_MEMORY
);
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
,
367 MaybeReportErrorsToGecko(errors
, aRv
, GetParentObject());
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();
387 const auto& e
= entry
.GetAsL10nIdArgs();
388 nsTArray
<ffi::L10nArg
> l10nArgs
;
389 ffi::L10nKey
* key
= l10nKeys
.AppendElement();
391 if (!e
.mArgs
.IsNull()) {
392 FluentBundle::ConvertArgs(e
.mArgs
.Value(), key
->args
);
397 bool rv
= ffi::localization_format_values_sync(mRaw
.get(), &l10nKeys
,
401 MaybeReportErrorsToGecko(errors
, aRv
, GetParentObject());
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();
421 const auto& e
= entry
.GetAsL10nIdArgs();
422 nsTArray
<ffi::L10nArg
> l10nArgs
;
423 ffi::L10nKey
* key
= l10nKeys
.AppendElement();
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
,
437 return aRv
.ThrowInvalidStateError(
438 "Can't use formatMessagesSync when state is async.");
440 MaybeReportErrorsToGecko(errors
, aRv
, GetParentObject());
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
{
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
;
467 virtual ~PromiseResolver();
469 RefPtr
<Promise
> mPromise
;
472 NS_INTERFACE_MAP_BEGIN(PromiseResolver
)
473 NS_INTERFACE_MAP_ENTRY(nsISupports
)
476 NS_IMPL_ADDREF(PromiseResolver
)
477 NS_IMPL_RELEASE(PromiseResolver
)
479 void PromiseResolver::ResolvedCallback(JSContext
* aCx
,
480 JS::Handle
<JS::Value
> aValue
,
482 mPromise
->MaybeResolveWithClone(aCx
, aValue
);
485 void PromiseResolver::RejectedCallback(JSContext
* aCx
,
486 JS::Handle
<JS::Value
> aValue
,
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())) {
519 auto resolver
= MakeRefPtr
<PromiseResolver
>(docPromise
);
520 aInnerPromise
->AppendNativeHandler(resolver
);
521 return docPromise
.forget();