use fluent::FluentArgs;
use fluent::FluentResource;
use fluent::FluentValue;
use fluent::bundle::FluentBundle;
use lib_humus_configuration::read_from_toml_file;
use log::warn;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Debug;
use std::fmt::Display;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use crate::language::LanguageEngineLoaderError;
use crate::language::LanguageManifest;
use crate::language::UnicodeLanguageIdentifier;
use crate::language::variable_description::Variable;
pub type Bundle = FluentBundle<FluentResource, intl_memoizer::concurrent::IntlLangMemoizer>;
#[derive(Clone)]
pub struct LanguageEngine {
language_manifest: LanguageManifest,
loaded_fluent_bundles: Arc<HashMap<UnicodeLanguageIdentifier, Bundle>>,
}
impl Debug for LanguageEngine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LanguageEngine")
.field("language_manifest", &self.language_manifest)
.field(
"loaded_fluent_bundles",
&self.loaded_fluent_bundles.keys().collect::<Vec<_>>(),
)
.finish()
}
}
impl LanguageEngine {
pub fn new_empty() -> Self {
let language: UnicodeLanguageIdentifier = unic_langid::langid!("und").into();
Self {
language_manifest: LanguageManifest::new_empty(),
loaded_fluent_bundles: Arc::new(
[(
language.clone(),
FluentBundle::new_concurrent(vec![language.into()]),
)]
.into_iter()
.collect(),
),
}
}
pub fn load_from_directory(path: impl AsRef<Path>) -> Result<Self, LanguageEngineLoaderError> {
let base_dir = path.as_ref();
if !base_dir.is_dir() {
return Err(LanguageEngineLoaderError::BaseDirectoryDoesNotExist {
path: base_dir.to_path_buf(),
is_other: base_dir.exists(),
});
}
let manifest_file = base_dir.join("manifest.toml");
let manifest: LanguageManifest = read_from_toml_file(manifest_file)
.map_err(LanguageEngineLoaderError::ErrorReadingManifest)?;
match manifest.languages.get(&manifest.default_language) {
Some(description) if description.is_hidden => {
return Err(LanguageEngineLoaderError::DefaultLanguageMustNotBeHidden {
default_language: manifest.default_language,
});
}
None => {
return Err(LanguageEngineLoaderError::DefaultLanguageNotInManifest {
default_language: manifest.default_language,
});
}
_ => { }
}
let mut missing_messages: Vec<(UnicodeLanguageIdentifier, String)> = vec![];
let mut loaded_fluent_bundles: HashMap<UnicodeLanguageIdentifier, Bundle> = HashMap::new();
for (language, description) in &manifest.languages {
let path = base_dir.join(format!("{language}.ftl"));
let text = fs::read_to_string(&path).map_err(|e| {
LanguageEngineLoaderError::ErrorReadingLanguageFile {
language: language.clone(),
path: path.clone(),
io_error: e,
}
})?;
let resource = FluentResource::try_new(text).map_err(|(_, e)| {
LanguageEngineLoaderError::ErrorParsingLanguageFile {
language: language.clone(),
path: path.clone(),
errors: e,
}
})?;
let mut bundle = FluentBundle::new_concurrent(vec![language.clone().into()]);
bundle.add_resource(resource).map_err(|e| {
LanguageEngineLoaderError::ErrorAddingLanguageFile {
language: language.clone(),
path: path.clone(),
errors: e,
}
})?;
if !description.is_hidden {
for message_id in &manifest.available_messages {
if !bundle.has_message(message_id) {
missing_messages.push((language.clone(), message_id.clone()))
}
}
}
loaded_fluent_bundles.insert(language.clone(), bundle);
}
if !missing_messages.is_empty() {
let mut missing_messages_text: String = "".to_string();
for (lang, id) in missing_messages {
missing_messages_text = format!("{missing_messages_text}\t* in {lang}: {id:?}\n");
}
warn!("Some non-hidden languages are incomplete!");
warn!("Missing messages:\n{missing_messages_text}");
warn!("Those missing messages will cause template errors!");
}
Ok(Self {
language_manifest: manifest,
loaded_fluent_bundles: Arc::new(loaded_fluent_bundles),
})
}
pub fn naive_get_text(
&self,
language: Option<UnicodeLanguageIdentifier>,
message_id: &str,
args: Option<&FluentArgs>,
) -> String {
match self.get_text_with_raw_args(language, message_id, args) {
Ok(text) => {
return text;
}
Err(e) => {
log::error!("{e:?}");
}
}
if let Some(args) = args {
format!("[NOT TRANSLATEABLE {message_id:?} {args:?}]")
} else {
format!("[NOT TRANSLATEABLE {message_id:?}]")
}
}
pub fn get_text_with_args(
&self,
language: Option<UnicodeLanguageIdentifier>,
message_id: &str,
args: impl IntoIterator<Item = (impl Into<String>, Variable)>,
) -> Result<String, TextFunctionError> {
let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
if let Some(message_description) =
self.language_manifest.message_descriptions.get(message_id)
{
let mut arg_problems: TextFunctionArgumentProblems = Default::default();
let mut fluent_args = FluentArgs::new();
for (name, value) in args {
let name = name.into();
if let Some(arg_description) = message_description.arguments.get(&name) {
if !arg_description.matches_variable(&value) {
arg_problems
.description_mismatch
.push((name.clone(), value));
fluent_args.set(name, FluentValue::Error);
continue;
}
fluent_args.set(name, value.into_fluent_value(arg_description));
} else {
arg_problems.too_many.push(name);
}
}
for (name, desc) in &message_description.arguments {
if fluent_args.get(name).is_some() {
continue;
}
if let Some(value) = &desc.default_value {
fluent_args.set(name, value.clone().into_fluent_value(desc));
} else if !desc.optional {
arg_problems.missing.push(name.to_owned());
}
}
if !arg_problems.is_empty() {
return Err(TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::ArgumentError(Box::new(arg_problems)),
));
}
self.get_text_with_raw_args(Some(language), message_id, Some(&fluent_args))
} else {
let mut too_many_arguments: Vec<String> = vec![];
for (key, _) in args {
too_many_arguments.push(key.into())
}
if too_many_arguments.is_empty() {
self.get_text_with_raw_args(Some(language), message_id, None)
} else {
Err(TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::ArgumentError(Box::new(TextFunctionArgumentProblems {
too_many: too_many_arguments,
missing: vec![],
description_mismatch: vec![],
})),
))
}
}
}
pub fn get_text(
&self,
language: Option<UnicodeLanguageIdentifier>,
message_id: &str,
) -> Result<String, TextFunctionError> {
let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
if let Some(message_description) =
self.language_manifest.message_descriptions.get(message_id)
&& !message_description.arguments.is_empty()
{
let mut arg_problems: TextFunctionArgumentProblems = Default::default();
for (name, desc) in &message_description.arguments {
if !(desc.optional || desc.default_value.is_some()) {
arg_problems.missing.push(name.to_owned());
}
}
if !arg_problems.is_empty() {
return Err(TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::ArgumentError(Box::new(arg_problems)),
));
}
}
self.get_text_with_raw_args(Some(language), message_id, None)
}
pub fn get_text_with_raw_args(
&self,
language: Option<UnicodeLanguageIdentifier>,
message_id: &str,
args: Option<&FluentArgs>,
) -> Result<String, TextFunctionError> {
let language = language.unwrap_or_else(|| self.language_manifest.default_language.clone());
if !self
.language_manifest
.available_messages
.contains(message_id)
{
return Err(TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::MessageIdNotListedAvailable,
));
}
let bundle = self.loaded_fluent_bundles.get(&language).ok_or_else(|| {
TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::LanguageIsNotLoadedOrPresent,
)
})?;
let message = bundle.get_message(message_id).ok_or_else(|| {
TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::MessageNotPresentInLanguage,
)
})?;
let pattern = message.value().ok_or_else(|| {
TextFunctionError::new(
&language,
message_id,
TextFunctionErrorKind::NoPatternForMessage,
)
})?;
let mut errors = vec![];
let out = bundle.format_pattern(pattern, args, &mut errors);
if !errors.is_empty() {
log::error!(
"Non fatal problems occurred while translating text {message_id:?} for language {language}: {errors:#?}"
);
}
Ok(out.to_string())
}
pub fn language_manifest(&self) -> &LanguageManifest {
&self.language_manifest
}
}
#[derive(Debug, Default)]
pub struct TextFunctionArgumentProblems {
pub too_many: Vec<String>,
pub missing: Vec<String>,
pub description_mismatch: Vec<(String, Variable)>,
}
impl TextFunctionArgumentProblems {
pub fn is_empty(&self) -> bool {
self.too_many.is_empty() && self.missing.is_empty() && self.description_mismatch.is_empty()
}
}
impl Display for TextFunctionArgumentProblems {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"uneccessary arguments: {:?}; missing_arguments: {:?}; description mismatches: {:?}",
self.too_many, self.missing, self.description_mismatch
)
}
}
#[derive(Debug)]
pub struct TextFunctionError {
pub language: UnicodeLanguageIdentifier,
pub message_id: String,
pub kind: TextFunctionErrorKind,
}
impl TextFunctionError {
pub fn new(
language: &UnicodeLanguageIdentifier,
message_id: &str,
kind: TextFunctionErrorKind,
) -> Self {
Self {
language: language.clone(),
message_id: message_id.to_owned(),
kind,
}
}
}
#[derive(Debug)]
pub enum TextFunctionErrorKind {
MessageIdNotListedAvailable,
NoPatternForMessage,
MessageNotPresentInLanguage,
LanguageIsNotLoadedOrPresent,
ArgumentError(Box<TextFunctionArgumentProblems>),
}
impl Display for TextFunctionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let id = &self.message_id;
let language = &self.language;
match &self.kind {
TextFunctionErrorKind::MessageIdNotListedAvailable => write!(
f,
"The messagge id {id:?} isn't listed as an available id in the language manifest. If this message should exist please register its name in the available_ids array!"
),
TextFunctionErrorKind::NoPatternForMessage => write!(
f,
"Unable to translate text {id:?} for language {language}, message present but it does not carry a pattern"
),
TextFunctionErrorKind::MessageNotPresentInLanguage => write!(
f,
"Unable to translate text {id:?} for language {language}, message not present in this language"
),
TextFunctionErrorKind::LanguageIsNotLoadedOrPresent => write!(
f,
"Unable to translate texts for language {language}, language is not loaded or not present in template."
),
TextFunctionErrorKind::ArgumentError(e) => {
write!(f, "Problem with passed arguments for message {id:?}: {e}")
}
}
}
}
impl Error for TextFunctionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}