summaryrefslogtreecommitdiffstats
path: root/intl/l10n/rust/fluent-ffi
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /intl/l10n/rust/fluent-ffi
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/l10n/rust/fluent-ffi')
-rw-r--r--intl/l10n/rust/fluent-ffi/Cargo.toml17
-rw-r--r--intl/l10n/rust/fluent-ffi/cbindgen.toml24
-rw-r--r--intl/l10n/rust/fluent-ffi/src/builtins.rs389
-rw-r--r--intl/l10n/rust/fluent-ffi/src/bundle.rs331
-rw-r--r--intl/l10n/rust/fluent-ffi/src/ffi.rs154
-rw-r--r--intl/l10n/rust/fluent-ffi/src/lib.rs12
-rw-r--r--intl/l10n/rust/fluent-ffi/src/resource.rs39
-rw-r--r--intl/l10n/rust/fluent-ffi/src/text_elements.rs164
8 files changed, 1130 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..1f922822d7
--- /dev/null
+++ b/intl/l10n/rust/fluent-ffi/Cargo.toml
@@ -0,0 +1,17 @@
+[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-syntax = "0.11.0"
+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..bb671f4b17
--- /dev/null
+++ b/intl/l10n/rust/fluent-ffi/src/lib.rs
@@ -0,0 +1,12 @@
+/* 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;
+mod text_elements;
+
+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);
+}
diff --git a/intl/l10n/rust/fluent-ffi/src/text_elements.rs b/intl/l10n/rust/fluent-ffi/src/text_elements.rs
new file mode 100644
index 0000000000..0ffeffd4c7
--- /dev/null
+++ b/intl/l10n/rust/fluent-ffi/src/text_elements.rs
@@ -0,0 +1,164 @@
+/* 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 fluent::FluentResource;
+use fluent_syntax::ast;
+use nsstring::nsCString;
+use thin_vec::ThinVec;
+
+#[repr(C)]
+pub struct TextElementInfo {
+ id: nsCString,
+ attr: nsCString,
+ text: nsCString,
+}
+
+struct TextElementsCollector<'a> {
+ current_id: Option<String>,
+ current_attr: Option<String>,
+ elements: &'a mut ThinVec<TextElementInfo>,
+}
+
+impl<'a> TextElementsCollector<'a> {
+ pub fn new(elements: &'a mut ThinVec<TextElementInfo>) -> Self {
+ Self {
+ current_id: None,
+ current_attr: None,
+ elements: elements,
+ }
+ }
+
+ fn collect_inline_expression(&mut self, x: &ast::InlineExpression<&str>) {
+ match x {
+ ast::InlineExpression::StringLiteral { .. } => {}
+ ast::InlineExpression::NumberLiteral { .. } => {}
+ ast::InlineExpression::FunctionReference { arguments, .. } => {
+ self.collect_call_arguments(arguments);
+ }
+ ast::InlineExpression::MessageReference { .. } => {}
+ ast::InlineExpression::TermReference { arguments, .. } => {
+ if let Some(y) = arguments {
+ self.collect_call_arguments(y);
+ }
+ }
+ ast::InlineExpression::VariableReference { .. } => {}
+ ast::InlineExpression::Placeable { expression } => {
+ self.collect_expression(expression.as_ref());
+ }
+ }
+ }
+
+ fn collect_named_argument(&mut self, x: &ast::NamedArgument<&str>) {
+ self.collect_inline_expression(&x.value);
+ }
+
+ fn collect_call_arguments(&mut self, x: &ast::CallArguments<&str>) {
+ for y in x.positional.iter() {
+ self.collect_inline_expression(y);
+ }
+ for y in x.named.iter() {
+ self.collect_named_argument(y);
+ }
+ }
+
+ fn collect_variant(&mut self, x: &ast::Variant<&str>) {
+ self.collect_pattern(&x.value);
+ }
+
+ fn collect_expression(&mut self, x: &ast::Expression<&str>) {
+ match x {
+ ast::Expression::Select { selector, variants } => {
+ self.collect_inline_expression(selector);
+ for y in variants.iter() {
+ self.collect_variant(y);
+ }
+ }
+ ast::Expression::Inline(i) => {
+ self.collect_inline_expression(i);
+ }
+ }
+ }
+
+ fn collect_pattern_element(&mut self, x: &ast::PatternElement<&str>) {
+ match x {
+ ast::PatternElement::TextElement { value } => {
+ self.elements.push(TextElementInfo {
+ id: self
+ .current_id
+ .as_ref()
+ .map_or_else(|| nsCString::new(), nsCString::from),
+ attr: self
+ .current_attr
+ .as_ref()
+ .map_or_else(|| nsCString::new(), nsCString::from),
+ text: nsCString::from(*value),
+ });
+ }
+ ast::PatternElement::Placeable { expression } => {
+ self.collect_expression(expression);
+ }
+ }
+ }
+
+ fn collect_pattern(&mut self, x: &ast::Pattern<&str>) {
+ for y in x.elements.iter() {
+ self.collect_pattern_element(y);
+ }
+ }
+
+ fn collect_attribute(&mut self, x: &ast::Attribute<&str>) {
+ self.current_attr = Some(x.id.name.to_string());
+
+ self.collect_pattern(&x.value);
+ }
+
+ fn collect_message(&mut self, x: &ast::Message<&str>) {
+ self.current_id = Some(x.id.name.to_string());
+ self.current_attr = None;
+
+ if let Some(ref y) = x.value {
+ self.collect_pattern(y);
+ }
+ for y in x.attributes.iter() {
+ self.collect_attribute(y);
+ }
+ }
+
+ fn collect_term(&mut self, x: &ast::Term<&str>) {
+ self.current_id = Some(x.id.name.to_string());
+ self.current_attr = None;
+
+ self.collect_pattern(&x.value);
+ for y in x.attributes.iter() {
+ self.collect_attribute(y);
+ }
+ }
+
+ pub fn collect_entry(&mut self, x: &ast::Entry<&str>) {
+ match x {
+ ast::Entry::Message(m) => {
+ self.collect_message(m);
+ }
+ ast::Entry::Term(t) => {
+ self.collect_term(t);
+ }
+ ast::Entry::Comment(_) => {}
+ ast::Entry::GroupComment(_) => {}
+ ast::Entry::ResourceComment(_) => {}
+ ast::Entry::Junk { .. } => {}
+ }
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn fluent_resource_get_text_elements(
+ res: &FluentResource,
+ elements: &mut ThinVec<TextElementInfo>,
+) {
+ let mut collector = TextElementsCollector::new(elements);
+
+ for entry in res.entries() {
+ collector.collect_entry(entry);
+ }
+}