diff options
Diffstat (limited to 'intl/l10n/rust/fluent-ffi')
-rw-r--r-- | intl/l10n/rust/fluent-ffi/Cargo.toml | 16 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/cbindgen.toml | 24 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/builtins.rs | 389 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/bundle.rs | 331 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/ffi.rs | 154 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/lib.rs | 11 | ||||
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/resource.rs | 39 |
7 files changed, 964 insertions, 0 deletions
diff --git a/intl/l10n/rust/fluent-ffi/Cargo.toml b/intl/l10n/rust/fluent-ffi/Cargo.toml new file mode 100644 index 0000000000..dad9498efd --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fluent-ffi" +version = "0.1.0" +authors = ["Zibi Braniecki <zibi@braniecki.net>"] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +fluent = { version = "0.16.0", features = ["fluent-pseudo"] } +fluent-pseudo = "0.3.1" +intl-memoizer = "0.5.1" +unic-langid = "0.9" +nsstring = { path = "../../../../xpcom/rust/nsstring" } +cstr = "0.2" +xpcom = { path = "../../../../xpcom/rust/xpcom" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } diff --git a/intl/l10n/rust/fluent-ffi/cbindgen.toml b/intl/l10n/rust/fluent-ffi/cbindgen.toml new file mode 100644 index 0000000000..5384a81b0a --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/cbindgen.toml @@ -0,0 +1,24 @@ +header = """/* 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/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +#ifndef mozilla_intl_l10n_FluentBindings_h +#error "Don't include this file directly, instead include FluentBindings.h" +#endif +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +namespaces = ["mozilla", "intl", "ffi"] + +[parse] +parse_deps = true +include = ["fluent", "fluent-bundle", "intl-memoizer"] + +[enum] +derive_helper_methods = true + +[export.rename] +"ThinVec" = "nsTArray" diff --git a/intl/l10n/rust/fluent-ffi/src/builtins.rs b/intl/l10n/rust/fluent-ffi/src/builtins.rs new file mode 100644 index 0000000000..c7ffe8c3ee --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/src/builtins.rs @@ -0,0 +1,389 @@ +/* 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/. */ + +use crate::ffi; +use fluent::types::{FluentNumberOptions, FluentType, FluentValue}; +use fluent::FluentArgs; +use intl_memoizer::IntlLangMemoizer; +use intl_memoizer::Memoizable; +use nsstring::nsCString; +use std::borrow::Cow; +use std::ptr::NonNull; +use unic_langid::LanguageIdentifier; + +pub struct NumberFormat { + raw: Option<NonNull<ffi::RawNumberFormatter>>, +} + +/** + * According to http://userguide.icu-project.org/design, as long as we constrain + * ourselves to const APIs ICU is const-correct. + */ +unsafe impl Send for NumberFormat {} +unsafe impl Sync for NumberFormat {} + +impl NumberFormat { + pub fn new(locale: LanguageIdentifier, options: &FluentNumberOptions) -> Self { + let loc: String = locale.to_string(); + Self { + raw: unsafe { + NonNull::new(ffi::FluentBuiltInNumberFormatterCreate( + &loc.into(), + &options.into(), + )) + }, + } + } + + pub fn format(&self, input: f64) -> String { + if let Some(raw) = self.raw { + unsafe { + let mut byte_count = 0; + let mut capacity = 0; + let buffer = ffi::FluentBuiltInNumberFormatterFormat( + raw.as_ptr(), + input, + &mut byte_count, + &mut capacity, + ); + if buffer.is_null() { + return String::new(); + } + String::from_raw_parts(buffer, byte_count, capacity) + } + } else { + String::new() + } + } +} + +impl Drop for NumberFormat { + fn drop(&mut self) { + if let Some(raw) = self.raw { + unsafe { ffi::FluentBuiltInNumberFormatterDestroy(raw.as_ptr()) }; + } + } +} + +impl Memoizable for NumberFormat { + type Args = (FluentNumberOptions,); + type Error = &'static str; + fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> { + Ok(NumberFormat::new(lang, &args.0)) + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum FluentDateTimeStyle { + Full, + Long, + Medium, + Short, + None, +} + +impl Default for FluentDateTimeStyle { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeStyle { + fn from(input: &str) -> Self { + match input { + "full" => Self::Full, + "long" => Self::Long, + "medium" => Self::Medium, + "short" => Self::Short, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FluentDateTimeHourCycle { + H24, + H23, + H12, + H11, + None, +} + +impl Default for FluentDateTimeHourCycle { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeHourCycle { + fn from(input: &str) -> Self { + match input { + "h24" => Self::H24, + "h23" => Self::H23, + "h12" => Self::H12, + "h11" => Self::H11, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FluentDateTimeTextComponent { + Long, + Short, + Narrow, + None, +} + +impl Default for FluentDateTimeTextComponent { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeTextComponent { + fn from(input: &str) -> Self { + match input { + "long" => Self::Long, + "short" => Self::Short, + "narrow" => Self::Narrow, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FluentDateTimeNumericComponent { + Numeric, + TwoDigit, + None, +} + +impl Default for FluentDateTimeNumericComponent { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeNumericComponent { + fn from(input: &str) -> Self { + match input { + "numeric" => Self::Numeric, + "2-digit" => Self::TwoDigit, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FluentDateTimeMonthComponent { + Numeric, + TwoDigit, + Long, + Short, + Narrow, + None, +} + +impl Default for FluentDateTimeMonthComponent { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeMonthComponent { + fn from(input: &str) -> Self { + match input { + "numeric" => Self::Numeric, + "2-digit" => Self::TwoDigit, + "long" => Self::Long, + "short" => Self::Short, + "narrow" => Self::Narrow, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FluentDateTimeTimeZoneNameComponent { + Long, + Short, + None, +} + +impl Default for FluentDateTimeTimeZoneNameComponent { + fn default() -> Self { + Self::None + } +} + +impl From<&str> for FluentDateTimeTimeZoneNameComponent { + fn from(input: &str) -> Self { + match input { + "long" => Self::Long, + "short" => Self::Short, + _ => Self::None, + } + } +} + +#[repr(C)] +#[derive(Default, Debug, Clone, Hash, PartialEq, Eq)] +pub struct FluentDateTimeOptions { + pub date_style: FluentDateTimeStyle, + pub time_style: FluentDateTimeStyle, + pub hour_cycle: FluentDateTimeHourCycle, + pub weekday: FluentDateTimeTextComponent, + pub era: FluentDateTimeTextComponent, + pub year: FluentDateTimeNumericComponent, + pub month: FluentDateTimeMonthComponent, + pub day: FluentDateTimeNumericComponent, + pub hour: FluentDateTimeNumericComponent, + pub minute: FluentDateTimeNumericComponent, + pub second: FluentDateTimeNumericComponent, + pub time_zone_name: FluentDateTimeTimeZoneNameComponent, +} + +impl FluentDateTimeOptions { + pub fn merge(&mut self, opts: &FluentArgs) { + for (key, value) in opts.iter() { + match (key, value) { + ("dateStyle", FluentValue::String(n)) => { + self.date_style = n.as_ref().into(); + } + ("timeStyle", FluentValue::String(n)) => { + self.time_style = n.as_ref().into(); + } + ("hourCycle", FluentValue::String(n)) => { + self.hour_cycle = n.as_ref().into(); + } + ("weekday", FluentValue::String(n)) => { + self.weekday = n.as_ref().into(); + } + ("era", FluentValue::String(n)) => { + self.era = n.as_ref().into(); + } + ("year", FluentValue::String(n)) => { + self.year = n.as_ref().into(); + } + ("month", FluentValue::String(n)) => { + self.month = n.as_ref().into(); + } + ("day", FluentValue::String(n)) => { + self.day = n.as_ref().into(); + } + ("hour", FluentValue::String(n)) => { + self.hour = n.as_ref().into(); + } + ("minute", FluentValue::String(n)) => { + self.minute = n.as_ref().into(); + } + ("second", FluentValue::String(n)) => { + self.second = n.as_ref().into(); + } + ("timeZoneName", FluentValue::String(n)) => { + self.time_zone_name = n.as_ref().into(); + } + _ => {} + } + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct FluentDateTime { + epoch: f64, + options: FluentDateTimeOptions, +} + +impl FluentType for FluentDateTime { + fn duplicate(&self) -> Box<dyn FluentType + Send> { + Box::new(self.clone()) + } + fn as_string(&self, intls: &IntlLangMemoizer) -> Cow<'static, str> { + let result = intls + .with_try_get::<DateTimeFormat, _, _>((self.options.clone(),), |dtf| { + dtf.format(self.epoch) + }) + .expect("Failed to retrieve a DateTimeFormat instance."); + result.into() + } + fn as_string_threadsafe( + &self, + _: &intl_memoizer::concurrent::IntlLangMemoizer, + ) -> Cow<'static, str> { + unimplemented!() + } +} + +impl std::fmt::Display for FluentDateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DATETIME: {}", self.epoch) + } +} + +impl FluentDateTime { + pub fn new(epoch: f64, options: FluentDateTimeOptions) -> Self { + Self { epoch, options } + } +} + +pub struct DateTimeFormat { + raw: Option<NonNull<ffi::RawDateTimeFormatter>>, +} + +/** + * According to http://userguide.icu-project.org/design, as long as we constrain + * ourselves to const APIs ICU is const-correct. + */ +unsafe impl Send for DateTimeFormat {} +unsafe impl Sync for DateTimeFormat {} + +impl DateTimeFormat { + pub fn new(locale: LanguageIdentifier, options: FluentDateTimeOptions) -> Self { + // ICU needs null-termination here, otherwise we could use nsCStr. + let loc: nsCString = locale.to_string().into(); + Self { + raw: unsafe { NonNull::new(ffi::FluentBuiltInDateTimeFormatterCreate(&loc, options)) }, + } + } + + pub fn format(&self, input: f64) -> String { + if let Some(raw) = self.raw { + unsafe { + let mut byte_count = 0; + let buffer = + ffi::FluentBuiltInDateTimeFormatterFormat(raw.as_ptr(), input, &mut byte_count); + if buffer.is_null() { + return String::new(); + } + String::from_raw_parts(buffer, byte_count as usize, byte_count as usize) + } + } else { + String::new() + } + } +} + +impl Drop for DateTimeFormat { + fn drop(&mut self) { + if let Some(raw) = self.raw { + unsafe { ffi::FluentBuiltInDateTimeFormatterDestroy(raw.as_ptr()) }; + } + } +} + +impl Memoizable for DateTimeFormat { + type Args = (FluentDateTimeOptions,); + type Error = &'static str; + fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> { + Ok(DateTimeFormat::new(lang, args.0)) + } +} diff --git a/intl/l10n/rust/fluent-ffi/src/bundle.rs b/intl/l10n/rust/fluent-ffi/src/bundle.rs new file mode 100644 index 0000000000..21bf0d52e9 --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/src/bundle.rs @@ -0,0 +1,331 @@ +/* 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/. */ + +use crate::builtins::{FluentDateTime, FluentDateTimeOptions, NumberFormat}; +use cstr::cstr; +pub use fluent::{FluentArgs, FluentBundle, FluentError, FluentResource, FluentValue}; +use fluent_pseudo::transform_dom; +pub use intl_memoizer::IntlLangMemoizer; +use nsstring::{nsACString, nsCString}; +use std::borrow::Cow; +use std::ffi::CStr; +use std::mem; +use std::rc::Rc; +use thin_vec::ThinVec; +use unic_langid::LanguageIdentifier; +use xpcom::interfaces::nsIPrefBranch; + +pub type FluentBundleRc = FluentBundle<Rc<FluentResource>>; + +#[derive(Debug)] +#[repr(C, u8)] +pub enum FluentArgument<'s> { + Double_(f64), + String(&'s nsACString), +} + +#[derive(Debug)] +#[repr(C)] +pub struct L10nArg<'s> { + pub id: &'s nsACString, + pub value: FluentArgument<'s>, +} + +fn transform_accented(s: &str) -> Cow<str> { + transform_dom(s, false, true, true) +} + +fn transform_bidi(s: &str) -> Cow<str> { + transform_dom(s, false, false, false) +} + +fn format_numbers(num: &FluentValue, intls: &IntlLangMemoizer) -> Option<String> { + match num { + FluentValue::Number(n) => { + let result = intls + .with_try_get::<NumberFormat, _, _>((n.options.clone(),), |nf| nf.format(n.value)) + .expect("Failed to retrieve a NumberFormat instance."); + Some(result) + } + _ => None, + } +} + +fn get_string_pref(name: &CStr) -> Option<nsCString> { + let mut value = nsCString::new(); + let prefs_service = + xpcom::get_service::<nsIPrefBranch>(cstr!("@mozilla.org/preferences-service;1"))?; + unsafe { + prefs_service + .GetCharPref(name.as_ptr(), &mut *value) + .to_result() + .ok()?; + } + Some(value) +} + +fn get_bool_pref(name: &CStr) -> Option<bool> { + let mut value = false; + let prefs_service = + xpcom::get_service::<nsIPrefBranch>(cstr!("@mozilla.org/preferences-service;1"))?; + unsafe { + prefs_service + .GetBoolPref(name.as_ptr(), &mut value) + .to_result() + .ok()?; + } + Some(value) +} + +pub fn adapt_bundle_for_gecko(bundle: &mut FluentBundleRc, pseudo_strategy: Option<&nsACString>) { + bundle.set_formatter(Some(format_numbers)); + + bundle + .add_function("PLATFORM", |_args, _named_args| { + if cfg!(target_os = "linux") { + "linux".into() + } else if cfg!(target_os = "windows") { + "windows".into() + } else if cfg!(target_os = "macos") { + "macos".into() + } else if cfg!(target_os = "android") { + "android".into() + } else { + "other".into() + } + }) + .expect("Failed to add a function to the bundle."); + bundle + .add_function("NUMBER", |args, named| { + if let Some(FluentValue::Number(n)) = args.get(0) { + let mut num = n.clone(); + num.options.merge(named); + FluentValue::Number(num) + } else { + FluentValue::None + } + }) + .expect("Failed to add a function to the bundle."); + bundle + .add_function("DATETIME", |args, named| { + if let Some(FluentValue::Number(n)) = args.get(0) { + let mut options = FluentDateTimeOptions::default(); + options.merge(&named); + FluentValue::Custom(Box::new(FluentDateTime::new(n.value, options))) + } else { + FluentValue::None + } + }) + .expect("Failed to add a function to the bundle."); + + enum PseudoStrategy { + Accented, + Bidi, + None, + } + // This is quirky because we can't coerce Option<&nsACString> and Option<nsCString> + // into bytes easily without allocating. + let strategy_kind = match pseudo_strategy.map(|s| &s[..]) { + Some(b"accented") => PseudoStrategy::Accented, + Some(b"bidi") => PseudoStrategy::Bidi, + _ => { + if let Some(pseudo_strategy) = get_string_pref(cstr!("intl.l10n.pseudo")) { + match &pseudo_strategy[..] { + b"accented" => PseudoStrategy::Accented, + b"bidi" => PseudoStrategy::Bidi, + _ => PseudoStrategy::None, + } + } else { + PseudoStrategy::None + } + } + }; + match strategy_kind { + PseudoStrategy::Accented => bundle.set_transform(Some(transform_accented)), + PseudoStrategy::Bidi => bundle.set_transform(Some(transform_bidi)), + PseudoStrategy::None => bundle.set_transform(None), + } + + // Temporarily disable bidi isolation due to Microsoft not supporting FSI/PDI. + // See bug 1439018 for details. + let default_use_isolating = false; + let use_isolating = + get_bool_pref(cstr!("intl.l10n.enable-bidi-marks")).unwrap_or(default_use_isolating); + bundle.set_use_isolating(use_isolating); +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_new_single( + locale: &nsACString, + use_isolating: bool, + pseudo_strategy: &nsACString, +) -> *mut FluentBundleRc { + let id = match locale.to_utf8().parse::<LanguageIdentifier>() { + Ok(id) => id, + Err(..) => return std::ptr::null_mut(), + }; + + Box::into_raw(fluent_bundle_new_internal( + &[id], + use_isolating, + pseudo_strategy, + )) +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_bundle_new( + locales: *const nsCString, + locale_count: usize, + use_isolating: bool, + pseudo_strategy: &nsACString, +) -> *mut FluentBundleRc { + let mut langids = Vec::with_capacity(locale_count); + let locales = std::slice::from_raw_parts(locales, locale_count); + for locale in locales { + let id = match locale.to_utf8().parse::<LanguageIdentifier>() { + Ok(id) => id, + Err(..) => return std::ptr::null_mut(), + }; + langids.push(id); + } + + Box::into_raw(fluent_bundle_new_internal( + &langids, + use_isolating, + pseudo_strategy, + )) +} + +fn fluent_bundle_new_internal( + langids: &[LanguageIdentifier], + use_isolating: bool, + pseudo_strategy: &nsACString, +) -> Box<FluentBundleRc> { + let mut bundle = FluentBundle::new(langids.to_vec()); + bundle.set_use_isolating(use_isolating); + + bundle.set_formatter(Some(format_numbers)); + + adapt_bundle_for_gecko(&mut bundle, Some(pseudo_strategy)); + + Box::new(bundle) +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_get_locales( + bundle: &FluentBundleRc, + result: &mut ThinVec<nsCString>, +) { + for locale in &bundle.locales { + result.push(locale.to_string().as_str().into()); + } +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_bundle_destroy(bundle: *mut FluentBundleRc) { + let _ = Box::from_raw(bundle); +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_has_message(bundle: &FluentBundleRc, id: &nsACString) -> bool { + bundle.has_message(id.to_string().as_str()) +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_get_message( + bundle: &FluentBundleRc, + id: &nsACString, + has_value: &mut bool, + attrs: &mut ThinVec<nsCString>, +) -> bool { + match bundle.get_message(&id.to_utf8()) { + Some(message) => { + attrs.reserve(message.attributes().count()); + *has_value = message.value().is_some(); + for attr in message.attributes() { + attrs.push(attr.id().into()); + } + true + } + None => { + *has_value = false; + false + } + } +} + +#[no_mangle] +pub extern "C" fn fluent_bundle_format_pattern( + bundle: &FluentBundleRc, + id: &nsACString, + attr: &nsACString, + args: &ThinVec<L10nArg>, + ret_val: &mut nsACString, + ret_errors: &mut ThinVec<nsCString>, +) -> bool { + let args = convert_args(&args); + + let message = match bundle.get_message(&id.to_utf8()) { + Some(message) => message, + None => return false, + }; + + let pattern = if !attr.is_empty() { + match message.get_attribute(&attr.to_utf8()) { + Some(attr) => attr.value(), + None => return false, + } + } else { + match message.value() { + Some(value) => value, + None => return false, + } + }; + + let mut errors = vec![]; + bundle + .write_pattern(ret_val, pattern, args.as_ref(), &mut errors) + .expect("Failed to write to a nsCString."); + append_fluent_errors_to_ret_errors(ret_errors, &errors); + true +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_bundle_add_resource( + bundle: &mut FluentBundleRc, + r: *const FluentResource, + allow_overrides: bool, + ret_errors: &mut ThinVec<nsCString>, +) { + // we don't own the resource + let r = mem::ManuallyDrop::new(Rc::from_raw(r)); + + if allow_overrides { + bundle.add_resource_overriding(Rc::clone(&r)); + } else if let Err(errors) = bundle.add_resource(Rc::clone(&r)) { + append_fluent_errors_to_ret_errors(ret_errors, &errors); + } +} + +pub fn convert_args<'s>(args: &[L10nArg<'s>]) -> Option<FluentArgs<'s>> { + if args.is_empty() { + return None; + } + + let mut result = FluentArgs::with_capacity(args.len()); + for arg in args { + let val = match arg.value { + FluentArgument::Double_(d) => FluentValue::from(d), + FluentArgument::String(s) => FluentValue::from(s.to_utf8()), + }; + result.set(arg.id.to_string(), val); + } + Some(result) +} + +fn append_fluent_errors_to_ret_errors(ret_errors: &mut ThinVec<nsCString>, errors: &[FluentError]) { + for error in errors { + ret_errors.push(error.to_string().into()); + } +} diff --git a/intl/l10n/rust/fluent-ffi/src/ffi.rs b/intl/l10n/rust/fluent-ffi/src/ffi.rs new file mode 100644 index 0000000000..a264ad11b7 --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/src/ffi.rs @@ -0,0 +1,154 @@ +/* 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/. */ + +use crate::builtins::FluentDateTimeOptions; +use fluent::types::FluentNumberCurrencyDisplayStyle; +use fluent::types::FluentNumberOptions; +use fluent::types::FluentNumberStyle; +use nsstring::nsCString; + +pub enum RawNumberFormatter {} + +#[repr(C)] +pub enum FluentNumberStyleRaw { + Decimal, + Currency, + Percent, +} + +impl From<FluentNumberStyle> for FluentNumberStyleRaw { + fn from(input: FluentNumberStyle) -> Self { + match input { + FluentNumberStyle::Decimal => Self::Decimal, + FluentNumberStyle::Currency => Self::Currency, + FluentNumberStyle::Percent => Self::Percent, + } + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub enum FluentNumberCurrencyDisplayStyleRaw { + Symbol, + Code, + Name, +} + +impl From<FluentNumberCurrencyDisplayStyle> for FluentNumberCurrencyDisplayStyleRaw { + fn from(input: FluentNumberCurrencyDisplayStyle) -> Self { + match input { + FluentNumberCurrencyDisplayStyle::Symbol => Self::Symbol, + FluentNumberCurrencyDisplayStyle::Code => Self::Code, + FluentNumberCurrencyDisplayStyle::Name => Self::Name, + } + } +} + +#[repr(C)] +pub struct FluentNumberOptionsRaw { + pub style: FluentNumberStyleRaw, + pub currency: nsCString, + pub currency_display: FluentNumberCurrencyDisplayStyleRaw, + pub use_grouping: bool, + pub minimum_integer_digits: usize, + pub minimum_fraction_digits: usize, + pub maximum_fraction_digits: usize, + pub minimum_significant_digits: isize, + pub maximum_significant_digits: isize, +} + +fn get_number_option(val: Option<usize>, min: usize, max: usize, default: usize) -> usize { + if let Some(val) = val { + if val >= min && val <= max { + val + } else { + default + } + } else { + default + } +} + +impl From<&FluentNumberOptions> for FluentNumberOptionsRaw { + fn from(input: &FluentNumberOptions) -> Self { + let currency: nsCString = if let Some(ref currency) = input.currency { + currency.into() + } else { + nsCString::new() + }; + + //XXX: This should be fetched from currency table. + let currency_digits = 2; + + // Keep it aligned with ECMA402 NumberFormat logic. + let minfd_default = if input.style == FluentNumberStyle::Currency { + currency_digits + } else { + 0 + }; + let maxfd_default = match input.style { + FluentNumberStyle::Decimal => 3, + FluentNumberStyle::Currency => currency_digits, + FluentNumberStyle::Percent => 0, + }; + let minid = get_number_option(input.minimum_integer_digits, 1, 21, 1); + let minfd = get_number_option(input.minimum_fraction_digits, 0, 20, minfd_default); + let maxfd_actual_default = std::cmp::max(minfd, maxfd_default); + let maxfd = get_number_option( + input.maximum_fraction_digits, + minfd, + 20, + maxfd_actual_default, + ); + + let (minsd, maxsd) = if input.minimum_significant_digits.is_some() + || input.maximum_significant_digits.is_some() + { + let minsd = get_number_option(input.minimum_significant_digits, 1, 21, 1); + let maxsd = get_number_option(input.maximum_significant_digits, minsd, 21, 21); + (minsd as isize, maxsd as isize) + } else { + (-1, -1) + }; + + Self { + style: input.style.into(), + currency, + currency_display: input.currency_display.into(), + use_grouping: input.use_grouping, + minimum_integer_digits: minid, + minimum_fraction_digits: minfd, + maximum_fraction_digits: maxfd, + minimum_significant_digits: minsd, + maximum_significant_digits: maxsd, + } + } +} + +pub enum RawDateTimeFormatter {} + +extern "C" { + pub fn FluentBuiltInNumberFormatterCreate( + locale: &nsCString, + options: &FluentNumberOptionsRaw, + ) -> *mut RawNumberFormatter; + pub fn FluentBuiltInNumberFormatterFormat( + formatter: *const RawNumberFormatter, + input: f64, + out_count: &mut usize, + out_capacity: &mut usize, + ) -> *mut u8; + pub fn FluentBuiltInNumberFormatterDestroy(formatter: *mut RawNumberFormatter); + + pub fn FluentBuiltInDateTimeFormatterCreate( + locale: &nsCString, + options: FluentDateTimeOptions, + ) -> *mut RawDateTimeFormatter; + pub fn FluentBuiltInDateTimeFormatterFormat( + formatter: *const RawDateTimeFormatter, + input: f64, + out_count: &mut u32, + ) -> *mut u8; + pub fn FluentBuiltInDateTimeFormatterDestroy(formatter: *mut RawDateTimeFormatter); +} diff --git a/intl/l10n/rust/fluent-ffi/src/lib.rs b/intl/l10n/rust/fluent-ffi/src/lib.rs new file mode 100644 index 0000000000..6cf56c2e4a --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/src/lib.rs @@ -0,0 +1,11 @@ +/* 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/. */ + +mod builtins; +mod bundle; +mod ffi; +mod resource; + +pub use bundle::*; +pub use resource::*; diff --git a/intl/l10n/rust/fluent-ffi/src/resource.rs b/intl/l10n/rust/fluent-ffi/src/resource.rs new file mode 100644 index 0000000000..dc011b9462 --- /dev/null +++ b/intl/l10n/rust/fluent-ffi/src/resource.rs @@ -0,0 +1,39 @@ +/* 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/. */ + +pub use fluent::FluentResource; +use nsstring::nsACString; +use std::{ + mem::{self, ManuallyDrop}, + rc::Rc, +}; + +#[no_mangle] +pub extern "C" fn fluent_resource_new( + name: &nsACString, + has_errors: &mut bool, +) -> *const FluentResource { + let res = match FluentResource::try_new(name.to_string()) { + Ok(res) => { + *has_errors = false; + res + } + Err((res, _)) => { + *has_errors = true; + res + } + }; + Rc::into_raw(Rc::new(res)) +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_resource_addref(res: *const FluentResource) { + let raw = ManuallyDrop::new(Rc::from_raw(res)); + mem::forget(Rc::clone(&raw)); +} + +#[no_mangle] +pub unsafe extern "C" fn fluent_resource_release(res: *const FluentResource) { + let _ = Rc::from_raw(res); +} |