268 lines
6.2 KiB
Rust
268 lines
6.2 KiB
Rust
use std::{fs::File, sync::LazyLock};
|
|
|
|
use ron::de::from_reader;
|
|
use serde::Deserialize;
|
|
use strum::EnumCount;
|
|
use strum_macros::EnumCount;
|
|
use tracing::{event, Level};
|
|
|
|
use crate::{consts, utils};
|
|
|
|
#[derive(Debug, Clone, EnumCount, Deserialize)]
|
|
pub enum Sentence {
|
|
MainTitle = 0,
|
|
CreateNewRecipe,
|
|
UnpublishedRecipes,
|
|
UntitledRecipe,
|
|
|
|
Name,
|
|
EmailAddress,
|
|
Password,
|
|
|
|
SignOut,
|
|
Save,
|
|
NotLoggedIn,
|
|
|
|
DatabaseError,
|
|
TemplateError,
|
|
|
|
// Sign in page.
|
|
SignInMenu,
|
|
SignInTitle,
|
|
SignInButton,
|
|
WrongEmailOrPassword,
|
|
|
|
// Sign up page.
|
|
SignUpMenu,
|
|
SignUpTitle,
|
|
SignUpButton,
|
|
SignUpEmailSent,
|
|
SignUpFollowEmailLink,
|
|
SignUpEmailValidationSuccess,
|
|
SignUpValidationExpired,
|
|
SignUpValidationErrorTryAgain,
|
|
SignUpClosed,
|
|
ChooseAPassword,
|
|
ReEnterPassword,
|
|
|
|
AccountMustBeValidatedFirst,
|
|
InvalidEmail,
|
|
PasswordDontMatch,
|
|
InvalidPassword,
|
|
EmailAlreadyTaken,
|
|
UnableToSendEmail,
|
|
|
|
// Validation.
|
|
ValidationSuccessful,
|
|
ValidationExpired,
|
|
ValidationErrorTryToSignUpAgain,
|
|
ValidationError,
|
|
ValidationUserAlreadyExists,
|
|
|
|
// Reset password page.
|
|
LostPassword,
|
|
AskResetButton,
|
|
AskResetAlreadyLoggedInError,
|
|
AskResetEmailAlreadyResetError,
|
|
AskResetFollowEmailLink,
|
|
AskResetEmailSent,
|
|
AskResetTokenMissing,
|
|
AskResetTokenExpired,
|
|
PasswordReset,
|
|
EmailUnknown,
|
|
UnableToSendResetEmail,
|
|
|
|
// Profile
|
|
ProfileTitle,
|
|
ProfileEmail,
|
|
ProfileNewPassword,
|
|
ProfileFollowEmailLink,
|
|
ProfileEmailSent,
|
|
ProfileSaved,
|
|
|
|
// Recipe.
|
|
RecipeNotAllowedToEdit,
|
|
RecipeNotAllowedToView,
|
|
RecipeNotFound,
|
|
RecipeTitle,
|
|
RecipeDescription,
|
|
RecipeServings,
|
|
RecipeEstimatedTime,
|
|
RecipeDifficulty,
|
|
RecipeDifficultyEasy,
|
|
RecipeDifficultyMedium,
|
|
RecipeDifficultyHard,
|
|
RecipeTags,
|
|
RecipeLanguage,
|
|
RecipeIsPublished,
|
|
RecipeDelete,
|
|
RecipeAddAGroup,
|
|
RecipeRemoveGroup,
|
|
RecipeGroupName,
|
|
RecipeGroupComment,
|
|
RecipeAddAStep,
|
|
RecipeRemoveStep,
|
|
RecipeStepAction,
|
|
RecipeAddAnIngredient,
|
|
RecipeRemoveIngredient,
|
|
RecipeIngredientName,
|
|
RecipeIngredientQuantity,
|
|
RecipeIngredientUnit,
|
|
RecipeIngredientComment,
|
|
|
|
// View Recipe.
|
|
RecipeOneServing,
|
|
RecipeSomeServings,
|
|
RecipeEstimatedTimeMinAbbreviation,
|
|
}
|
|
|
|
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
|
|
pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
|
|
|
|
#[derive(Clone)]
|
|
pub struct Tr {
|
|
lang: &'static Language,
|
|
}
|
|
|
|
impl Tr {
|
|
pub fn new(code: &str) -> Self {
|
|
Self {
|
|
lang: get_language_translation(code),
|
|
}
|
|
}
|
|
|
|
pub fn t(&self, sentence: Sentence) -> &'static str {
|
|
self.lang.get(sentence)
|
|
}
|
|
|
|
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
|
|
let text = self.lang.get(sentence);
|
|
let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
|
|
utils::substitute(
|
|
text,
|
|
PLACEHOLDER_SUBSTITUTE,
|
|
¶ms_as_string
|
|
.iter()
|
|
.map(AsRef::as_ref)
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
|
|
pub fn current_lang_code(&self) -> &str {
|
|
&self.lang.code
|
|
}
|
|
}
|
|
|
|
// #[macro_export]
|
|
// macro_rules! t {
|
|
// ($self:expr, $str:expr) => {
|
|
// $self.t($str)
|
|
// };
|
|
// ($self:expr, $str:expr, $( $x:expr ),+ ) => {
|
|
// {
|
|
// let mut result = $self.t($str);
|
|
// $( result = result.replacen("{}", &$x.to_string(), 1); )+
|
|
// result
|
|
// }
|
|
// };
|
|
// }
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct StoredLanguage {
|
|
code: String,
|
|
name: String,
|
|
translation: Vec<(Sentence, String)>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Language {
|
|
code: String,
|
|
name: String,
|
|
translation: Vec<String>,
|
|
}
|
|
|
|
impl Language {
|
|
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
|
|
Self {
|
|
code: stored_language.code,
|
|
name: stored_language.name,
|
|
translation: {
|
|
let mut translation = vec![String::new(); Sentence::COUNT];
|
|
for (sentence, text) in stored_language.translation {
|
|
translation[sentence as usize] = text;
|
|
}
|
|
translation
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn get(&'static self, sentence: Sentence) -> &'static str {
|
|
let text: &str = self
|
|
.translation
|
|
.get(sentence.clone() as usize)
|
|
.unwrap()
|
|
.as_ref();
|
|
if text.is_empty() && self.code != DEFAULT_LANGUAGE_CODE {
|
|
return get_language_translation(DEFAULT_LANGUAGE_CODE).get(sentence);
|
|
}
|
|
text
|
|
}
|
|
}
|
|
|
|
pub fn available_languages() -> Vec<(&'static str, &'static str)> {
|
|
TRANSLATIONS
|
|
.iter()
|
|
.map(|tr| (tr.code.as_ref(), tr.name.as_ref()))
|
|
.collect()
|
|
}
|
|
|
|
pub fn available_codes() -> Vec<&'static str> {
|
|
TRANSLATIONS.iter().map(|tr| tr.code.as_ref()).collect()
|
|
}
|
|
|
|
fn get_language_translation(code: &str) -> &'static Language {
|
|
for lang in TRANSLATIONS.iter() {
|
|
if lang.code == code {
|
|
return lang;
|
|
}
|
|
}
|
|
|
|
event!(
|
|
Level::WARN,
|
|
"Unable to find translation for language {}",
|
|
code
|
|
);
|
|
|
|
if code != DEFAULT_LANGUAGE_CODE {
|
|
get_language_translation(DEFAULT_LANGUAGE_CODE)
|
|
} else {
|
|
// 'DEFAULT_LANGUAGE_CODE' must exist.
|
|
panic!("Unable to find language {}", code);
|
|
}
|
|
}
|
|
|
|
static TRANSLATIONS: LazyLock<Vec<Language>> =
|
|
LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) {
|
|
Ok(file) => {
|
|
let stored_languages: Vec<StoredLanguage> = from_reader(file).unwrap_or_else(|error| {
|
|
{
|
|
panic!(
|
|
"Failed to read translation file {}: {}",
|
|
consts::TRANSLATION_FILE,
|
|
error
|
|
)
|
|
}
|
|
});
|
|
stored_languages
|
|
.into_iter()
|
|
.map(Language::from_stored_language)
|
|
.collect()
|
|
}
|
|
Err(error) => {
|
|
panic!(
|
|
"Failed to open translation file {}: {}",
|
|
consts::TRANSLATION_FILE,
|
|
error
|
|
)
|
|
}
|
|
});
|