/* 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>; #[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 { transform_dom(s, false, true, true) } fn transform_bidi(s: &str) -> Cow { transform_dom(s, false, false, false) } fn format_numbers(num: &FluentValue, intls: &IntlLangMemoizer) -> Option { match num { FluentValue::Number(n) => { let result = intls .with_try_get::((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 { let mut value = nsCString::new(); let prefs_service = xpcom::get_service::(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 { let mut value = false; let prefs_service = xpcom::get_service::(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 // 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::() { 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::() { 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 { 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, ) { 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, ) -> 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, ret_val: &mut nsACString, ret_errors: &mut ThinVec, ) -> 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, ) { // 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> { 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, errors: &[FluentError]) { for error in errors { ret_errors.push(error.to_string().into()); } }