use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use std::path::Path;
use mf2_catalog::{Catalog, Dir};
use mf2_runtime::{
BidiStrategy, ErrorSink, FormatContext, FormatError, Formatter, NoErrors, Sink, TimeZone,
};
use super::Error;
use super::locale::{LocaleSource, best};
use crate::{CatalogFile, Corpus, Message};
pub struct Catalogs {
corpus: &'static Corpus,
by_locale: Vec<Option<Catalog>>,
available: Vec<(&'static str, Dir)>,
cx: FormatContext,
}
impl fmt::Debug for Catalogs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Catalogs")
.field("source_locale", &self.corpus.source_locale())
.field("catalogs", &self.by_locale)
.field("context", &self.cx)
.finish_non_exhaustive()
}
}
trait Writes {
fn write_to(&self, f: &Formatter<'_>, out: &mut dyn Sink, errs: &mut dyn ErrorSink);
}
impl<M: Message + ?Sized> Writes for M {
fn write_to(&self, f: &Formatter<'_>, out: &mut dyn Sink, errs: &mut dyn ErrorSink) {
self.write(f, out, errs);
}
}
impl Catalogs {
pub fn embedded(corpus: &'static Corpus) -> Result<Catalogs, Error> {
Catalogs::load(corpus, |tag, file| {
let bytes = file
.bytes()
.ok_or_else(|| Error::NotEmbedded(tag.to_owned()))?;
Catalog::from_static(bytes, corpus.manifest_hash())
.map(Some)
.map_err(|source| Error::Catalog {
locale: tag.to_owned(),
source,
})
})
}
pub fn from_directory(
corpus: &'static Corpus,
directory: impl AsRef<Path>,
) -> Result<Catalogs, Error> {
Catalogs::files(corpus, directory.as_ref(), true)
}
pub(crate) fn files(
corpus: &'static Corpus,
directory: &Path,
partial: bool,
) -> Result<Catalogs, Error> {
Catalogs::load(corpus, |tag, file| {
let path = directory.join(file.file_name());
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(source)
if partial
&& source.kind() == std::io::ErrorKind::NotFound
&& tag != corpus.source_locale() =>
{
return Ok(None);
}
Err(source) => return Err(Error::Io { path, source }),
};
let actual = mf2_catalog::content_hash(&bytes);
if name_hash(file.file_name()) != Some(actual.as_str()) {
return Err(Error::ContentMismatch { path, actual });
}
Catalog::new(bytes, corpus.manifest_hash())
.map(Some)
.map_err(|source| Error::Catalog {
locale: tag.to_owned(),
source,
})
})
}
fn load(
corpus: &'static Corpus,
mut read: impl FnMut(&str, &CatalogFile) -> Result<Option<Catalog>, Error>,
) -> Result<Catalogs, Error> {
let mut by_locale = Vec::with_capacity(corpus.locales().len());
let mut available = Vec::with_capacity(corpus.locales().len());
for &(tag, dir) in corpus.locales() {
let file = corpus
.catalogs()
.iter()
.find(|f| f.tag() == tag)
.ok_or_else(|| Error::MissingCatalog(tag.to_owned()))?;
let catalog = read(tag, file)?;
if let Some(catalog) = &catalog {
if catalog.locale() != tag {
return Err(Error::LocaleMismatch {
expected: tag.to_owned(),
actual: catalog.locale().to_owned(),
});
}
available.push((tag, dir));
}
by_locale.push(catalog);
}
let mut cx = FormatContext::new(&mf2_host_std::HOST);
cx.bidi = BidiStrategy::None;
cx.time_zone = super::zone::system();
Ok(Catalogs {
corpus,
by_locale,
available,
cx,
})
}
#[must_use]
pub const fn corpus(&self) -> &'static Corpus {
self.corpus
}
pub fn locales(&self) -> impl ExactSizeIterator<Item = &'static str> + '_ {
self.available.iter().map(|(tag, _)| *tag)
}
#[must_use]
pub fn format(&self, locale: &str, message: &impl Message) -> String {
self.format_at(self.index_or_source(locale), message)
}
#[must_use]
pub fn format_with_errors(
&self,
locale: &str,
message: &impl Message,
) -> (String, Vec<FormatError>) {
self.format_at_with_errors(self.index_or_source(locale), message)
}
#[must_use]
pub fn formatter(&self, locale: &str) -> Option<Formatter<'_>> {
self.formatter_at(self.index_or_source(locale))
}
#[must_use]
pub const fn bidi(&self) -> BidiStrategy {
self.cx.bidi
}
pub const fn set_bidi(&mut self, bidi: BidiStrategy) {
self.cx.bidi = bidi;
}
#[must_use]
pub const fn time_zone(&self) -> TimeZone {
self.cx.time_zone
}
pub const fn set_time_zone(&mut self, zone: TimeZone) {
self.cx.time_zone = zone;
}
pub(crate) fn index(&self, locale: &str) -> Option<usize> {
let tag = best(self.corpus.language_matching(), [locale], &self.available)?;
self.position(tag)
}
fn index_or_source(&self, locale: &str) -> usize {
self.index(locale).unwrap_or_else(|| self.source())
}
pub(crate) fn typed_index(&self, tag: &str) -> usize {
match self.position(tag) {
Some(index) if self.catalog(index).is_some() => index,
_ => self.index_or_source(tag),
}
}
pub(crate) fn source(&self) -> usize {
self.position(self.corpus.source_locale()).unwrap_or(0)
}
fn position(&self, tag: &str) -> Option<usize> {
self.corpus.locales().iter().position(|(t, _)| *t == tag)
}
pub(crate) fn system_choice(&self) -> (usize, LocaleSource) {
let system = sys_locale::get_locales().collect::<Vec<_>>();
let matching = self.corpus.language_matching();
match best(matching, system.iter().map(String::as_str), &self.available)
.and_then(|tag| self.position(tag))
{
Some(i) => (i, LocaleSource::System),
None => (self.source(), LocaleSource::Source),
}
}
pub(crate) fn tag(&self, index: usize) -> &'static str {
self.corpus
.locales()
.get(index)
.map_or(self.corpus.source_locale(), |(tag, _)| tag)
}
pub(crate) fn dir(&self, index: usize) -> Dir {
self.corpus
.locales()
.get(index)
.map_or(Dir::Ltr, |(_, dir)| *dir)
}
pub(crate) fn catalog(&self, index: usize) -> Option<&Catalog> {
self.by_locale.get(index)?.as_ref()
}
pub(crate) fn context(&self) -> &FormatContext {
&self.cx
}
pub(crate) fn formatter_at(&self, index: usize) -> Option<Formatter<'_>> {
Some(Formatter::new(
self.catalog(index)?,
self.corpus.registry(),
&self.cx,
))
}
pub(crate) fn format_at(&self, index: usize, message: &impl Message) -> String {
self.text_at(index, message)
}
pub(crate) fn format_at_with_errors(
&self,
index: usize,
message: &impl Message,
) -> (String, Vec<FormatError>) {
self.text_with_errors_at(index, message)
}
fn text_at(&self, index: usize, message: &dyn Writes) -> String {
let mut out = String::new();
if let Some(f) = self.formatter_at(index) {
message.write_to(&f, &mut out, &mut NoErrors);
}
out
}
fn text_with_errors_at(
&self,
index: usize,
message: &dyn Writes,
) -> (String, Vec<FormatError>) {
let mut out = String::new();
let mut errors = Vec::new();
if let Some(f) = self.formatter_at(index) {
message.write_to(&f, &mut out, &mut errors);
}
(out, errors)
}
}
fn name_hash(file_name: &str) -> Option<&str> {
let (_, hash) = file_name.strip_suffix(".mf2b")?.rsplit_once('.')?;
(hash.len() == mf2_catalog::CONTENT_HASH_LEN).then_some(hash)
}