use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
use fluent_bundle::types::{FluentNumber, FluentNumberOptions};
use crate::{Args, Catalog, Lang, Language, Phrasebook};
#[derive(Clone)]
pub struct Phrases {
book: Phrasebook,
lang: Lang,
backstop: Option<Backstop>,
}
#[derive(Clone)]
struct Backstop {
catalog: &'static Catalog,
book: Phrasebook,
}
thread_local! {
static EMPTY: Phrasebook = Phrasebook::builder().build();
static CATALOGS: RefCell<Vec<(usize, Phrasebook)>> = const { RefCell::new(Vec::new()) };
}
fn catalog_book(catalog: &'static Catalog) -> Phrasebook {
let address = catalog as *const Catalog as usize;
CATALOGS.with(|books| {
if let Some((_, book)) = books.borrow().iter().find(|(a, _)| *a == address) {
return book.clone();
}
let mut builder = Phrasebook::builder();
for lang in catalog.languages() {
builder = builder.language(lang, "");
}
let book = builder.catalog(catalog).build();
books.borrow_mut().push((address, book.clone()));
book
})
}
impl Phrases {
pub(crate) fn new(book: Phrasebook, lang: Lang) -> Phrases {
Phrases {
book,
lang,
backstop: None,
}
}
pub fn empty() -> Phrases {
let book = EMPTY.with(Phrasebook::clone);
let lang = book.fallback();
Phrases::new(book, lang)
}
pub fn from_catalog(catalog: &'static Catalog) -> Phrases {
let book = catalog_book(catalog);
let lang = book.fallback();
Phrases::new(book, lang)
}
pub fn with_backstop(&self, catalog: &'static Catalog) -> Phrases {
Phrases {
book: self.book.clone(),
lang: self.lang,
backstop: Some(Backstop {
catalog,
book: catalog_book(catalog),
}),
}
}
pub fn lang(&self) -> Lang {
self.lang
}
pub fn languages(&self) -> &[Language] {
self.book.languages()
}
pub fn phrasebook(&self) -> &Phrasebook {
&self.book
}
pub fn with_lang(&self, lang: Lang) -> Phrases {
Phrases {
book: self.book.clone(),
lang: self.book.resolve(lang),
backstop: self.backstop.clone(),
}
}
pub fn text(&self, id: &str) -> Rc<str> {
match self.try_text(id) {
Some(text) => text,
None => {
self.missing(id);
Rc::from(id)
}
}
}
pub fn msg(&self, id: &str, args: &Args<'_>) -> String {
match self.try_msg(id, args) {
Some(text) => text,
None => {
self.missing(id);
id.to_owned()
}
}
}
pub fn try_text(&self, id: &str) -> Option<Rc<str>> {
if let Some(text) = self.book.inner.text(self.lang, id, false) {
return Some(text);
}
let backstop = self.backstop.as_ref()?;
let lang = backstop.book.resolve(self.lang);
let text = backstop.book.inner.text(lang, id, true)?;
self.backstopped(id, backstop);
Some(text)
}
pub fn try_msg(&self, id: &str, args: &Args<'_>) -> Option<String> {
if let Some(text) = self.book.inner.format(self.lang, id, Some(args), false) {
return Some(text);
}
let backstop = self.backstop.as_ref()?;
let lang = backstop.book.resolve(self.lang);
let text = backstop.book.inner.format(lang, id, Some(args), true)?;
self.backstopped(id, backstop);
Some(text)
}
pub fn number(&self, value: f64) -> String {
let number = FluentNumber::new(value, FluentNumberOptions::default());
#[cfg(all(feature = "browser", target_arch = "wasm32"))]
if self.book.inner.locale_numbers
&& let Some(text) = crate::intl::number(self.lang, &number)
{
return text;
}
number.as_string().into_owned()
}
pub fn msg_around(&self, id: &str, args: &Args<'_>, name: &str) -> Around {
const MARK: &str = "\u{E000}";
const ISOLATES: [char; 2] = ['\u{2068}', '\u{2069}'];
const ISOLATED: &str = "\u{2068}\u{E000}\u{2069}";
let mut marked = Args::new();
for (key, value) in args.inner.iter() {
if key != name {
marked.inner.set(key, value.clone());
}
}
marked.inner.set(name, MARK);
let text = self.msg(id, &marked);
match text.split_once(MARK) {
Some((before, after)) => Around {
before: before
.strip_suffix(ISOLATES[0])
.unwrap_or(before)
.to_owned(),
after: Some(
after
.strip_prefix(ISOLATES[1])
.unwrap_or(after)
.replace(ISOLATED, "")
.replace(MARK, ""),
),
},
None => Around {
before: text,
after: None,
},
}
}
fn missing(&self, id: &str) {
if self.book.inner.first_miss(self.lang, id) {
tracing::warn!(lang = %self.lang, id, "phrasebook: message missing");
}
}
fn backstopped(&self, id: &str, backstop: &Backstop) {
if self.book.inner.first_miss(self.lang, id) {
tracing::debug!(
lang = %self.lang,
id,
catalog = backstop.catalog.name(),
"phrasebook: message from the backstop catalog"
);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Around {
pub before: String,
pub after: Option<String>,
}
#[cfg(feature = "browser")]
#[cfg_attr(phrasebook_docsrs, doc(cfg(feature = "browser")))]
impl Phrases {
pub fn store(&self) {
self.store_as(crate::browser::DEFAULT_KEY);
}
pub fn store_as(&self, key: &str) {
crate::browser::store(self.lang, key);
}
}
impl PartialEq for Phrases {
fn eq(&self, other: &Self) -> bool {
self.lang == other.lang
&& self.book.ptr_eq(&other.book)
&& match (&self.backstop, &other.backstop) {
(None, None) => true,
(Some(a), Some(b)) => core::ptr::eq(a.catalog, b.catalog),
_ => false,
}
}
}
impl fmt::Debug for Phrases {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Phrases")
.field("lang", &self.lang)
.field("phrasebook", &self.book)
.field(
"backstop",
&self.backstop.as_ref().map(|b| b.catalog.name()),
)
.finish()
}
}