diff options
Diffstat (limited to '')
-rw-r--r-- | intl/l10n/FluentBundle.cpp | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/intl/l10n/FluentBundle.cpp b/intl/l10n/FluentBundle.cpp new file mode 100644 index 0000000000..a20fca5564 --- /dev/null +++ b/intl/l10n/FluentBundle.cpp @@ -0,0 +1,506 @@ +/* -*- 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/ToJSValue.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<CharType*>(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<void*>(ptr)); } + }; + + UniquePtr<CharType[], FreePolicy> 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<JSObject*> 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<ffi::FluentBundleRc> aRaw) + : mParent(aParent), mRaw(std::move(aRaw)) { + MOZ_COUNT_CTOR(FluentBundle); +} + +already_AddRefed<FluentBundle> FluentBundle::Constructor( + const dom::GlobalObject& aGlobal, + const UTF8StringOrUTF8StringSequence& aLocales, + const dom::FluentBundleOptions& aOptions, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> 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<ffi::FluentBundleRc> 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<JSObject*> aGivenProto) { + return FluentBundle_Binding::Wrap(aCx, this, aGivenProto); +} + +FluentBundle::~FluentBundle() { MOZ_COUNT_DTOR(FluentBundle); }; + +void FluentBundle::GetLocales(nsTArray<nsCString>& aLocales) { + fluent_bundle_get_locales(mRaw.get(), &aLocales); +} + +void FluentBundle::AddResource( + FluentResource& aResource, + const dom::FluentBundleAddResourceOptions& aOptions) { + bool allowOverrides = aOptions.mAllowOverrides; + nsTArray<nsCString> 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<FluentMessage>& aRetVal) { + bool hasValue = false; + nsTArray<nsCString> 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<JSObject*> aErrors, + nsTArray<nsCString>& aInput) { + uint32_t length; + if (NS_WARN_IF(!JS::GetArrayLength(aCx, aErrors, &length))) { + return false; + } + + for (auto& err : aInput) { + JS::Rooted<JS::Value> 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<ffi::L10nArg>& 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<L10nArgs>& aArgs, + const Optional<JS::Handle<JSObject*>>& aErrors, + nsACString& aRetVal, ErrorResult& aRv) { + nsTArray<ffi::L10nArg> l10nArgs; + + if (!aArgs.IsNull()) { + const L10nArgs& args = aArgs.Value(); + ConvertArgs(args, l10nArgs); + } + + nsTArray<nsCString> 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<UniquePtr<NumberFormat>, ICUError> result = + NumberFormat::TryCreate(aLocale->get(), options); + + MOZ_ASSERT(result.isOk()); + + if (result.isOk()) { + return reinterpret_cast<ffi::RawNumberFormatter*>( + 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<const NumberFormat*>(aFormatter); + + SizeableUTF8Buffer buffer; + if (nf->format(input, buffer).isOk()) { + *aOutCount = buffer.mWritten; + *aOutCapacity = buffer.mCapacity; + return reinterpret_cast<uint8_t*>(buffer.mBuffer.release()); + } + + return nullptr; +} + +void FluentBuiltInNumberFormatterDestroy(ffi::RawNumberFormatter* aFormatter) { + delete reinterpret_cast<NumberFormat*>(aFormatter); +} + +/* DateTime */ + +static Maybe<DateTimeFormat::Style> 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<DateTimeFormat::Text> 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<DateTimeFormat::Month> 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<DateTimeFormat::Numeric> 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<DateTimeFormat::TimeZoneName> 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<DateTimeFormat::HourCycle> 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<DateTimeFormat::ComponentsBag> 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> 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<ffi::RawDateTimeFormatter*>( + 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<ffi::RawDateTimeFormatter*>( + result.unwrap().release()); +} + +uint8_t* FluentBuiltInDateTimeFormatterFormat( + const ffi::RawDateTimeFormatter* aFormatter, double aUnixEpoch, + uint32_t* aOutCount) { + const auto* dtFormat = reinterpret_cast<const DateTimeFormat*>(aFormatter); + + SizeableUTF8Buffer buffer; + dtFormat->TryFormat(aUnixEpoch, buffer).unwrap(); + + *aOutCount = buffer.mWritten; + + return reinterpret_cast<uint8_t*>(buffer.mBuffer.release()); +} + +void FluentBuiltInDateTimeFormatterDestroy( + ffi::RawDateTimeFormatter* aFormatter) { + delete reinterpret_cast<const DateTimeFormat*>(aFormatter); +} +} + +} // namespace intl +} // namespace mozilla |