summaryrefslogtreecommitdiffstats
path: root/intl/l10n/FluentBundle.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'intl/l10n/FluentBundle.cpp')
-rw-r--r--intl/l10n/FluentBundle.cpp506
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