/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FluentBundle.h" #include "nsContentUtils.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/intl/NumberFormat.h" #include "mozilla/intl/DateTimeFormat.h" #include "mozilla/intl/DateTimePatternGenerator.h" #include "nsIInputStream.h" #include "nsStringFwd.h" #include "nsTArray.h" #include "js/PropertyAndElement.h" // JS_DefineElement using namespace mozilla::dom; namespace mozilla { namespace intl { class SizeableUTF8Buffer { public: using CharType = char; bool reserve(size_t size) { mBuffer.reset(reinterpret_cast(malloc(size))); mCapacity = size; return true; } CharType* data() { return mBuffer.get(); } size_t capacity() const { return mCapacity; } void written(size_t amount) { mWritten = amount; } size_t mWritten = 0; size_t mCapacity = 0; struct FreePolicy { void operator()(const void* ptr) { free(const_cast(ptr)); } }; UniquePtr mBuffer; }; NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentPattern, mParent) FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId) : mId(aId), mParent(aParent) { MOZ_COUNT_CTOR(FluentPattern); } FluentPattern::FluentPattern(nsISupports* aParent, const nsACString& aId, const nsACString& aAttrName) : mId(aId), mAttrName(aAttrName), mParent(aParent) { MOZ_COUNT_CTOR(FluentPattern); } JSObject* FluentPattern::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return FluentPattern_Binding::Wrap(aCx, this, aGivenProto); } FluentPattern::~FluentPattern() { MOZ_COUNT_DTOR(FluentPattern); }; /* FluentBundle */ NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FluentBundle, mParent) FluentBundle::FluentBundle(nsISupports* aParent, UniquePtr aRaw) : mParent(aParent), mRaw(std::move(aRaw)) { MOZ_COUNT_CTOR(FluentBundle); } already_AddRefed FluentBundle::Constructor( const dom::GlobalObject& aGlobal, const UTF8StringOrUTF8StringSequence& aLocales, const dom::FluentBundleOptions& aOptions, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } bool useIsolating = aOptions.mUseIsolating; nsAutoCString pseudoStrategy; if (aOptions.mPseudoStrategy.WasPassed()) { pseudoStrategy = aOptions.mPseudoStrategy.Value(); } UniquePtr raw; if (aLocales.IsUTF8String()) { const nsACString& locale = aLocales.GetAsUTF8String(); raw.reset( ffi::fluent_bundle_new_single(&locale, useIsolating, &pseudoStrategy)); } else { const auto& locales = aLocales.GetAsUTF8StringSequence(); raw.reset(ffi::fluent_bundle_new(locales.Elements(), locales.Length(), useIsolating, &pseudoStrategy)); } if (!raw) { aRv.ThrowInvalidStateError( "Failed to create the FluentBundle. Check the " "locales and pseudo strategy arguments."); return nullptr; } return do_AddRef(new FluentBundle(global, std::move(raw))); } JSObject* FluentBundle::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return FluentBundle_Binding::Wrap(aCx, this, aGivenProto); } FluentBundle::~FluentBundle() { MOZ_COUNT_DTOR(FluentBundle); }; void FluentBundle::GetLocales(nsTArray& aLocales) { fluent_bundle_get_locales(mRaw.get(), &aLocales); } void FluentBundle::AddResource( FluentResource& aResource, const dom::FluentBundleAddResourceOptions& aOptions) { bool allowOverrides = aOptions.mAllowOverrides; nsTArray errors; fluent_bundle_add_resource(mRaw.get(), aResource.Raw(), allowOverrides, &errors); for (auto& err : errors) { nsContentUtils::LogSimpleConsoleError(NS_ConvertUTF8toUTF16(err), "L10n"_ns, false, true, nsIScriptError::warningFlag); } } bool FluentBundle::HasMessage(const nsACString& aId) { return fluent_bundle_has_message(mRaw.get(), &aId); } void FluentBundle::GetMessage(const nsACString& aId, Nullable& aRetVal) { bool hasValue = false; nsTArray attributes; bool exists = fluent_bundle_get_message(mRaw.get(), &aId, &hasValue, &attributes); if (exists) { FluentMessage& msg = aRetVal.SetValue(); if (hasValue) { msg.mValue = new FluentPattern(mParent, aId); } for (auto& name : attributes) { auto newEntry = msg.mAttributes.Entries().AppendElement(fallible); newEntry->mKey = name; newEntry->mValue = new FluentPattern(mParent, aId, name); } } } bool extendJSArrayWithErrors(JSContext* aCx, JS::Handle aErrors, nsTArray& aInput) { uint32_t length; if (NS_WARN_IF(!JS::GetArrayLength(aCx, aErrors, &length))) { return false; } for (auto& err : aInput) { JS::Rooted jsval(aCx); if (!ToJSValue(aCx, NS_ConvertUTF8toUTF16(err), &jsval)) { return false; } if (!JS_DefineElement(aCx, aErrors, length++, jsval, JSPROP_ENUMERATE)) { return false; } } return true; } /* static */ void FluentBundle::ConvertArgs(const L10nArgs& aArgs, nsTArray& aRetVal) { aRetVal.SetCapacity(aArgs.Entries().Length()); for (const auto& entry : aArgs.Entries()) { if (!entry.mValue.IsNull()) { const auto& value = entry.mValue.Value(); if (value.IsUTF8String()) { aRetVal.AppendElement(ffi::L10nArg{ &entry.mKey, ffi::FluentArgument::String(&value.GetAsUTF8String())}); } else { aRetVal.AppendElement(ffi::L10nArg{ &entry.mKey, ffi::FluentArgument::Double_(value.GetAsDouble())}); } } } } void FluentBundle::FormatPattern(JSContext* aCx, const FluentPattern& aPattern, const Nullable& aArgs, const Optional>& aErrors, nsACString& aRetVal, ErrorResult& aRv) { nsTArray l10nArgs; if (!aArgs.IsNull()) { const L10nArgs& args = aArgs.Value(); ConvertArgs(args, l10nArgs); } nsTArray errors; bool succeeded = fluent_bundle_format_pattern(mRaw.get(), &aPattern.mId, &aPattern.mAttrName, &l10nArgs, &aRetVal, &errors); if (!succeeded) { return aRv.ThrowInvalidStateError( "Failed to format the FluentPattern. Likely the " "pattern could not be retrieved from the bundle."); } if (aErrors.WasPassed()) { if (!extendJSArrayWithErrors(aCx, aErrors.Value(), errors)) { aRv.ThrowUnknownError("Failed to add errors to an error array."); } } } // FFI extern "C" { ffi::RawNumberFormatter* FluentBuiltInNumberFormatterCreate( const nsCString* aLocale, const ffi::FluentNumberOptionsRaw* aOptions) { NumberFormatOptions options; switch (aOptions->style) { case ffi::FluentNumberStyleRaw::Decimal: break; case ffi::FluentNumberStyleRaw::Currency: { std::string currency = aOptions->currency.get(); switch (aOptions->currency_display) { case ffi::FluentNumberCurrencyDisplayStyleRaw::Symbol: options.mCurrency = Some(std::make_pair( currency, NumberFormatOptions::CurrencyDisplay::Symbol)); break; case ffi::FluentNumberCurrencyDisplayStyleRaw::Code: options.mCurrency = Some(std::make_pair( currency, NumberFormatOptions::CurrencyDisplay::Code)); break; case ffi::FluentNumberCurrencyDisplayStyleRaw::Name: options.mCurrency = Some(std::make_pair( currency, NumberFormatOptions::CurrencyDisplay::Name)); break; default: MOZ_ASSERT_UNREACHABLE(); break; } } break; case ffi::FluentNumberStyleRaw::Percent: options.mPercent = true; break; default: MOZ_ASSERT_UNREACHABLE(); break; } options.mGrouping = aOptions->use_grouping ? NumberFormatOptions::Grouping::Auto : NumberFormatOptions::Grouping::Never; options.mMinIntegerDigits = Some(aOptions->minimum_integer_digits); if (aOptions->minimum_significant_digits >= 0 || aOptions->maximum_significant_digits >= 0) { options.mSignificantDigits = Some(std::make_pair(aOptions->minimum_significant_digits, aOptions->maximum_significant_digits)); } else { options.mFractionDigits = Some(std::make_pair( aOptions->minimum_fraction_digits, aOptions->maximum_fraction_digits)); } Result, ICUError> result = NumberFormat::TryCreate(aLocale->get(), options); MOZ_ASSERT(result.isOk()); if (result.isOk()) { return reinterpret_cast( result.unwrap().release()); } return nullptr; } uint8_t* FluentBuiltInNumberFormatterFormat( const ffi::RawNumberFormatter* aFormatter, double input, size_t* aOutCount, size_t* aOutCapacity) { const NumberFormat* nf = reinterpret_cast(aFormatter); SizeableUTF8Buffer buffer; if (nf->format(input, buffer).isOk()) { *aOutCount = buffer.mWritten; *aOutCapacity = buffer.mCapacity; return reinterpret_cast(buffer.mBuffer.release()); } return nullptr; } void FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter* aFormatter) { delete reinterpret_cast(aFormatter); } /* DateTime */ static Maybe GetStyle(ffi::FluentDateTimeStyle aStyle) { switch (aStyle) { case ffi::FluentDateTimeStyle::Full: return Some(DateTimeFormat::Style::Full); case ffi::FluentDateTimeStyle::Long: return Some(DateTimeFormat::Style::Long); case ffi::FluentDateTimeStyle::Medium: return Some(DateTimeFormat::Style::Medium); case ffi::FluentDateTimeStyle::Short: return Some(DateTimeFormat::Style::Short); case ffi::FluentDateTimeStyle::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetText( ffi::FluentDateTimeTextComponent aText) { switch (aText) { case ffi::FluentDateTimeTextComponent::Long: return Some(DateTimeFormat::Text::Long); case ffi::FluentDateTimeTextComponent::Short: return Some(DateTimeFormat::Text::Short); case ffi::FluentDateTimeTextComponent::Narrow: return Some(DateTimeFormat::Text::Narrow); case ffi::FluentDateTimeTextComponent::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetMonth( ffi::FluentDateTimeMonthComponent aMonth) { switch (aMonth) { case ffi::FluentDateTimeMonthComponent::Numeric: return Some(DateTimeFormat::Month::Numeric); case ffi::FluentDateTimeMonthComponent::TwoDigit: return Some(DateTimeFormat::Month::TwoDigit); case ffi::FluentDateTimeMonthComponent::Long: return Some(DateTimeFormat::Month::Long); case ffi::FluentDateTimeMonthComponent::Short: return Some(DateTimeFormat::Month::Short); case ffi::FluentDateTimeMonthComponent::Narrow: return Some(DateTimeFormat::Month::Narrow); case ffi::FluentDateTimeMonthComponent::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetNumeric( ffi::FluentDateTimeNumericComponent aNumeric) { switch (aNumeric) { case ffi::FluentDateTimeNumericComponent::Numeric: return Some(DateTimeFormat::Numeric::Numeric); case ffi::FluentDateTimeNumericComponent::TwoDigit: return Some(DateTimeFormat::Numeric::TwoDigit); case ffi::FluentDateTimeNumericComponent::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetTimeZoneName( ffi::FluentDateTimeTimeZoneNameComponent aTimeZoneName) { switch (aTimeZoneName) { case ffi::FluentDateTimeTimeZoneNameComponent::Long: return Some(DateTimeFormat::TimeZoneName::Long); case ffi::FluentDateTimeTimeZoneNameComponent::Short: return Some(DateTimeFormat::TimeZoneName::Short); case ffi::FluentDateTimeTimeZoneNameComponent::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetHourCycle( ffi::FluentDateTimeHourCycle aHourCycle) { switch (aHourCycle) { case ffi::FluentDateTimeHourCycle::H24: return Some(DateTimeFormat::HourCycle::H24); case ffi::FluentDateTimeHourCycle::H23: return Some(DateTimeFormat::HourCycle::H23); case ffi::FluentDateTimeHourCycle::H12: return Some(DateTimeFormat::HourCycle::H12); case ffi::FluentDateTimeHourCycle::H11: return Some(DateTimeFormat::HourCycle::H11); case ffi::FluentDateTimeHourCycle::None: return Nothing(); } MOZ_ASSERT_UNREACHABLE(); return Nothing(); } static Maybe GetComponentsBag( ffi::FluentDateTimeOptions aOptions) { if (GetStyle(aOptions.date_style) || GetStyle(aOptions.time_style)) { return Nothing(); } DateTimeFormat::ComponentsBag components; components.era = GetText(aOptions.era); components.year = GetNumeric(aOptions.year); components.month = GetMonth(aOptions.month); components.day = GetNumeric(aOptions.day); components.weekday = GetText(aOptions.weekday); components.hour = GetNumeric(aOptions.hour); components.minute = GetNumeric(aOptions.minute); components.second = GetNumeric(aOptions.second); components.timeZoneName = GetTimeZoneName(aOptions.time_zone_name); components.hourCycle = GetHourCycle(aOptions.hour_cycle); if (!components.era && !components.year && !components.month && !components.day && !components.weekday && !components.hour && !components.minute && !components.second && !components.timeZoneName) { return Nothing(); } return Some(components); } ffi::RawDateTimeFormatter* FluentBuiltInDateTimeFormatterCreate( const nsCString* aLocale, ffi::FluentDateTimeOptions aOptions) { auto genResult = DateTimePatternGenerator::TryCreate(aLocale->get()); if (genResult.isErr()) { MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); return nullptr; } UniquePtr dateTimePatternGenerator = genResult.unwrap(); if (auto components = GetComponentsBag(aOptions)) { auto result = DateTimeFormat::TryCreateFromComponents( Span(*aLocale), *components, dateTimePatternGenerator.get()); if (result.isErr()) { MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); return nullptr; } return reinterpret_cast( result.unwrap().release()); } DateTimeFormat::StyleBag style; style.date = GetStyle(aOptions.date_style); style.time = GetStyle(aOptions.time_style); auto result = DateTimeFormat::TryCreateFromStyle( Span(*aLocale), style, dateTimePatternGenerator.get()); if (result.isErr()) { MOZ_ASSERT_UNREACHABLE("There was an error in DateTimeFormat"); return nullptr; } return reinterpret_cast( result.unwrap().release()); } uint8_t* FluentBuiltInDateTimeFormatterFormat( const ffi::RawDateTimeFormatter* aFormatter, double aUnixEpoch, uint32_t* aOutCount) { const auto* dtFormat = reinterpret_cast(aFormatter); SizeableUTF8Buffer buffer; dtFormat->TryFormat(aUnixEpoch, buffer).unwrap(); *aOutCount = buffer.mWritten; return reinterpret_cast(buffer.mBuffer.release()); } void FluentBuiltInDateTimeFormatterDestroy( ffi::RawDateTimeFormatter* aFormatter) { delete reinterpret_cast(aFormatter); } } } // namespace intl } // namespace mozilla