#![warn(
clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::cargo_common_metadata
)]
#![allow(clippy::non_ascii_literal)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_lossless
)]
#![allow(let_underscore_drop)]
use std::{sync::Mutex, thread};
use crossbeam_channel::{unbounded, Receiver, Sender};
use diskit::Diskit;
use either::Either;
use fluent::{types::FluentNumber, FluentArgs, FluentBundle, FluentResource, FluentValue};
use unic_langid::LanguageIdentifier;
#[cfg(feature = "log")]
use log::{debug, error, info, trace, warn};
mod message_handler;
mod err;
pub use err::Error;
pub use message_handler::{MessageBuffer, MessageHandler};
#[cfg_attr(not(feature = "log"), allow(rustdoc::broken_intra_doc_links))]
pub type Writer = crossbeam_channel::Sender<(LogLevel, String)>;
pub trait Lang
{
fn deconstruct_lang_id<D>(self, diskit: D) -> Result<Vec<(String, LanguageIdentifier)>, Error>
where
D: Diskit;
}
pub trait Message
{
fn to_str(&self) -> &'static str;
fn into_vec(self) -> Vec<(&'static str, Either<String, FluentNumber>)>;
fn loglevel(&self) -> LogLevel;
}
#[derive(Clone, Copy)]
pub struct L10n
{
inner: &'static Mutex<(Command, Answer, Writer)>,
}
#[derive(Copy, Clone, Debug)]
pub enum LogLevel
{
Error,
Warn,
Info,
Debug,
Trace,
Println,
Unreachable,
}
struct L10nInner
{
bundles: Vec<FluentBundle<FluentResource>>,
}
type KeyType = &'static str;
type ArgSliceType = Vec<(&'static str, Either<String, FluentNumber>)>;
type Command = Sender<(KeyType, ArgSliceType)>;
type Answer = Receiver<String>;
impl L10nInner
{
fn new<L, D>(lang: L, diskit: D) -> Result<Self, Error>
where
L: Lang,
D: Diskit,
{
fn inner(
(s, lang): (String, LanguageIdentifier),
) -> Result<FluentBundle<FluentResource>, Error>
{
let mut bundle = FluentBundle::new(vec![lang]);
bundle.add_resource(FluentResource::try_new(s).map_err(|(_, x)| x)?)?;
Ok(bundle)
}
let langs = lang
.deconstruct_lang_id(diskit)?
.into_iter()
.map(inner)
.collect::<Result<Vec<_>, _>>()?;
if langs.is_empty()
{
return Err(Error::NoLanguage);
}
Ok(Self { bundles: langs })
}
fn get_raw(
bundle: &FluentBundle<FluentResource>,
key: &str,
args: Option<&FluentArgs>,
) -> Result<String, Error>
{
let mut errors = vec![];
let msg = bundle.format_pattern(
bundle
.get_message(key)
.ok_or_else(|| format!("Message doesn't exist: {key:?}"))
.map(|msg| {
msg.value()
.ok_or_else(|| format!("Message has no value: {key:?}",))
})
.and_then(|x| x)?,
args,
&mut errors,
);
if !errors.is_empty()
{
return Err(errors.into());
}
Ok(msg.to_string())
}
fn get(&self, key: &KeyType, arg_slice: ArgSliceType) -> String
{
let args = if arg_slice.is_empty()
{
None
}
else
{
let mut args = FluentArgs::new();
for (key, value) in arg_slice
{
match value
{
Either::Left(s) => args.set(key, FluentValue::from(s)),
Either::Right(s) => args.set(key, FluentValue::from(s)),
}
}
Some(args)
};
self.bundles
.iter()
.map(|bundle| Self::get_raw(bundle, key, args.as_ref()))
.next()
.expect("There should always be at least one language.")
.expect("Error with l10n.")
}
}
impl L10n
{
pub fn new<L, D>(lang: L, writer: Writer, diskit: D) -> Result<Self, Error>
where
L: Lang + Send + 'static,
D: Diskit + Send + 'static,
{
let (tx_error, rx_error) = unbounded();
let (tx_com, rx_com) = unbounded();
let (tx_data, rx_data) = unbounded();
thread::spawn(move || match L10nInner::new(lang, diskit)
{
Ok(lang) =>
{
tx_error
.send(Ok(Self {
inner: Box::leak(Box::new(Mutex::new((tx_com, rx_data, writer)))),
}))
.expect("Failed to initialise l10n");
while let Ok((key, arg_slice)) = rx_com.recv()
{
tx_data
.send(lang.get(&key, arg_slice))
.expect("Failed to answer l10n info");
}
}
Err(err) => tx_error
.send(Err(err))
.expect("Failed to signal the failing of initialising of l10n"),
});
rx_error.recv().map_err(Into::into).and_then(|x| x)
}
fn get_raw(self, key: &'static str, arg_slice: ArgSliceType) -> String
{
let lock = self
.inner
.lock()
.expect("Lock over l10n struct is poisoned");
lock.0
.send((key, arg_slice))
.expect("Can't request l10n info");
lock.1.recv().expect("Can't get l10n info")
}
#[must_use]
pub fn get<M>(self, message: M) -> String
where
M: Message,
{
self.get_raw(message.to_str(), message.into_vec())
}
}
impl<M> MessageHandler<M> for L10n
where
M: Message,
{
fn write(&self, message: M)
{
let loglevel = message.loglevel();
let msg = self.get(message);
self.inner
.lock()
.expect("Panicing due to previous panic.")
.2
.send((loglevel, msg))
.expect("Write handler has stopped.");
}
}
#[cfg(feature = "log")]
#[must_use]
pub fn standard_write_handler() -> crossbeam_channel::Sender<(LogLevel, String)>
{
let (tx, rx) = unbounded();
thread::spawn(move || {
while let Ok((loglevel, msg)) = rx.recv()
{
match loglevel
{
LogLevel::Error => error!("{msg}"),
LogLevel::Warn => warn!("{msg}"),
LogLevel::Info => info!("{msg}"),
LogLevel::Debug => debug!("{msg}"),
LogLevel::Trace => trace!("{msg}"),
LogLevel::Println => println!("{msg}"),
LogLevel::Unreachable => unreachable!("{msg}"),
}
}
});
tx
}