Avoid to use an hash map in translations

This commit is contained in:
Greg Burri 2025-01-07 16:47:24 +01:00
parent 03ebbb74fa
commit 91ab379718
10 changed files with 334 additions and 237 deletions

42
Cargo.lock generated
View file

@ -122,9 +122,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.84" version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1756,18 +1756,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.7" version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.7" version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1776,9 +1776,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1930,9 +1930,10 @@ dependencies = [
"rinja", "rinja",
"rinja_axum", "rinja_axum",
"ron", "ron",
"rustc-hash",
"serde", "serde",
"sqlx", "sqlx",
"strum",
"strum_macros",
"thiserror 2.0.9", "thiserror 2.0.9",
"tokio", "tokio",
"tower", "tower",
@ -2210,9 +2211,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.134" version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -2572,6 +2573,25 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"

View file

@ -23,7 +23,6 @@ ron = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
itertools = "0.14" itertools = "0.14"
rustc-hash = "2.1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] } sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
@ -34,6 +33,8 @@ rinja_axum = "0.3"
argon2 = { version = "0.5", features = ["default", "std"] } argon2 = { version = "0.5", features = ["default", "std"] }
rand_core = { version = "0.6", features = ["std"] } rand_core = { version = "0.6", features = ["std"] }
rand = "0.8" rand = "0.8"
strum = "0.26"
strum_macros = "0.26"
lettre = { version = "0.11", default-features = false, features = [ lettre = { version = "0.11", default-features = false, features = [
"smtp-transport", "smtp-transport",

View file

@ -76,6 +76,12 @@ body {
font-size: 0.5em; font-size: 0.5em;
} }
.drag-handle {
width: 20px;
height: 20px;
background-color: gray;
}
.main-container { .main-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -2,7 +2,7 @@ use rinja_axum::Template;
use crate::{ use crate::{
data::model, data::model,
translation::{Sentence, Tr}, translation::{self, Sentence, Tr},
}; };
pub struct Recipes { pub struct Recipes {

View file

@ -231,7 +231,7 @@ async fn translation(
let language = if let Some(user) = user { let language = if let Some(user) = user {
user.lang user.lang
} else { } else {
let available_codes = Tr::available_codes(); let available_codes = translation::available_codes();
let jar = CookieJar::from_headers(req.headers()); let jar = CookieJar::from_headers(req.headers());
match jar.get(consts::COOKIE_LANG_NAME) { match jar.get(consts::COOKIE_LANG_NAME) {
Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(), Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(),

View file

@ -249,7 +249,7 @@ pub async fn set_language(
Extension(user): Extension<Option<model::User>>, Extension(user): Extension<Option<model::User>>,
ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>, ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>,
) -> Result<StatusCode> { ) -> Result<StatusCode> {
if !crate::translation::Tr::available_codes() if !crate::translation::available_codes()
.iter() .iter()
.any(|&l| l == ron.lang) .any(|&l| l == ron.lang)
{ {

View file

@ -1,15 +1,16 @@
use std::{fs::File, sync::LazyLock}; use std::{fs::File, sync::LazyLock};
use ron::de::from_reader; use ron::de::from_reader;
use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
use strum::EnumCount;
use strum_macros::EnumCount;
use tracing::{event, Level}; use tracing::{event, Level};
use crate::consts; use crate::consts;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Clone)] #[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence { pub enum Sentence {
MainTitle, MainTitle = 0,
CreateNewRecipe, CreateNewRecipe,
UnpublishedRecipes, UnpublishedRecipes,
UntitledRecipe, UntitledRecipe,
@ -107,6 +108,8 @@ pub enum Sentence {
RecipeIngredientComment, RecipeIngredientComment,
} }
const DEFAULT_LANGUAGE_CODE: &str = "en";
#[derive(Clone)] #[derive(Clone)]
pub struct Tr { pub struct Tr {
lang: &'static Language, lang: &'static Language,
@ -114,61 +117,48 @@ pub struct Tr {
impl Tr { impl Tr {
pub fn new(code: &str) -> Self { pub fn new(code: &str) -> Self {
for lang in TRANSLATIONS.iter() { Self {
if lang.code == code { lang: get_language_translation(code),
return Self { lang };
} }
} }
event!(
Level::WARN,
"Unable to find translation for language {}",
code
);
Tr::new("en")
}
pub fn t(&self, sentence: Sentence) -> String { pub fn t(&self, sentence: Sentence) -> String {
match self.lang.translation.get(&sentence) { //&'static str {
Some(str) => str.clone(), self.lang.get(sentence).to_string()
None => format!( // match self.lang.translation.get(&sentence) {
"Translation missing, lang: {}/{}, element: {:?}", // Some(str) => str.clone(),
self.lang.name, self.lang.code, sentence // None => format!(
), // "Translation missing, lang: {}/{}, element: {:?}",
} // self.lang.name, self.lang.code, sentence
// ),
// }
} }
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String { pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
match self.lang.translation.get(&sentence) { // match self.lang.translation.get(&sentence) {
Some(str) => { // Some(str) => {
let mut result = str.clone(); // let mut result = str.clone();
// for p in params {
// result = result.replacen("{}", &p.to_string(), 1);
// }
// result
// }
// None => format!(
// "Translation missing, lang: {}/{}, element: {:?}",
// self.lang.name, self.lang.code, sentence
// ),
// }
let text = self.lang.get(sentence);
let mut result = text.to_string();
for p in params { for p in params {
result = result.replacen("{}", &p.to_string(), 1); result = result.replacen("{}", &p.to_string(), 1);
} }
result result
} }
None => format!(
"Translation missing, lang: {}/{}, element: {:?}",
self.lang.name, self.lang.code, sentence
),
}
}
pub fn current_lang_code(&self) -> &str { pub fn current_lang_code(&self) -> &str {
&self.lang.code &self.lang.code
} }
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()
}
} }
// #[macro_export] // #[macro_export]
@ -185,22 +175,98 @@ impl Tr {
// }; // };
// } // }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize)]
struct StoredLanguage {
code: String,
name: String,
translation: Vec<(Sentence, String)>,
}
#[derive(Debug)]
struct Language { struct Language {
code: String, code: String,
name: String, name: String,
translation: FxHashMap<Sentence, String>, translation: Vec<String>,
}
impl Language {
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
println!("!!!!!!!!!!!! {:?}", &stored_language.code);
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>> = static TRANSLATIONS: LazyLock<Vec<Language>> =
LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) { LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) {
Ok(file) => from_reader(file).unwrap_or_else(|error| { Ok(file) => {
let stored_languages: Vec<StoredLanguage> = from_reader(file).unwrap_or_else(|error| {
{
panic!( panic!(
"Failed to read translation file {}: {}", "Failed to read translation file {}: {}",
consts::TRANSLATION_FILE, consts::TRANSLATION_FILE,
error error
) )
}), }
});
stored_languages
.into_iter()
.map(Language::from_stored_language)
.collect()
}
Err(error) => { Err(error) => {
panic!( panic!(
"Failed to open translation file {}: {}", "Failed to open translation file {}: {}",

View file

@ -21,7 +21,7 @@
{% endmatch %} {% endmatch %}
<select id="select-website-language"> <select id="select-website-language">
{% for lang in Tr::available_languages() %} {% for lang in translation::available_languages() %}
<option value="{{ lang.0 }}" <option value="{{ lang.0 }}"
{%+ if tr.current_lang_code() == lang.0 %} {%+ if tr.current_lang_code() == lang.0 %}
selected selected

View file

@ -59,7 +59,7 @@
<label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label> <label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label>
<select id="select-language"> <select id="select-language">
{% for lang in Tr::available_languages() %} {% for lang in translation::available_languages() %}
<option value="{{ lang.0 }}" <option value="{{ lang.0 }}"
{%+ if recipe.lang == lang.0 %} {%+ if recipe.lang == lang.0 %}
selected selected
@ -86,6 +86,8 @@
<div id="hidden-templates"> <div id="hidden-templates">
<div class="group"> <div class="group">
<div class="drag-handle"></div>
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label> <label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
<input class="input-group-name" type="text" /> <input class="input-group-name" type="text" />
@ -100,6 +102,8 @@
</div> </div>
<div class="step"> <div class="step">
<div class="drag-handle"></div>
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label> <label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
<textarea class="text-area-step-action"></textarea> <textarea class="text-area-step-action"></textarea>

View file

@ -2,193 +2,193 @@
( (
code: "en", code: "en",
name: "English", name: "English",
translation: { translation: [
MainTitle: "Cooking Recipes", (MainTitle, "Cooking Recipes"),
CreateNewRecipe: "Create a new recipe", (CreateNewRecipe, "Create a new recipe"),
UnpublishedRecipes: "Unpublished recipes", (UnpublishedRecipes, "Unpublished recipes"),
UntitledRecipe: "Untitled recipe", (UntitledRecipe, "Untitled recipe"),
Name: "Name", (Name, "Name"),
EmailAddress: "Email address", (EmailAddress, "Email address"),
Password: "Password", (Password, "Password"),
SignOut: "Sign out", (SignOut, "Sign out"),
Save: "Save", (Save, "Save"),
NotLoggedIn: "No logged in", (NotLoggedIn, "No logged in"),
DatabaseError: "Database error", (DatabaseError, "Database error"),
SignInMenu: "Sign in", (SignInMenu, "Sign in"),
SignInTitle: "Sign in", (SignInTitle, "Sign in"),
SignInButton: "Sign in", (SignInButton, "Sign in"),
WrongEmailOrPassword: "Wrong email or password", (WrongEmailOrPassword, "Wrong email or password"),
AccountMustBeValidatedFirst: "This account must be validated first", (AccountMustBeValidatedFirst, "This account must be validated first"),
InvalidEmail: "Invalid email", (InvalidEmail, "Invalid email"),
PasswordDontMatch: "Passwords don't match", (PasswordDontMatch, "Passwords don't match"),
InvalidPassword: "Password must have at least {} characters", (InvalidPassword, "Password must have at least {} characters"),
EmailAlreadyTaken: "This email is not available", (EmailAlreadyTaken, "This email is not available"),
UnableToSendEmail: "Unable to send the validation email", (UnableToSendEmail, "Unable to send the validation email"),
ValidationSuccessful: "Email validation successful", (ValidationSuccessful, "Email validation successful"),
ValidationExpired: "The validation has expired. Try to sign up again with the same email", (ValidationExpired, "The validation has expired. Try to sign up again with the same email"),
ValidationErrorTryToSignUpAgain: "Validation error. Try to sign up again with the same email", (ValidationErrorTryToSignUpAgain, "Validation error. Try to sign up again with the same email"),
ValidationError: "Validation error", (ValidationError, "Validation error"),
ValidationUserAlreadyExists: "User already exists", (ValidationUserAlreadyExists, "User already exists"),
SignUpMenu: "Sign up", (SignUpMenu, "Sign up"),
SignUpTitle: "Sign up", (SignUpTitle, "Sign up"),
SignUpButton: "Sign up", (SignUpButton, "Sign up"),
SignUpEmailSent: "An email has been sent, follow the link to validate your account", (SignUpEmailSent, "An email has been sent), follow the link to validate your account"),
SignUpFollowEmailLink: "Follow this link to confirm your inscription: {}", (SignUpFollowEmailLink, "Follow this link to confirm your inscription, {}"),
SignUpEmailValidationSuccess: "Email validation successful, your account has been created", (SignUpEmailValidationSuccess, "Email validation successful), your account has been created"),
SignUpValidationExpired: "The validation has expired. Try to sign up again", (SignUpValidationExpired, "The validation has expired. Try to sign up again"),
SignUpValidationErrorTryAgain: "Validation error. Try to sign up again", (SignUpValidationErrorTryAgain, "Validation error. Try to sign up again"),
ChooseAPassword: "Choose a password (minimum {} characters)", (ChooseAPassword, "Choose a password (minimum {} characters)"),
ReEnterPassword: "Re-enter password", (ReEnterPassword, "Re-enter password"),
LostPassword: "Lost password", (LostPassword, "Lost password"),
AskResetButton: "Ask reset", (AskResetButton, "Ask reset"),
AskResetAlreadyLoggedInError: "Can't ask to reset password when already logged in", (AskResetAlreadyLoggedInError, "Can't ask to reset password when already logged in"),
AskResetEmailAlreadyResetError: "The password has already been reset for this email", (AskResetEmailAlreadyResetError, "The password has already been reset for this email"),
AskResetFollowEmailLink: "Follow this link to reset your password: {}", (AskResetFollowEmailLink, "Follow this link to reset your password, {}"),
AskResetEmailSent: "An email has been sent, follow the link to reset your password", (AskResetEmailSent, "An email has been sent), follow the link to reset your password"),
AskResetTokenMissing: "Reset token missing", (AskResetTokenMissing, "Reset token missing"),
AskResetTokenExpired: "Token expired, try to reset password again", (AskResetTokenExpired, "Token expired), try to reset password again"),
PasswordReset: "Your password has been reset", (PasswordReset, "Your password has been reset"),
EmailUnknown: "Email unknown", (EmailUnknown, "Email unknown"),
UnableToSendResetEmail: "Unable to send the reset password email", (UnableToSendResetEmail, "Unable to send the reset password email"),
ProfileTitle: "Profile", (ProfileTitle, "Profile"),
ProfileEmail: "Email (need to be revalidated if changed)", (ProfileEmail, "Email (need to be revalidated if changed)"),
ProfileNewPassword: "New password (minimum {} characters)", (ProfileNewPassword, "New password (minimum {} characters)"),
ProfileFollowEmailLink: "Follow this link to validate this email address: {}", (ProfileFollowEmailLink, "Follow this link to validate this email address, {}"),
ProfileEmailSent: "An email has been sent, follow the link to validate your new email", (ProfileEmailSent, "An email has been sent), follow the link to validate your new email"),
ProfileSaved: "Profile saved", (ProfileSaved, "Profile saved"),
RecipeNotAllowedToEdit: "Not allowed to edit this recipe", (RecipeNotAllowedToEdit, "Not allowed to edit this recipe"),
RecipeNotAllowedToView: "Not allowed the view the recipe {}", (RecipeNotAllowedToView, "Not allowed the view the recipe {}"),
RecipeNotFound: "Recipe not found", (RecipeNotFound, "Recipe not found"),
RecipeTitle : "Title", (RecipeTitle, "Title"),
RecipeDescription : "Description", (RecipeDescription, "Description"),
RecipeServings : "Servings", (RecipeServings, "Servings"),
RecipeEstimatedTime : "Estimated time [min]", (RecipeEstimatedTime, "Estimated time [min]"),
RecipeDifficulty : "Difficulty", (RecipeDifficulty, "Difficulty"),
RecipeDifficultyEasy : "Easy", (RecipeDifficultyEasy, "Easy"),
RecipeDifficultyMedium : "Medium", (RecipeDifficultyMedium, "Medium"),
RecipeDifficultyHard : "Hard", (RecipeDifficultyHard, "Hard"),
RecipeTags : "Tags", (RecipeTags, "Tags"),
RecipeLanguage : "Language", (RecipeLanguage, "Language"),
RecipeIsPublished : "Is published", (RecipeIsPublished, "Is published"),
RecipeDelete : "Delete recipe", (RecipeDelete, "Delete recipe"),
RecipeAddAGroup : "Add a group", (RecipeAddAGroup, "Add a group"),
RecipeRemoveGroup : "Remove group", (RecipeRemoveGroup, "Remove group"),
RecipeGroupName : "Name", (RecipeGroupName, "Name"),
RecipeGroupComment : "Comment", (RecipeGroupComment, "Comment"),
RecipeAddAStep : "Add a step", (RecipeAddAStep, "Add a step"),
RecipeRemoveStep : "Remove step", (RecipeRemoveStep, "Remove step"),
RecipeStepAction : "Action", (RecipeStepAction, "Action"),
RecipeAddAnIngredient : "Add an ingredient", (RecipeAddAnIngredient, "Add an ingredient"),
RecipeRemoveIngredient : "Remove ingredient", (RecipeRemoveIngredient, "Remove ingredient"),
RecipeIngredientName : "Name", (RecipeIngredientName, "Name"),
RecipeIngredientQuantity : "Quantity", (RecipeIngredientQuantity, "Quantity"),
RecipeIngredientUnit : "Unit", (RecipeIngredientUnit, "Unit"),
RecipeIngredientComment : "Comment", (RecipeIngredientComment, "Comment"),
} ]
), ),
( (
code: "fr", code: "fr",
name: "Français", name: "Français",
translation: { translation: [
MainTitle: "Recettes de Cuisine", (MainTitle, "Recettes de Cuisine"),
CreateNewRecipe: "Créer une nouvelle recette", (CreateNewRecipe, "Créer une nouvelle recette"),
UnpublishedRecipes: "Recettes non-publiés", (UnpublishedRecipes, "Recettes non-publiés"),
UntitledRecipe: "Recette sans nom", (UntitledRecipe, "Recette sans nom"),
Name: "Nom", (Name, "Nom"),
EmailAddress: "Adresse email", (EmailAddress, "Adresse email"),
Password: "Mot de passe", (Password, "Mot de passe"),
SignOut: "Se déconnecter", (SignOut, "Se déconnecter"),
Save: "Sauvegarder", (Save, "Sauvegarder"),
NotLoggedIn: "Pas connecté", (NotLoggedIn, "Pas connecté"),
DatabaseError: "Erreur de la base de données", (DatabaseError, "Erreur de la base de données"),
SignInMenu: "Se connecter", (SignInMenu, "Se connecter"),
SignInTitle: "Se connecter", (SignInTitle, "Se connecter"),
SignInButton: "Se connecter", (SignInButton, "Se connecter"),
WrongEmailOrPassword: "Mot de passe ou email invalide", (WrongEmailOrPassword, "Mot de passe ou email invalide"),
AccountMustBeValidatedFirst: "Ce compte doit d'abord être validé", (AccountMustBeValidatedFirst, "Ce compte doit d'abord être validé"),
InvalidEmail: "Adresse email invalide", (InvalidEmail, "Adresse email invalide"),
PasswordDontMatch: "Les mots de passe ne correspondent pas", (PasswordDontMatch, "Les mots de passe ne correspondent pas"),
InvalidPassword: "Le mot de passe doit avoir au moins {} caractères", (InvalidPassword, "Le mot de passe doit avoir au moins {} caractères"),
EmailAlreadyTaken: "Cette adresse email n'est pas disponible", (EmailAlreadyTaken, "Cette adresse email n'est pas disponible"),
UnableToSendEmail: "L'email de validation n'a pas pu être envoyé", (UnableToSendEmail, "L'email de validation n'a pas pu être envoyé"),
ValidationSuccessful: "Email validé avec succès", (ValidationSuccessful, "Email validé avec succès"),
ValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau avec la même adresse email", (ValidationExpired, "La validation a expiré. Essayez de vous inscrire à nouveau avec la même adresse email"),
ValidationErrorTryToSignUpAgain: "Erreur de validation. Essayez de vous inscrire à nouveau avec la même adresse email", (ValidationErrorTryToSignUpAgain, "Erreur de validation. Essayez de vous inscrire à nouveau avec la même adresse email"),
ValidationError: "Erreur de validation", (ValidationError, "Erreur de validation"),
ValidationUserAlreadyExists: "Utilisateur déjà existant", (ValidationUserAlreadyExists, "Utilisateur déjà existant"),
SignUpMenu: "S'inscrire", (SignUpMenu, "S'inscrire"),
SignUpTitle: "Inscription", (SignUpTitle, "Inscription"),
SignUpButton: "Valider", (SignUpButton, "Valider"),
SignUpEmailSent: "Un email a été envoyé, suivez le lien pour valider votre compte", (SignUpEmailSent, "Un email a été envoyé), suivez le lien pour valider votre compte"),
SignUpFollowEmailLink: "Suivez ce lien pour valider votre inscription: {}", (SignUpFollowEmailLink, "Suivez ce lien pour valider votre inscription, {}"),
SignUpEmailValidationSuccess: "La validation de votre email s'est déroulée avec succès, votre compte a été créé", (SignUpEmailValidationSuccess, "La validation de votre email s'est déroulée avec succès), votre compte a été créé"),
SignUpValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau", (SignUpValidationExpired, "La validation a expiré. Essayez de vous inscrire à nouveau"),
SignUpValidationErrorTryAgain: "Erreur de validation. Essayez de vous inscrire à nouveau", (SignUpValidationErrorTryAgain, "Erreur de validation. Essayez de vous inscrire à nouveau"),
ChooseAPassword: "Choisir un mot de passe (minimum {} caractères)", (ChooseAPassword, "Choisir un mot de passe (minimum {} caractères)"),
ReEnterPassword: "Entrez à nouveau le mot de passe", (ReEnterPassword, "Entrez à nouveau le mot de passe"),
LostPassword: "Mot de passe perdu", (LostPassword, "Mot de passe perdu"),
AskResetButton: "Demander la réinitialisation", (AskResetButton, "Demander la réinitialisation"),
AskResetAlreadyLoggedInError: "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté", (AskResetAlreadyLoggedInError, "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté"),
AskResetEmailAlreadyResetError: "Le mot de passe a déjà été réinitialisé pour cette adresse email", (AskResetEmailAlreadyResetError, "Le mot de passe a déjà été réinitialisé pour cette adresse email"),
AskResetFollowEmailLink: "Suivez ce lien pour réinitialiser votre mot de passe: {}", (AskResetFollowEmailLink, "Suivez ce lien pour réinitialiser votre mot de passe, {}"),
AskResetEmailSent: "Un email a été envoyé, suivez le lien pour réinitialiser votre mot de passe", (AskResetEmailSent, "Un email a été envoyé), suivez le lien pour réinitialiser votre mot de passe"),
AskResetTokenMissing: "Jeton de réinitialisation manquant", (AskResetTokenMissing, "Jeton de réinitialisation manquant"),
AskResetTokenExpired: "Jeton expiré, essayez de réinitialiser votre mot de passe à nouveau", (AskResetTokenExpired, "Jeton expiré), essayez de réinitialiser votre mot de passe à nouveau"),
PasswordReset: "Votre mot de passe a été réinitialisé", (PasswordReset, "Votre mot de passe a été réinitialisé"),
EmailUnknown: "Email inconnu", (EmailUnknown, "Email inconnu"),
UnableToSendResetEmail: "Impossible d'envoyer l'email pour la réinitialisation du mot de passe", (UnableToSendResetEmail, "Impossible d'envoyer l'email pour la réinitialisation du mot de passe"),
ProfileTitle: "Profile", (ProfileTitle, "Profile"),
ProfileEmail: "Email (doit être revalidé si changé)", (ProfileEmail, "Email (doit être revalidé si changé)"),
ProfileNewPassword: "Nouveau mot de passe (minimum {} caractères)", (ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"),
ProfileFollowEmailLink: "Suivez ce lien pour valider l'adresse email: {}", (ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"),
ProfileEmailSent: "Un email a été envoyé, suivez le lien pour valider la nouvelle adresse email", (ProfileEmailSent, "Un email a été envoyé), suivez le lien pour valider la nouvelle adresse email"),
ProfileSaved: "Profile sauvegardé", (ProfileSaved, "Profile sauvegardé"),
RecipeNotAllowedToEdit: "Vous n'êtes pas autorisé à éditer cette recette", (RecipeNotAllowedToEdit, "Vous n'êtes pas autorisé à éditer cette recette"),
RecipeNotAllowedToView: "Vous n'êtes pas autorisé à voir la recette {}", (RecipeNotAllowedToView, "Vous n'êtes pas autorisé à voir la recette {}"),
RecipeNotFound: "Recette non-trouvée", (RecipeNotFound, "Recette non-trouvée"),
RecipeTitle : "Titre", (RecipeTitle, "Titre"),
RecipeDescription : "Description", (RecipeDescription, "Description"),
RecipeServings : "Nombre de personnes", (RecipeServings, "Nombre de personnes"),
RecipeEstimatedTime : "Temps estimé", (RecipeEstimatedTime, "Temps estimé"),
RecipeDifficulty : "Difficulté", (RecipeDifficulty, "Difficulté"),
RecipeDifficultyEasy : "Facile", (RecipeDifficultyEasy, "Facile"),
RecipeDifficultyMedium : "Moyen", (RecipeDifficultyMedium, "Moyen"),
RecipeDifficultyHard : "Difficile", (RecipeDifficultyHard, "Difficile"),
RecipeTags : "Tags", (RecipeTags, "Tags"),
RecipeLanguage : "Langue", (RecipeLanguage, "Langue"),
RecipeIsPublished : "Est publié", (RecipeIsPublished, "Est publié"),
RecipeDelete : "Supprimer la recette", (RecipeDelete, "Supprimer la recette"),
RecipeAddAGroup : "Ajouter un groupe", (RecipeAddAGroup, "Ajouter un groupe"),
RecipeRemoveGroup : "Supprimer le groupe", (RecipeRemoveGroup, "Supprimer le groupe"),
RecipeGroupName : "Nom", (RecipeGroupName, "Nom"),
RecipeGroupComment : "Commentaire", (RecipeGroupComment, "Commentaire"),
RecipeAddAStep : "Ajouter une étape", (RecipeAddAStep, "Ajouter une étape"),
RecipeRemoveStep : "Supprimer l'étape", (RecipeRemoveStep, "Supprimer l'étape"),
RecipeStepAction : "Action", (RecipeStepAction, "Action"),
RecipeAddAnIngredient : "Ajouter un ingrédient", (RecipeAddAnIngredient, "Ajouter un ingrédient"),
RecipeRemoveIngredient : "Supprimer l'ingrédient", (RecipeRemoveIngredient, "Supprimer l'ingrédient"),
RecipeIngredientName : "Nom", (RecipeIngredientName, "Nom"),
RecipeIngredientQuantity : "Quantité", (RecipeIngredientQuantity, "Quantité"),
RecipeIngredientUnit : "Unité", (RecipeIngredientUnit, "Unité"),
RecipeIngredientComment : "Commentaire", (RecipeIngredientComment, "Commentaire"),
} ]
) )
] ]