зеркало из https://github.com/mozilla/gecko-dev.git
507 строки
16 KiB
C++
507 строки
16 KiB
C++
/* -*- 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
|