Source code

Revision control

Copy as Markdown

Other Tools

/* 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 https://mozilla.org/MPL/2.0/. */
use fluent::FluentValue;
use fluent_fallback::{
types::{
L10nAttribute as FluentL10nAttribute, L10nKey as FluentL10nKey,
L10nMessage as FluentL10nMessage, ResourceId,
},
Localization,
};
use fluent_ffi::{convert_args, FluentArgs, FluentArgument, L10nArg};
use l10nregistry_ffi::{
env::GeckoEnvironment,
registry::{get_l10n_registry, GeckoL10nRegistry, GeckoResourceId},
};
use nsstring::{nsACString, nsCString};
use std::os::raw::c_void;
use std::{borrow::Cow, cell::RefCell};
use thin_vec::ThinVec;
use unic_langid::LanguageIdentifier;
use xpcom::{
interfaces::{nsIRunnablePriority},
RefCounted, RefPtr, Refcnt,
};
#[derive(Debug)]
#[repr(C)]
pub struct L10nKey<'s> {
id: &'s nsACString,
args: ThinVec<L10nArg<'s>>,
}
impl<'s> From<&'s L10nKey<'s>> for FluentL10nKey<'static> {
fn from(input: &'s L10nKey<'s>) -> Self {
FluentL10nKey {
id: input.id.to_utf8().to_string().into(),
args: convert_args_to_owned(&input.args),
}
}
}
// This is a variant of `convert_args` from `fluent-ffi` with a 'static constrain
// put on the resulting `FluentArgs` to make it acceptable into `spqwn_current_thread`.
pub fn convert_args_to_owned(args: &[L10nArg]) -> Option<FluentArgs<'static>> {
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),
// We need this to be owned because we pass the result into `spawn_local`.
FluentArgument::String(s) => FluentValue::from(Cow::Owned(s.to_utf8().to_string())),
};
result.set(arg.id.to_string(), val);
}
Some(result)
}
#[derive(Debug)]
#[repr(C)]
pub struct L10nAttribute {
name: nsCString,
value: nsCString,
}
impl From<FluentL10nAttribute<'_>> for L10nAttribute {
fn from(attr: FluentL10nAttribute<'_>) -> Self {
Self {
name: nsCString::from(&*attr.name),
value: nsCString::from(&*attr.value),
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct L10nMessage {
value: nsCString,
attributes: ThinVec<L10nAttribute>,
}
impl std::default::Default for L10nMessage {
fn default() -> Self {
Self {
value: nsCString::new(),
attributes: ThinVec::new(),
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct OptionalL10nMessage {
is_present: bool,
message: L10nMessage,
}
impl From<FluentL10nMessage<'_>> for L10nMessage {
fn from(input: FluentL10nMessage) -> Self {
let value = if let Some(value) = input.value {
value.to_string().into()
} else {
let mut s = nsCString::new();
s.set_is_void(true);
s
};
Self {
value,
attributes: input.attributes.into_iter().map(Into::into).collect(),
}
}
}
pub struct LocalizationRc {
inner: RefCell<Localization<GeckoL10nRegistry, GeckoEnvironment>>,
refcnt: Refcnt,
}
// xpcom::RefPtr support
unsafe impl RefCounted for LocalizationRc {
unsafe fn addref(&self) {
localization_addref(self);
}
unsafe fn release(&self) {
localization_release(self);
}
}
impl LocalizationRc {
pub fn new(
res_ids: Vec<ResourceId>,
is_sync: bool,
registry: Option<&GeckoL10nRegistry>,
locales: Option<Vec<LanguageIdentifier>>,
) -> RefPtr<Self> {
let env = GeckoEnvironment::new(locales);
let inner = if let Some(reg) = registry {
Localization::with_env(res_ids, is_sync, env, reg.clone())
} else {
let reg = (*get_l10n_registry()).clone();
Localization::with_env(res_ids, is_sync, env, reg)
};
let loc = Box::new(LocalizationRc {
inner: RefCell::new(inner),
refcnt: unsafe { Refcnt::new() },
});
unsafe {
RefPtr::from_raw(Box::into_raw(loc))
.expect("Failed to create RefPtr<LocalizationRc> from Box<LocalizationRc>")
}
}
pub fn add_resource_id(&self, res_id: ResourceId) {
self.inner.borrow_mut().add_resource_id(res_id);
}
pub fn add_resource_ids(&self, res_ids: Vec<ResourceId>) {
self.inner.borrow_mut().add_resource_ids(res_ids);
}
pub fn remove_resource_id(&self, res_id: ResourceId) -> usize {
self.inner.borrow_mut().remove_resource_id(res_id)
}
pub fn remove_resource_ids(&self, res_ids: Vec<ResourceId>) -> usize {
self.inner.borrow_mut().remove_resource_ids(res_ids)
}
pub fn set_async(&self) {
if self.is_sync() {
self.inner.borrow_mut().set_async();
}
}
pub fn is_sync(&self) -> bool {
self.inner.borrow().is_sync()
}
pub fn on_change(&self) {
self.inner.borrow_mut().on_change();
}
pub fn format_value_sync(
&self,
id: &nsACString,
args: &ThinVec<L10nArg>,
ret_val: &mut nsACString,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
let mut errors = vec![];
let args = convert_args(&args);
if let Ok(value) = self.inner.borrow().bundles().format_value_sync(
&id.to_utf8(),
args.as_ref(),
&mut errors,
) {
if let Some(value) = value {
ret_val.assign(&value);
} else {
ret_val.set_is_void(true);
}
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &[id], |id| id.to_string());
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_values_sync(
&self,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<nsCString>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
ret_val.reserve(keys.len());
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let mut errors = vec![];
if let Ok(values) = self
.inner
.borrow()
.bundles()
.format_values_sync(&keys, &mut errors)
{
for value in values.iter() {
if let Some(value) = value {
ret_val.push(value.as_ref().into());
} else {
let mut void_string = nsCString::new();
void_string.set_is_void(true);
ret_val.push(void_string);
}
}
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string());
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_messages_sync(
&self,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<OptionalL10nMessage>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
ret_val.reserve(keys.len());
let mut errors = vec![];
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
if let Ok(messages) = self
.inner
.borrow()
.bundles()
.format_messages_sync(&keys, &mut errors)
{
for msg in messages {
ret_val.push(if let Some(msg) = msg {
OptionalL10nMessage {
is_present: true,
message: msg.into(),
}
} else {
OptionalL10nMessage {
is_present: false,
message: L10nMessage::default(),
}
});
}
assert_eq!(keys.len(), ret_val.len());
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string());
ret_err.extend(errors.into_iter().map(|err| err.to_string().into()));
true
} else {
false
}
}
pub fn format_value(
&self,
id: &nsACString,
args: &ThinVec<L10nArg>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>),
) {
let bundles = self.inner.borrow().bundles().clone();
let args = convert_args_to_owned(&args);
let id = nsCString::from(id);
let strong_promise = RefPtr::new(promise);
moz_task::TaskBuilder::new("LocalizationRc::format_value", async move {
let mut errors = vec![];
let value = if let Some(value) = bundles
.format_value(&id.to_utf8(), args.as_ref(), &mut errors)
.await
{
let v: nsCString = value.to_string().into();
v
} else {
let mut v = nsCString::new();
v.set_is_void(true);
v
};
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &[id], |id| id.to_string());
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &value, &errors);
})
.priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32)
.spawn_local()
.detach();
}
pub fn format_values(
&self,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>),
) {
let bundles = self.inner.borrow().bundles().clone();
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let strong_promise = RefPtr::new(promise);
moz_task::TaskBuilder::new("LocalizationRc::format_values", async move {
let mut errors = vec![];
let ret_val = bundles
.format_values(&keys, &mut errors)
.await
.into_iter()
.map(|value| {
if let Some(value) = value {
nsCString::from(value.as_ref())
} else {
let mut v = nsCString::new();
v.set_is_void(true);
v
}
})
.collect::<ThinVec<_>>();
assert_eq!(keys.len(), ret_val.len());
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string());
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &ret_val, &errors);
})
.priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32)
.spawn_local()
.detach();
}
pub fn format_messages(
&self,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(
&xpcom::Promise,
&ThinVec<OptionalL10nMessage>,
&ThinVec<nsCString>,
),
) {
let bundles = self.inner.borrow().bundles().clone();
let keys: Vec<FluentL10nKey> = keys.into_iter().map(|k| k.into()).collect();
let strong_promise = RefPtr::new(promise);
moz_task::TaskBuilder::new("LocalizationRc::format_messages", async move {
let mut errors = vec![];
let ret_val = bundles
.format_messages(&keys, &mut errors)
.await
.into_iter()
.map(|msg| {
if let Some(msg) = msg {
OptionalL10nMessage {
is_present: true,
message: msg.into(),
}
} else {
OptionalL10nMessage {
is_present: false,
message: L10nMessage::default(),
}
}
})
.collect::<ThinVec<_>>();
assert_eq!(keys.len(), ret_val.len());
#[cfg(debug_assertions)]
debug_assert_variables_exist(&errors, &keys, |key| key.id.to_string());
let errors = errors
.into_iter()
.map(|err| err.to_string().into())
.collect();
callback(&strong_promise, &ret_val, &errors);
})
.priority(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING as u32)
.spawn_local()
.detach();
}
}
#[no_mangle]
pub extern "C" fn localization_parse_locale(input: &nsCString) -> *const c_void {
let l: LanguageIdentifier = input.to_utf8().parse().unwrap();
Box::into_raw(Box::new(l)) as *const c_void
}
#[no_mangle]
pub extern "C" fn localization_new(
res_ids: &ThinVec<GeckoResourceId>,
is_sync: bool,
reg: Option<&GeckoL10nRegistry>,
result: &mut *const LocalizationRc,
) {
*result = std::ptr::null();
let res_ids: Vec<ResourceId> = res_ids.iter().map(ResourceId::from).collect();
*result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, None));
}
#[no_mangle]
pub extern "C" fn localization_new_with_locales(
res_ids: &ThinVec<GeckoResourceId>,
is_sync: bool,
reg: Option<&GeckoL10nRegistry>,
locales: Option<&ThinVec<nsCString>>,
result: &mut *const LocalizationRc,
) -> bool {
*result = std::ptr::null();
let res_ids: Vec<ResourceId> = res_ids.iter().map(ResourceId::from).collect();
let locales: Result<Option<Vec<LanguageIdentifier>>, _> = locales
.map(|locales| {
locales
.iter()
.map(|s| LanguageIdentifier::from_bytes(&s))
.collect()
})
.transpose();
if let Ok(locales) = locales {
*result = RefPtr::forget_into_raw(LocalizationRc::new(res_ids, is_sync, reg, locales));
true
} else {
false
}
}
#[no_mangle]
pub unsafe extern "C" fn localization_addref(loc: &LocalizationRc) {
loc.refcnt.inc();
}
#[no_mangle]
pub unsafe extern "C" fn localization_release(loc: *const LocalizationRc) {
let rc = (*loc).refcnt.dec();
if rc == 0 {
std::mem::drop(Box::from_raw(loc as *const _ as *mut LocalizationRc));
}
}
#[no_mangle]
pub extern "C" fn localization_add_res_id(loc: &LocalizationRc, res_id: &GeckoResourceId) {
loc.add_resource_id(res_id.into());
}
#[no_mangle]
pub extern "C" fn localization_add_res_ids(loc: &LocalizationRc, res_ids: &ThinVec<GeckoResourceId>) {
let res_ids = res_ids.iter().map(ResourceId::from).collect();
loc.add_resource_ids(res_ids);
}
#[no_mangle]
pub extern "C" fn localization_remove_res_id(loc: &LocalizationRc, res_id: &GeckoResourceId) -> usize {
loc.remove_resource_id(res_id.into())
}
#[no_mangle]
pub extern "C" fn localization_remove_res_ids(
loc: &LocalizationRc,
res_ids: &ThinVec<GeckoResourceId>,
) -> usize {
let res_ids = res_ids.iter().map(ResourceId::from).collect();
loc.remove_resource_ids(res_ids)
}
#[no_mangle]
pub extern "C" fn localization_format_value_sync(
loc: &LocalizationRc,
id: &nsACString,
args: &ThinVec<L10nArg>,
ret_val: &mut nsACString,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_value_sync(id, args, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_values_sync(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<nsCString>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_values_sync(keys, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_messages_sync(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
ret_val: &mut ThinVec<OptionalL10nMessage>,
ret_err: &mut ThinVec<nsCString>,
) -> bool {
loc.format_messages_sync(keys, ret_val, ret_err)
}
#[no_mangle]
pub extern "C" fn localization_format_value(
loc: &LocalizationRc,
id: &nsACString,
args: &ThinVec<L10nArg>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &nsACString, &ThinVec<nsCString>),
) {
loc.format_value(id, args, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_format_values(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<nsCString>, &ThinVec<nsCString>),
) {
loc.format_values(keys, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_format_messages(
loc: &LocalizationRc,
keys: &ThinVec<L10nKey>,
promise: &xpcom::Promise,
callback: extern "C" fn(&xpcom::Promise, &ThinVec<OptionalL10nMessage>, &ThinVec<nsCString>),
) {
loc.format_messages(keys, promise, callback);
}
#[no_mangle]
pub extern "C" fn localization_set_async(loc: &LocalizationRc) {
loc.set_async();
}
#[no_mangle]
pub extern "C" fn localization_is_sync(loc: &LocalizationRc) -> bool {
loc.is_sync()
}
#[no_mangle]
pub extern "C" fn localization_on_change(loc: &LocalizationRc) {
loc.on_change();
}
#[cfg(debug_assertions)]
fn debug_assert_variables_exist<K, F>(
errors: &[fluent_fallback::LocalizationError],
keys: &[K],
to_string: F,
) where
F: Fn(&K) -> String,
{
for error in errors {
if let fluent_fallback::LocalizationError::Resolver { errors, .. } = error {
use fluent::{
resolver::{errors::ReferenceKind, ResolverError},
FluentError,
};
for error in errors {
if let FluentError::ResolverError(ResolverError::Reference(
ReferenceKind::Variable { id },
)) = error
{
// This error needs to be actionable for Firefox engineers to fix
// their Fluent issues. It might be nicer to share the specific
// message, but at this point we don't have that information.
eprintln!(
"Fluent error, the argument \"${}\" was not provided a value.",
id
);
eprintln!("This error happened while formatting the following messages:");
for key in keys {
eprintln!(" {:?}", to_string(key))
}
// Panic with the slightly more cryptic ResolverError.
panic!("{}", error.to_string());
}
}
}
}
}