diff options
Diffstat (limited to 'intl/l10n/rust/fluent-ffi/src/bundle.rs')
-rw-r--r-- | intl/l10n/rust/fluent-ffi/src/bundle.rs | 331 |
1 files changed, 331 insertions, 0 deletions
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()); + } +} |