Localization.cpp (18323B)
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/. */ 6 7 #include "Localization.h" 8 #include "nsIObserverService.h" 9 #include "xpcpublic.h" 10 #include "mozilla/BasePrincipal.h" 11 #include "mozilla/Preferences.h" 12 #include "mozilla/Services.h" 13 #include "mozilla/dom/Document.h" 14 #include "mozilla/dom/PromiseNativeHandler.h" 15 16 #define INTL_APP_LOCALES_CHANGED "intl:app-locales-changed" 17 #define L10N_PSEUDO_PREF "intl.l10n.pseudo" 18 19 using namespace mozilla; 20 using namespace mozilla::dom; 21 using namespace mozilla::intl; 22 23 static const char* kObservedPrefs[] = {L10N_PSEUDO_PREF, nullptr}; 24 25 static nsTArray<ffi::L10nKey> ConvertFromL10nKeys( 26 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys) { 27 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length()); 28 29 for (const auto& entry : aKeys) { 30 if (entry.IsUTF8String()) { 31 const auto& id = entry.GetAsUTF8String(); 32 ffi::L10nKey* key = l10nKeys.AppendElement(); 33 key->id = &id; 34 } else { 35 const auto& e = entry.GetAsL10nIdArgs(); 36 ffi::L10nKey* key = l10nKeys.AppendElement(); 37 key->id = &e.mId; 38 if (!e.mArgs.IsNull()) { 39 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); 40 } 41 } 42 } 43 44 return l10nKeys; 45 } 46 47 [[nodiscard]] static bool ConvertToAttributeNameValue( 48 const nsTArray<ffi::L10nAttribute>& aAttributes, 49 FallibleTArray<AttributeNameValue>& aValues) { 50 if (!aValues.SetCapacity(aAttributes.Length(), fallible)) { 51 return false; 52 } 53 for (const auto& attr : aAttributes) { 54 auto* cvtAttr = aValues.AppendElement(fallible); 55 MOZ_ASSERT(cvtAttr, "SetCapacity didn't set enough capacity somehow?"); 56 cvtAttr->mName = attr.name; 57 cvtAttr->mValue = attr.value; 58 } 59 return true; 60 } 61 62 [[nodiscard]] static bool ConvertToL10nMessages( 63 const nsTArray<ffi::OptionalL10nMessage>& aMessages, 64 nsTArray<Nullable<L10nMessage>>& aOut) { 65 if (!aOut.SetCapacity(aMessages.Length(), fallible)) { 66 return false; 67 } 68 69 for (const auto& entry : aMessages) { 70 Nullable<L10nMessage>* msg = aOut.AppendElement(fallible); 71 MOZ_ASSERT(msg, "SetCapacity didn't set enough capacity somehow?"); 72 73 if (!entry.is_present) { 74 continue; 75 } 76 77 L10nMessage& m = msg->SetValue(); 78 if (!entry.message.value.IsVoid()) { 79 m.mValue = entry.message.value; 80 } 81 if (!entry.message.attributes.IsEmpty()) { 82 auto& value = m.mAttributes.SetValue(); 83 if (!ConvertToAttributeNameValue(entry.message.attributes, value)) { 84 return false; 85 } 86 } 87 } 88 89 return true; 90 } 91 92 NS_IMPL_CYCLE_COLLECTING_ADDREF(Localization) 93 NS_IMPL_CYCLE_COLLECTING_RELEASE(Localization) 94 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Localization) 95 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 96 NS_INTERFACE_MAP_ENTRY(nsIObserver) 97 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) 98 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) 99 NS_INTERFACE_MAP_END 100 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_WEAK(Localization, mGlobal) 101 102 /* static */ 103 already_AddRefed<Localization> Localization::Create( 104 const nsTArray<nsCString>& aResourceIds, bool aIsSync) { 105 return MakeAndAddRef<Localization>(aResourceIds, aIsSync); 106 } 107 108 /* static */ 109 already_AddRefed<Localization> Localization::Create( 110 const nsTArray<nsCString>& aResourceIds, bool aIsSync, 111 const nsTArray<nsCString>& aLocales) { 112 return MakeAndAddRef<Localization>(aResourceIds, aIsSync, aLocales); 113 } 114 115 /* static */ 116 already_AddRefed<Localization> Localization::Create( 117 const nsTArray<ffi::GeckoResourceId>& aResourceIds, bool aIsSync) { 118 return MakeAndAddRef<Localization>(aResourceIds, aIsSync); 119 } 120 121 Localization::Localization(const nsTArray<nsCString>& aResIds, bool aIsSync) { 122 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResIds)}; 123 ffi::localization_new(&ffiResourceIds, aIsSync, nullptr, 124 getter_AddRefs(mRaw)); 125 126 RegisterObservers(); 127 } 128 129 Localization::Localization(const nsTArray<nsCString>& aResIds, bool aIsSync, 130 const nsTArray<nsCString>& aLocales) { 131 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResIds)}; 132 ffi::localization_new_with_locales(&ffiResourceIds, aIsSync, nullptr, 133 &aLocales, getter_AddRefs(mRaw)); 134 } 135 136 Localization::Localization(const nsTArray<ffi::GeckoResourceId>& aResIds, 137 bool aIsSync) { 138 ffi::localization_new(&aResIds, aIsSync, nullptr, getter_AddRefs(mRaw)); 139 140 RegisterObservers(); 141 } 142 143 Localization::Localization(nsIGlobalObject* aGlobal, 144 const nsTArray<nsCString>& aResIds, bool aIsSync) 145 : mGlobal(aGlobal) { 146 nsTArray<ffi::GeckoResourceId> resourceIds{ 147 L10nRegistry::ResourceIdsToFFI(aResIds)}; 148 ffi::localization_new(&resourceIds, aIsSync, nullptr, getter_AddRefs(mRaw)); 149 150 RegisterObservers(); 151 } 152 153 Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync) 154 : mGlobal(aGlobal) { 155 nsTArray<ffi::GeckoResourceId> resIds; 156 ffi::localization_new(&resIds, aIsSync, nullptr, getter_AddRefs(mRaw)); 157 158 RegisterObservers(); 159 } 160 161 Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync, 162 const ffi::LocalizationRc* aRaw) 163 : mGlobal(aGlobal), mRaw(aRaw) { 164 RegisterObservers(); 165 } 166 167 Localization::Localization(nsIGlobalObject* aGlobal, bool aIsSync, 168 const nsTArray<nsCString>& aLocales) 169 : mGlobal(aGlobal) { 170 nsTArray<ffi::GeckoResourceId> resIds; 171 ffi::localization_new_with_locales(&resIds, aIsSync, nullptr, &aLocales, 172 getter_AddRefs(mRaw)); 173 } 174 175 /* static */ 176 bool Localization::IsAPIEnabled(JSContext* aCx, JSObject* aObject) { 177 JS::Rooted<JSObject*> obj(aCx, aObject); 178 return Document::DocumentSupportsL10n(aCx, obj) || 179 IsChromeOrUAWidget(aCx, obj); 180 } 181 182 already_AddRefed<Localization> Localization::Constructor( 183 const GlobalObject& aGlobal, 184 const Sequence<OwningUTF8StringOrResourceId>& aResourceIds, bool aIsSync, 185 const Optional<NonNull<L10nRegistry>>& aRegistry, 186 const Optional<Sequence<nsCString>>& aLocales, ErrorResult& aRv) { 187 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; 188 Maybe<nsTArray<nsCString>> locales; 189 190 if (aLocales.WasPassed()) { 191 locales.emplace(); 192 locales->SetCapacity(aLocales.Value().Length()); 193 for (const auto& locale : aLocales.Value()) { 194 locales->AppendElement(locale); 195 } 196 } 197 198 RefPtr<const ffi::LocalizationRc> raw; 199 200 bool result = ffi::localization_new_with_locales( 201 &ffiResourceIds, aIsSync, 202 aRegistry.WasPassed() ? aRegistry.Value().Raw() : nullptr, 203 locales.ptrOr(nullptr), getter_AddRefs(raw)); 204 205 if (!result) { 206 aRv.ThrowInvalidStateError( 207 "Failed to create the Localization. Check the locales arguments."); 208 return nullptr; 209 } 210 211 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 212 213 return do_AddRef(new Localization(global, aIsSync, raw)); 214 } 215 216 JSObject* Localization::WrapObject(JSContext* aCx, 217 JS::Handle<JSObject*> aGivenProto) { 218 return Localization_Binding::Wrap(aCx, this, aGivenProto); 219 } 220 221 Localization::~Localization() = default; 222 223 NS_IMETHODIMP 224 Localization::Observe(nsISupports* aSubject, const char* aTopic, 225 const char16_t* aData) { 226 if (!strcmp(aTopic, INTL_APP_LOCALES_CHANGED)) { 227 OnChange(); 228 } else { 229 MOZ_ASSERT(!strcmp("nsPref:changed", aTopic)); 230 nsDependentString pref(aData); 231 if (pref.EqualsLiteral(L10N_PSEUDO_PREF)) { 232 OnChange(); 233 } 234 } 235 236 return NS_OK; 237 } 238 239 void Localization::RegisterObservers() { 240 DebugOnly<nsresult> rv = Preferences::AddWeakObservers(this, kObservedPrefs); 241 MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed."); 242 243 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 244 if (obs) { 245 obs->AddObserver(this, INTL_APP_LOCALES_CHANGED, true); 246 } 247 } 248 249 void Localization::OnChange() { ffi::localization_on_change(mRaw.get()); } 250 251 void Localization::AddResourceId(const ffi::GeckoResourceId& aResourceId) { 252 ffi::localization_add_res_id(mRaw.get(), &aResourceId); 253 } 254 void Localization::AddResourceId(const nsCString& aResourceId) { 255 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; 256 AddResourceId(ffiResourceId); 257 } 258 void Localization::AddResourceId( 259 const dom::OwningUTF8StringOrResourceId& aResourceId) { 260 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; 261 AddResourceId(ffiResourceId); 262 } 263 264 uint32_t Localization::RemoveResourceId( 265 const ffi::GeckoResourceId& aResourceId) { 266 return ffi::localization_remove_res_id(mRaw.get(), &aResourceId); 267 } 268 uint32_t Localization::RemoveResourceId(const nsCString& aResourceId) { 269 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; 270 return RemoveResourceId(ffiResourceId); 271 } 272 uint32_t Localization::RemoveResourceId( 273 const dom::OwningUTF8StringOrResourceId& aResourceId) { 274 auto ffiResourceId{L10nRegistry::ResourceIdToFFI(aResourceId)}; 275 return RemoveResourceId(ffiResourceId); 276 } 277 278 void Localization::AddResourceIds( 279 const nsTArray<dom::OwningUTF8StringOrResourceId>& aResourceIds) { 280 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; 281 ffi::localization_add_res_ids(mRaw.get(), &ffiResourceIds); 282 } 283 284 uint32_t Localization::RemoveResourceIds( 285 const nsTArray<dom::OwningUTF8StringOrResourceId>& aResourceIds) { 286 auto ffiResourceIds{L10nRegistry::ResourceIdsToFFI(aResourceIds)}; 287 return ffi::localization_remove_res_ids(mRaw.get(), &ffiResourceIds); 288 } 289 290 already_AddRefed<Promise> Localization::FormatValue( 291 const nsACString& aId, const Optional<L10nArgs>& aArgs, ErrorResult& aRv) { 292 nsTArray<ffi::L10nArg> l10nArgs; 293 nsTArray<nsCString> errors; 294 295 if (aArgs.WasPassed()) { 296 const L10nArgs& args = aArgs.Value(); 297 FluentBundle::ConvertArgs(args, l10nArgs); 298 } 299 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); 300 301 ffi::localization_format_value( 302 mRaw.get(), &aId, &l10nArgs, promise, 303 [](const Promise* aPromise, const nsACString* aValue, 304 const nsTArray<nsCString>* aErrors) { 305 Promise* promise = const_cast<Promise*>(aPromise); 306 307 ErrorResult rv; 308 if (MaybeReportErrorsToGecko(*aErrors, rv, 309 promise->GetParentObject())) { 310 promise->MaybeReject(std::move(rv)); 311 } else { 312 promise->MaybeResolve(aValue); 313 } 314 }); 315 316 return MaybeWrapPromise(promise); 317 } 318 319 already_AddRefed<Promise> Localization::FormatValues( 320 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) { 321 nsTArray<ffi::L10nKey> l10nKeys = ConvertFromL10nKeys(aKeys); 322 323 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); 324 if (aRv.Failed()) { 325 return nullptr; 326 } 327 328 ffi::localization_format_values( 329 mRaw.get(), &l10nKeys, promise, 330 // callback function which will be invoked by the rust code, passing the 331 // promise back in. 332 [](const Promise* aPromise, const nsTArray<nsCString>* aValues, 333 const nsTArray<nsCString>* aErrors) { 334 Promise* promise = const_cast<Promise*>(aPromise); 335 336 ErrorResult rv; 337 if (MaybeReportErrorsToGecko(*aErrors, rv, 338 promise->GetParentObject())) { 339 promise->MaybeReject(std::move(rv)); 340 } else { 341 promise->MaybeResolve(*aValues); 342 } 343 }); 344 345 return MaybeWrapPromise(promise); 346 } 347 348 already_AddRefed<Promise> Localization::FormatMessages( 349 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, ErrorResult& aRv) { 350 auto l10nKeys = ConvertFromL10nKeys(aKeys); 351 352 RefPtr<Promise> promise = Promise::Create(mGlobal, aRv); 353 if (aRv.Failed()) { 354 return nullptr; 355 } 356 357 ffi::localization_format_messages( 358 mRaw.get(), &l10nKeys, promise, 359 // callback function which will be invoked by the rust code, passing the 360 // promise back in. 361 [](const Promise* aPromise, 362 const nsTArray<ffi::OptionalL10nMessage>* aRaw, 363 const nsTArray<nsCString>* aErrors) { 364 Promise* promise = const_cast<Promise*>(aPromise); 365 366 ErrorResult rv; 367 if (MaybeReportErrorsToGecko(*aErrors, rv, 368 promise->GetParentObject())) { 369 promise->MaybeReject(std::move(rv)); 370 } else { 371 nsTArray<Nullable<L10nMessage>> messages; 372 if (!ConvertToL10nMessages(*aRaw, messages)) { 373 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); 374 } else { 375 promise->MaybeResolve(std::move(messages)); 376 } 377 } 378 }); 379 380 return MaybeWrapPromise(promise); 381 } 382 383 void Localization::FormatValueSync(const nsACString& aId, 384 const Optional<L10nArgs>& aArgs, 385 nsACString& aRetVal, ErrorResult& aRv) { 386 nsTArray<ffi::L10nArg> l10nArgs; 387 nsTArray<nsCString> errors; 388 389 if (aArgs.WasPassed()) { 390 const L10nArgs& args = aArgs.Value(); 391 FluentBundle::ConvertArgs(args, l10nArgs); 392 } 393 394 bool rv = ffi::localization_format_value_sync(mRaw.get(), &aId, &l10nArgs, 395 &aRetVal, &errors); 396 397 if (rv) { 398 MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); 399 } else { 400 aRv.ThrowInvalidStateError( 401 "Can't use formatValueSync when state is async."); 402 } 403 } 404 405 void Localization::FormatValuesSync( 406 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, 407 nsTArray<nsCString>& aRetVal, ErrorResult& aRv) { 408 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length()); 409 nsTArray<nsCString> errors; 410 411 for (const auto& entry : aKeys) { 412 if (entry.IsUTF8String()) { 413 const auto& id = entry.GetAsUTF8String(); 414 nsTArray<ffi::L10nArg> l10nArgs; 415 ffi::L10nKey* key = l10nKeys.AppendElement(); 416 key->id = &id; 417 } else { 418 const auto& e = entry.GetAsL10nIdArgs(); 419 nsTArray<ffi::L10nArg> l10nArgs; 420 ffi::L10nKey* key = l10nKeys.AppendElement(); 421 key->id = &e.mId; 422 if (!e.mArgs.IsNull()) { 423 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); 424 } 425 } 426 } 427 428 bool rv = ffi::localization_format_values_sync(mRaw.get(), &l10nKeys, 429 &aRetVal, &errors); 430 431 if (rv) { 432 MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); 433 } else { 434 aRv.ThrowInvalidStateError( 435 "Can't use formatValuesSync when state is async."); 436 } 437 } 438 439 void Localization::FormatMessagesSync( 440 const Sequence<OwningUTF8StringOrL10nIdArgs>& aKeys, 441 nsTArray<Nullable<L10nMessage>>& aRetVal, ErrorResult& aRv) { 442 nsTArray<ffi::L10nKey> l10nKeys(aKeys.Length()); 443 nsTArray<nsCString> errors; 444 445 for (const auto& entry : aKeys) { 446 if (entry.IsUTF8String()) { 447 const auto& id = entry.GetAsUTF8String(); 448 nsTArray<ffi::L10nArg> l10nArgs; 449 ffi::L10nKey* key = l10nKeys.AppendElement(); 450 key->id = &id; 451 } else { 452 const auto& e = entry.GetAsL10nIdArgs(); 453 nsTArray<ffi::L10nArg> l10nArgs; 454 ffi::L10nKey* key = l10nKeys.AppendElement(); 455 key->id = &e.mId; 456 if (!e.mArgs.IsNull()) { 457 FluentBundle::ConvertArgs(e.mArgs.Value(), key->args); 458 } 459 } 460 } 461 462 nsTArray<ffi::OptionalL10nMessage> result(l10nKeys.Length()); 463 464 bool rv = ffi::localization_format_messages_sync(mRaw.get(), &l10nKeys, 465 &result, &errors); 466 467 if (!rv) { 468 return aRv.ThrowInvalidStateError( 469 "Can't use formatMessagesSync when state is async."); 470 } 471 MaybeReportErrorsToGecko(errors, aRv, GetParentObject()); 472 if (aRv.Failed()) { 473 return; 474 } 475 if (!ConvertToL10nMessages(result, aRetVal)) { 476 return aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 477 } 478 } 479 480 void Localization::SetAsync() { ffi::localization_set_async(mRaw.get()); } 481 bool Localization::IsSync() { return ffi::localization_is_sync(mRaw.get()); } 482 483 /** 484 * PromiseResolver is a PromiseNativeHandler used 485 * by MaybeWrapPromise method. 486 */ 487 class PromiseResolver final : public PromiseNativeHandler { 488 public: 489 NS_DECL_ISUPPORTS 490 491 explicit PromiseResolver(Promise* aPromise) : mPromise(aPromise) {} 492 void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 493 ErrorResult& aRv) override; 494 void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, 495 ErrorResult& aRv) override; 496 497 protected: 498 virtual ~PromiseResolver(); 499 500 RefPtr<Promise> mPromise; 501 }; 502 503 NS_INTERFACE_MAP_BEGIN(PromiseResolver) 504 NS_INTERFACE_MAP_ENTRY(nsISupports) 505 NS_INTERFACE_MAP_END 506 507 NS_IMPL_ADDREF(PromiseResolver) 508 NS_IMPL_RELEASE(PromiseResolver) 509 510 void PromiseResolver::ResolvedCallback(JSContext* aCx, 511 JS::Handle<JS::Value> aValue, 512 ErrorResult& aRv) { 513 mPromise->MaybeResolveWithClone(aCx, aValue); 514 } 515 516 void PromiseResolver::RejectedCallback(JSContext* aCx, 517 JS::Handle<JS::Value> aValue, 518 ErrorResult& aRv) { 519 mPromise->MaybeRejectWithClone(aCx, aValue); 520 } 521 522 PromiseResolver::~PromiseResolver() { mPromise = nullptr; } 523 524 /** 525 * MaybeWrapPromise is a helper method used by Localization 526 * API methods to clone the value returned by a promise 527 * into a new context. 528 * 529 * This allows for a promise from a privileged context 530 * to be returned into an unprivileged document. 531 * 532 * This method is only used for promises that carry values. 533 */ 534 already_AddRefed<Promise> Localization::MaybeWrapPromise( 535 Promise* aInnerPromise) { 536 MOZ_ASSERT(aInnerPromise->State() == Promise::PromiseState::Pending); 537 // For system principal we don't need to wrap the 538 // result promise at all. 539 nsIPrincipal* principal = mGlobal->PrincipalOrNull(); 540 if (principal && principal->IsSystemPrincipal()) { 541 return do_AddRef(aInnerPromise); 542 } 543 544 IgnoredErrorResult result; 545 RefPtr<Promise> docPromise = Promise::Create(mGlobal, result); 546 if (NS_WARN_IF(result.Failed())) { 547 return nullptr; 548 } 549 550 auto resolver = MakeRefPtr<PromiseResolver>(docPromise); 551 aInnerPromise->AppendNativeHandler(resolver); 552 return docPromise.forget(); 553 }