FluentBundle.cpp (16646B)
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 "FluentBundle.h" 8 #include "nsContentUtils.h" 9 #include "mozilla/dom/ToJSValue.h" 10 #include "mozilla/dom/UnionTypes.h" 11 #include "mozilla/intl/NumberFormat.h" 12 #include "mozilla/intl/DateTimeFormat.h" 13 #include "mozilla/intl/DateTimePatternGenerator.h" 14 #include "nsIInputStream.h" 15 #include "nsStringFwd.h" 16 #include "nsTArray.h" 17 #include "js/PropertyAndElement.h" // JS_DefineElement 18 19 using namespace mozilla::dom; 20 21 namespace mozilla { 22 namespace intl { 23 24 class SizeableUTF8Buffer { 25 public: 26 using CharType = char; 27 28 bool reserve(size_t size) { 29 mBuffer.reset(reinterpret_cast<CharType*>(malloc(size))); 30 mCapacity = size; 31 return true; 32 } 33 34 CharType* data() { return mBuffer.get(); } 35 36 size_t capacity() const { return mCapacity; } 37 38 void written(size_t amount) { mWritten = amount; } 39 40 size_t mWritten = 0; 41 size_t mCapacity = 0; 42 43 struct FreePolicy { 44 void operator()(const void* ptr) { free(const_cast<void*>(ptr)); } 45 }; 46 47 UniquePtr<CharType[], FreePolicy> mBuffer; 48 }; 49 50 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentPattern, mParent) 51 52 FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId) 53 : mId(aId), mParent(aParent) { 54 MOZ_COUNT_CTOR(FluentPattern); 55 } 56 FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId, 57 const nsACString& aAttrName) 58 : mId(aId), mAttrName(aAttrName), mParent(aParent) { 59 MOZ_COUNT_CTOR(FluentPattern); 60 } 61 62 JSObject* FluentPattern::WrapObject(JSContext* aCx, 63 JS::Handle<JSObject*> aGivenProto) { 64 return FluentPattern_Binding::Wrap(aCx, this, aGivenProto); 65 } 66 67 FluentPattern::~FluentPattern() { MOZ_COUNT_DTOR(FluentPattern); }; 68 69 /* FluentBundle */ 70 71 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentBundle, mParent) 72 73 FluentBundle::FluentBundle(nsISupports* aParent, 74 UniquePtr<ffi::FluentBundleRc> aRaw) 75 : mParent(aParent), mRaw(std::move(aRaw)) { 76 MOZ_COUNT_CTOR(FluentBundle); 77 } 78 79 already_AddRefed<FluentBundle> FluentBundle::Constructor( 80 const dom::GlobalObject& aGlobal, 81 const UTF8StringOrUTF8StringSequence& aLocales, 82 const dom::FluentBundleOptions& aOptions, ErrorResult& aRv) { 83 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); 84 if (!global) { 85 aRv.Throw(NS_ERROR_FAILURE); 86 return nullptr; 87 } 88 89 bool useIsolating = aOptions.mUseIsolating; 90 91 nsAutoCString pseudoStrategy; 92 if (aOptions.mPseudoStrategy.WasPassed()) { 93 pseudoStrategy = aOptions.mPseudoStrategy.Value(); 94 } 95 96 UniquePtr<ffi::FluentBundleRc> raw; 97 98 if (aLocales.IsUTF8String()) { 99 const nsACString& locale = aLocales.GetAsUTF8String(); 100 raw.reset( 101 ffi::fluent_bundle_new_single(&locale, useIsolating, &pseudoStrategy)); 102 } else { 103 const auto& locales = aLocales.GetAsUTF8StringSequence(); 104 raw.reset(ffi::fluent_bundle_new(locales.Elements(), locales.Length(), 105 useIsolating, &pseudoStrategy)); 106 } 107 108 if (!raw) { 109 aRv.ThrowInvalidStateError( 110 "Failed to create the FluentBundle. Check the " 111 "locales and pseudo strategy arguments."); 112 return nullptr; 113 } 114 115 return do_AddRef(new FluentBundle(global, std::move(raw))); 116 } 117 118 JSObject* FluentBundle::WrapObject(JSContext* aCx, 119 JS::Handle<JSObject*> aGivenProto) { 120 return FluentBundle_Binding::Wrap(aCx, this, aGivenProto); 121 } 122 123 FluentBundle::~FluentBundle() { MOZ_COUNT_DTOR(FluentBundle); }; 124 125 void FluentBundle::GetLocales(nsTArray<nsCString>& aLocales) { 126 fluent_bundle_get_locales(mRaw.get(), &aLocales); 127 } 128 129 void FluentBundle::AddResource( 130 FluentResource& aResource, 131 const dom::FluentBundleAddResourceOptions& aOptions) { 132 bool allowOverrides = aOptions.mAllowOverrides; 133 nsTArray<nsCString> errors; 134 135 fluent_bundle_add_resource(mRaw.get(), aResource.Raw(), allowOverrides, 136 &errors); 137 138 for (auto& err : errors) { 139 nsContentUtils::LogSimpleConsoleError(NS_ConvertUTF8toUTF16(err), "L10n"_ns, 140 false, true, 141 nsIScriptError::warningFlag); 142 } 143 } 144 145 bool FluentBundle::HasMessage(const nsACString& aId) { 146 return fluent_bundle_has_message(mRaw.get(), &aId); 147 } 148 149 void FluentBundle::GetMessage(const nsACString& aId, 150 Nullable<FluentMessage>& aRetVal) { 151 bool hasValue = false; 152 nsTArray<nsCString> attributes; 153 bool exists = 154 fluent_bundle_get_message(mRaw.get(), &aId, &hasValue, &attributes); 155 if (exists) { 156 FluentMessage& msg = aRetVal.SetValue(); 157 if (hasValue) { 158 msg.mValue = new FluentPattern(mParent, aId); 159 } 160 for (auto& name : attributes) { 161 auto newEntry = msg.mAttributes.Entries().AppendElement(fallible); 162 newEntry->mKey = name; 163 newEntry->mValue = new FluentPattern(mParent, aId, name); 164 } 165 } 166 } 167 168 bool extendJSArrayWithErrors(JSContext* aCx, JS::Handle<JSObject*> aErrors, 169 nsTArray<nsCString>& aInput) { 170 uint32_t length; 171 if (NS_WARN_IF(!JS::GetArrayLength(aCx, aErrors, &length))) { 172 return false; 173 } 174 175 for (auto& err : aInput) { 176 JS::Rooted<JS::Value> jsval(aCx); 177 if (!ToJSValue(aCx, NS_ConvertUTF8toUTF16(err), &jsval)) { 178 return false; 179 } 180 if (!JS_DefineElement(aCx, aErrors, length++, jsval, JSPROP_ENUMERATE)) { 181 return false; 182 } 183 } 184 return true; 185 } 186 187 /* static */ 188 void FluentBundle::ConvertArgs(const L10nArgs& aArgs, 189 nsTArray<ffi::L10nArg>& aRetVal) { 190 aRetVal.SetCapacity(aArgs.Entries().Length()); 191 for (const auto& entry : aArgs.Entries()) { 192 if (!entry.mValue.IsNull()) { 193 const auto& value = entry.mValue.Value(); 194 195 if (value.IsUTF8String()) { 196 aRetVal.AppendElement(ffi::L10nArg{ 197 &entry.mKey, 198 ffi::FluentArgument::String(&value.GetAsUTF8String())}); 199 } else { 200 aRetVal.AppendElement(ffi::L10nArg{ 201 &entry.mKey, ffi::FluentArgument::Double_(value.GetAsDouble())}); 202 } 203 } 204 } 205 } 206 207 void FluentBundle::FormatPattern(JSContext* aCx, const FluentPattern& aPattern, 208 const Nullable<L10nArgs>& aArgs, 209 const Optional<JS::Handle<JSObject*>>& aErrors, 210 nsACString& aRetVal, ErrorResult& aRv) { 211 nsTArray<ffi::L10nArg> l10nArgs; 212 213 if (!aArgs.IsNull()) { 214 const L10nArgs& args = aArgs.Value(); 215 ConvertArgs(args, l10nArgs); 216 } 217 218 nsTArray<nsCString> errors; 219 bool succeeded = fluent_bundle_format_pattern(mRaw.get(), &aPattern.mId, 220 &aPattern.mAttrName, &l10nArgs, 221 &aRetVal, &errors); 222 223 if (!succeeded) { 224 return aRv.ThrowInvalidStateError( 225 "Failed to format the FluentPattern. Likely the " 226 "pattern could not be retrieved from the bundle."); 227 } 228 229 if (aErrors.WasPassed()) { 230 if (!extendJSArrayWithErrors(aCx, aErrors.Value(), errors)) { 231 aRv.ThrowUnknownError("Failed to add errors to an error array."); 232 } 233 } 234 } 235 236 // FFI 237 238 extern "C" { 239 ffi::RawNumberFormatter* FluentBuiltInNumberFormatterCreate( 240 const nsCString* aLocale, const ffi::FluentNumberOptionsRaw* aOptions) { 241 NumberFormatOptions options; 242 switch (aOptions->style) { 243 case ffi::FluentNumberStyleRaw::Decimal: 244 break; 245 case ffi::FluentNumberStyleRaw::Currency: { 246 std::string currency = aOptions->currency.get(); 247 switch (aOptions->currency_display) { 248 case ffi::FluentNumberCurrencyDisplayStyleRaw::Symbol: 249 options.mCurrency = Some(std::make_pair( 250 currency, NumberFormatOptions::CurrencyDisplay::Symbol)); 251 break; 252 case ffi::FluentNumberCurrencyDisplayStyleRaw::Code: 253 options.mCurrency = Some(std::make_pair( 254 currency, NumberFormatOptions::CurrencyDisplay::Code)); 255 break; 256 case ffi::FluentNumberCurrencyDisplayStyleRaw::Name: 257 options.mCurrency = Some(std::make_pair( 258 currency, NumberFormatOptions::CurrencyDisplay::Name)); 259 break; 260 default: 261 MOZ_ASSERT_UNREACHABLE(); 262 break; 263 } 264 } break; 265 case ffi::FluentNumberStyleRaw::Percent: 266 options.mPercent = true; 267 break; 268 default: 269 MOZ_ASSERT_UNREACHABLE(); 270 break; 271 } 272 273 options.mGrouping = aOptions->use_grouping 274 ? NumberFormatOptions::Grouping::Auto 275 : NumberFormatOptions::Grouping::Never; 276 options.mMinIntegerDigits = Some(aOptions->minimum_integer_digits); 277 278 if (aOptions->minimum_significant_digits >= 0 || 279 aOptions->maximum_significant_digits >= 0) { 280 options.mSignificantDigits = 281 Some(std::make_pair(aOptions->minimum_significant_digits, 282 aOptions->maximum_significant_digits)); 283 } else { 284 options.mFractionDigits = Some(std::make_pair( 285 aOptions->minimum_fraction_digits, aOptions->maximum_fraction_digits)); 286 } 287 288 Result<UniquePtr<NumberFormat>, ICUError> result = 289 NumberFormat::TryCreate(aLocale->get(), options); 290 291 MOZ_ASSERT(result.isOk()); 292 293 if (result.isOk()) { 294 return reinterpret_cast<ffi::RawNumberFormatter*>( 295 result.unwrap().release()); 296 } 297 298 return nullptr; 299 } 300 301 uint8_t* FluentBuiltInNumberFormatterFormat( 302 const ffi::RawNumberFormatter* aFormatter, double input, size_t* aOutCount, 303 size_t* aOutCapacity) { 304 const NumberFormat* nf = reinterpret_cast<const NumberFormat*>(aFormatter); 305 306 SizeableUTF8Buffer buffer; 307 if (nf->format(input, buffer).isOk()) { 308 *aOutCount = buffer.mWritten; 309 *aOutCapacity = buffer.mCapacity; 310 return reinterpret_cast<uint8_t*>(buffer.mBuffer.release()); 311 } 312 313 return nullptr; 314 } 315 316 void FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter* aFormatter) { 317 delete reinterpret_cast<NumberFormat*>(aFormatter); 318 } 319 320 /* DateTime */ 321 322 static Maybe<DateTimeFormat::Style> GetStyle(ffi::FluentDateTimeStyle aStyle) { 323 switch (aStyle) { 324 case ffi::FluentDateTimeStyle::Full: 325 return Some(DateTimeFormat::Style::Full); 326 case ffi::FluentDateTimeStyle::Long: 327 return Some(DateTimeFormat::Style::Long); 328 case ffi::FluentDateTimeStyle::Medium: 329 return Some(DateTimeFormat::Style::Medium); 330 case ffi::FluentDateTimeStyle::Short: 331 return Some(DateTimeFormat::Style::Short); 332 case ffi::FluentDateTimeStyle::None: 333 return Nothing(); 334 } 335 MOZ_ASSERT_UNREACHABLE(); 336 return Nothing(); 337 } 338 339 static Maybe<DateTimeFormat::Text> GetText( 340 ffi::FluentDateTimeTextComponent aText) { 341 switch (aText) { 342 case ffi::FluentDateTimeTextComponent::Long: 343 return Some(DateTimeFormat::Text::Long); 344 case ffi::FluentDateTimeTextComponent::Short: 345 return Some(DateTimeFormat::Text::Short); 346 case ffi::FluentDateTimeTextComponent::Narrow: 347 return Some(DateTimeFormat::Text::Narrow); 348 case ffi::FluentDateTimeTextComponent::None: 349 return Nothing(); 350 } 351 MOZ_ASSERT_UNREACHABLE(); 352 return Nothing(); 353 } 354 355 static Maybe<DateTimeFormat::Month> GetMonth( 356 ffi::FluentDateTimeMonthComponent aMonth) { 357 switch (aMonth) { 358 case ffi::FluentDateTimeMonthComponent::Numeric: 359 return Some(DateTimeFormat::Month::Numeric); 360 case ffi::FluentDateTimeMonthComponent::TwoDigit: 361 return Some(DateTimeFormat::Month::TwoDigit); 362 case ffi::FluentDateTimeMonthComponent::Long: 363 return Some(DateTimeFormat::Month::Long); 364 case ffi::FluentDateTimeMonthComponent::Short: 365 return Some(DateTimeFormat::Month::Short); 366 case ffi::FluentDateTimeMonthComponent::Narrow: 367 return Some(DateTimeFormat::Month::Narrow); 368 case ffi::FluentDateTimeMonthComponent::None: 369 return Nothing(); 370 } 371 MOZ_ASSERT_UNREACHABLE(); 372 return Nothing(); 373 } 374 375 static Maybe<DateTimeFormat::Numeric> GetNumeric( 376 ffi::FluentDateTimeNumericComponent aNumeric) { 377 switch (aNumeric) { 378 case ffi::FluentDateTimeNumericComponent::Numeric: 379 return Some(DateTimeFormat::Numeric::Numeric); 380 case ffi::FluentDateTimeNumericComponent::TwoDigit: 381 return Some(DateTimeFormat::Numeric::TwoDigit); 382 case ffi::FluentDateTimeNumericComponent::None: 383 return Nothing(); 384 } 385 MOZ_ASSERT_UNREACHABLE(); 386 return Nothing(); 387 } 388 389 static Maybe<DateTimeFormat::TimeZoneName> GetTimeZoneName( 390 ffi::FluentDateTimeTimeZoneNameComponent aTimeZoneName) { 391 switch (aTimeZoneName) { 392 case ffi::FluentDateTimeTimeZoneNameComponent::Long: 393 return Some(DateTimeFormat::TimeZoneName::Long); 394 case ffi::FluentDateTimeTimeZoneNameComponent::Short: 395 return Some(DateTimeFormat::TimeZoneName::Short); 396 case ffi::FluentDateTimeTimeZoneNameComponent::None: 397 return Nothing(); 398 } 399 MOZ_ASSERT_UNREACHABLE(); 400 return Nothing(); 401 } 402 403 static Maybe<DateTimeFormat::HourCycle> GetHourCycle( 404 ffi::FluentDateTimeHourCycle aHourCycle) { 405 switch (aHourCycle) { 406 case ffi::FluentDateTimeHourCycle::H24: 407 return Some(DateTimeFormat::HourCycle::H24); 408 case ffi::FluentDateTimeHourCycle::H23: 409 return Some(DateTimeFormat::HourCycle::H23); 410 case ffi::FluentDateTimeHourCycle::H12: 411 return Some(DateTimeFormat::HourCycle::H12); 412 case ffi::FluentDateTimeHourCycle::H11: 413 return Some(DateTimeFormat::HourCycle::H11); 414 case ffi::FluentDateTimeHourCycle::None: 415 return Nothing(); 416 } 417 MOZ_ASSERT_UNREACHABLE(); 418 return Nothing(); 419 } 420 421 static Maybe<DateTimeFormat::ComponentsBag> GetComponentsBag( 422 ffi::FluentDateTimeOptions aOptions) { 423 if (GetStyle(aOptions.date_style) || GetStyle(aOptions.time_style)) { 424 return Nothing(); 425 } 426 427 DateTimeFormat::ComponentsBag components; 428 components.era = GetText(aOptions.era); 429 components.year = GetNumeric(aOptions.year); 430 components.month = GetMonth(aOptions.month); 431 components.day = GetNumeric(aOptions.day); 432 components.weekday = GetText(aOptions.weekday); 433 components.hour = GetNumeric(aOptions.hour); 434 components.minute = GetNumeric(aOptions.minute); 435 components.second = GetNumeric(aOptions.second); 436 components.timeZoneName = GetTimeZoneName(aOptions.time_zone_name); 437 components.hourCycle = GetHourCycle(aOptions.hour_cycle); 438 439 if (!components.era && !components.year && !components.month && 440 !components.day && !components.weekday && !components.hour && 441 !components.minute && !components.second && !components.timeZoneName) { 442 return Nothing(); 443 } 444 445 return Some(components); 446 } 447 448 ffi::RawDateTimeFormatter* FluentBuiltInDateTimeFormatterCreate( 449 const nsCString* aLocale, ffi::FluentDateTimeOptions aOptions) { 450 auto genResult = DateTimePatternGenerator::TryCreate(aLocale->get()); 451 if (genResult.isErr()) { 452 MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); 453 return nullptr; 454 } 455 UniquePtr<DateTimePatternGenerator> dateTimePatternGenerator = 456 genResult.unwrap(); 457 458 if (auto components = GetComponentsBag(aOptions)) { 459 auto result = DateTimeFormat::TryCreateFromComponents( 460 Span(*aLocale), *components, dateTimePatternGenerator.get()); 461 if (result.isErr()) { 462 MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); 463 return nullptr; 464 } 465 466 return reinterpret_cast<ffi::RawDateTimeFormatter*>( 467 result.unwrap().release()); 468 } 469 470 DateTimeFormat::StyleBag style; 471 style.date = GetStyle(aOptions.date_style); 472 style.time = GetStyle(aOptions.time_style); 473 474 auto result = DateTimeFormat::TryCreateFromStyle( 475 Span(*aLocale), style, dateTimePatternGenerator.get()); 476 477 if (result.isErr()) { 478 MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); 479 return nullptr; 480 } 481 482 return reinterpret_cast<ffi::RawDateTimeFormatter*>( 483 result.unwrap().release()); 484 } 485 486 uint8_t* FluentBuiltInDateTimeFormatterFormat( 487 const ffi::RawDateTimeFormatter* aFormatter, double aUnixEpoch, 488 uint32_t* aOutCount) { 489 const auto* dtFormat = reinterpret_cast<const DateTimeFormat*>(aFormatter); 490 491 SizeableUTF8Buffer buffer; 492 dtFormat->TryFormat(aUnixEpoch, buffer).unwrap(); 493 494 *aOutCount = buffer.mWritten; 495 496 return reinterpret_cast<uint8_t*>(buffer.mBuffer.release()); 497 } 498 499 void FluentBuiltInDateTimeFormatterDestroy( 500 ffi::RawDateTimeFormatter* aFormatter) { 501 delete reinterpret_cast<const DateTimeFormat*>(aFormatter); 502 } 503 } 504 505 } // namespace intl 506 } // namespace mozilla