aboutsummaryrefslogtreecommitdiff
path: root/common/src
diff options
context:
space:
mode:
Diffstat (limited to 'common/src')
-rw-r--r--common/src/Common/Message/Key.hs150
-rw-r--r--common/src/Common/Message/Lang.hs7
-rw-r--r--common/src/Common/Message/Translation.hs655
-rw-r--r--common/src/Common/Model.hs26
-rw-r--r--common/src/Common/Model/Category.hs24
-rw-r--r--common/src/Common/Model/CategoryPage.hs18
-rw-r--r--common/src/Common/Model/CreateCategoryForm.hs15
-rw-r--r--common/src/Common/Model/CreateIncomeForm.hs15
-rw-r--r--common/src/Common/Model/CreatePaymentForm.hs21
-rw-r--r--common/src/Common/Model/Currency.hs12
-rw-r--r--common/src/Common/Model/EditCategoryForm.hs18
-rw-r--r--common/src/Common/Model/EditIncome.hs17
-rw-r--r--common/src/Common/Model/EditIncomeForm.hs18
-rw-r--r--common/src/Common/Model/EditPaymentForm.hs23
-rw-r--r--common/src/Common/Model/Email.hs12
-rw-r--r--common/src/Common/Model/ExceedingPayer.hs16
-rw-r--r--common/src/Common/Model/Frequency.hs14
-rw-r--r--common/src/Common/Model/Income.hs27
-rw-r--r--common/src/Common/Model/IncomeHeader.hs18
-rw-r--r--common/src/Common/Model/IncomePage.hs19
-rw-r--r--common/src/Common/Model/Init.hs18
-rw-r--r--common/src/Common/Model/Password.hs12
-rw-r--r--common/src/Common/Model/Payment.hs33
-rw-r--r--common/src/Common/Model/PaymentHeader.hs18
-rw-r--r--common/src/Common/Model/PaymentPage.hs21
-rw-r--r--common/src/Common/Model/SignInForm.hs15
-rw-r--r--common/src/Common/Model/Stats.hs23
-rw-r--r--common/src/Common/Model/User.hs27
-rw-r--r--common/src/Common/Msg.hs13
-rw-r--r--common/src/Common/Util/Text.hs49
-rw-r--r--common/src/Common/Util/Time.hs26
-rw-r--r--common/src/Common/Util/Validation.hs13
-rw-r--r--common/src/Common/Validation/Atomic.hs61
-rw-r--r--common/src/Common/Validation/Category.hs15
-rw-r--r--common/src/Common/Validation/Income.hs17
-rw-r--r--common/src/Common/Validation/Payment.hs31
-rw-r--r--common/src/Common/Validation/SignIn.hs17
-rw-r--r--common/src/Common/View/Format.hs78
38 files changed, 1612 insertions, 0 deletions
diff --git a/common/src/Common/Message/Key.hs b/common/src/Common/Message/Key.hs
new file mode 100644
index 0000000..f3b0837
--- /dev/null
+++ b/common/src/Common/Message/Key.hs
@@ -0,0 +1,150 @@
+module Common.Message.Key
+ ( Key(..)
+ ) where
+
+import Data.Text
+
+data Key =
+
+ App_Title
+
+ | Category_Add
+ | Category_Clone
+ | Category_Color
+ | Category_DeleteConfirm
+ | Category_Edit
+ | Category_Empty
+ | Category_Name
+ | Category_NotDeleted
+ | Category_Title
+ | Category_Used
+
+ | Date_Long Int Text Int
+ | Date_Short Int Int Int
+ | Date_ShortMonthAndYear Int Int
+
+ | Dialog_Confirm
+ | Dialog_Undo
+
+ | Error_CategoryCreate
+ | Error_CategoryDelete
+ | Error_CategoryEdit
+ | Error_IncomeCreate
+ | Error_IncomeDelete
+ | Error_IncomeEdit
+ | Error_PaymentCreate
+ | Error_PaymentDelete
+ | Error_PaymentEdit
+ | Error_SignOut
+
+ | Form_AlreadyExists
+ | Form_NonEmpty
+ | Form_MinChars Int
+ | Form_NonNullNumber
+ | Form_GreaterIntThan Int
+ | Form_InvalidCategory
+ | Form_InvalidColor
+ | Form_InvalidDate
+ | Form_InvalidInt
+ | Form_InvalidString
+ | Form_SmallerIntThan Int
+
+ | HttpError_BadPayload
+ | HttpError_BadUrl
+ | HttpError_NetworkError
+ | HttpError_Timeout
+
+ | Income_AddLong
+ | Income_AddShort
+ | Income_Name
+ | Income_Amount
+ | Income_Clone
+ | Income_CumulativeSince Text
+ | Income_Date
+ | Income_DeleteConfirm
+ | Income_Edit
+ | Income_Empty
+ | Income_MonthlyNet
+ | Income_Title
+
+ | Month_January
+ | Month_February
+ | Month_March
+ | Month_April
+ | Month_May
+ | Month_June
+ | Month_July
+ | Month_August
+ | Month_September
+ | Month_October
+ | Month_November
+ | Month_December
+
+ | PageNotFound_Title
+
+ | Payment_Add
+ | Payment_Balanced
+ | Payment_By Text Text
+ | Payment_Category
+ | Payment_CloneLong
+ | Payment_CloneShort
+ | Payment_Cost
+ | Payment_Date
+ | Payment_Delete
+ | Payment_DeleteConfirm
+ | Payment_EditLong
+ | Payment_EditShort
+ | Payment_Empty
+ | Payment_Frequency
+ | Payment_InvalidFrequency
+ | Payment_Many
+ | Payment_MonthlyFemale
+ | Payment_MonthlyMale
+ | Payment_Name
+ | Payment_NotDeleted
+ | Payment_One
+ | Payment_PunctualFemale
+ | Payment_PunctualMale
+ | Payment_Title
+ | Payment_User
+ | Payment_Worth Text Text
+
+ | Search_Monthly
+ | Search_Name
+ | Search_Punctual
+
+ | Secure_Forbidden
+ | Secure_Unauthorized
+
+ | SignIn_Button
+ | SignIn_DisconnectSuccess
+ | SignIn_InvalidCredentials
+ | SignIn_EmailLabel
+ | SignIn_PasswordLabel
+
+ | Statistics_Title
+ | Statistics_ByMonthsAndMean Text Text
+ | Statistics_TotalPayments
+ | Statistics_TotalIncomes
+
+ | WeeklyReport_Empty
+ | WeeklyReport_IncomesCreated Int
+ | WeeklyReport_IncomesDeleted Int
+ | WeeklyReport_IncomesEdited Int
+ | WeeklyReport_IncomeCreated Int
+ | WeeklyReport_IncomeDeleted Int
+ | WeeklyReport_IncomeEdited Int
+ | WeeklyReport_PayedFor Text Text Text Text
+ | WeeklyReport_PayedForNot Text Text Text Text
+ | WeeklyReport_PayedFrom Text Text Text
+ | WeeklyReport_PayedFromNot Text Text Text
+ | WeeklyReport_PaymentsCreated Int
+ | WeeklyReport_PaymentsDeleted Int
+ | WeeklyReport_PaymentsEdited Int
+ | WeeklyReport_PaymentCreated Int
+ | WeeklyReport_PaymentDeleted Int
+ | WeeklyReport_PaymentEdited Int
+ | WeeklyReport_Title
+
+ | NotFound_Message
+ | NotFound_LinkMessage
diff --git a/common/src/Common/Message/Lang.hs b/common/src/Common/Message/Lang.hs
new file mode 100644
index 0000000..0a32ede
--- /dev/null
+++ b/common/src/Common/Message/Lang.hs
@@ -0,0 +1,7 @@
+module Common.Message.Lang
+ ( Lang(..)
+ ) where
+
+data Lang =
+ English
+ | French
diff --git a/common/src/Common/Message/Translation.hs b/common/src/Common/Message/Translation.hs
new file mode 100644
index 0000000..222e669
--- /dev/null
+++ b/common/src/Common/Message/Translation.hs
@@ -0,0 +1,655 @@
+module Common.Message.Translation
+ ( get
+ ) where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+
+import Common.Message.Key
+import Common.Message.Lang (Lang (..))
+
+get :: Lang -> Key -> Text
+get = m
+
+m :: Lang -> Key -> Text
+
+m l App_Title =
+ case l of
+ English -> "Shared Cost"
+ French -> "Partage des frais"
+
+m l Category_Add =
+ case l of
+ English -> "Add an category"
+ French -> "Ajouter une catégorie"
+
+m l Category_Clone =
+ case l of
+ English -> "Clone an category"
+ French -> "Cloner une catégorie"
+
+m l Category_Color =
+ case l of
+ English -> "Color"
+ French -> "Couleur"
+
+m l Category_DeleteConfirm =
+ case l of
+ English -> "Are you sure to delete this category ?"
+ French -> "Voulez-vous vraiment supprimer cette catégorie ?"
+
+m l Category_Edit =
+ case l of
+ English -> "Edit an category"
+ French -> "Modifier une catégorie"
+
+m l Category_Empty =
+ case l of
+ English -> "No category."
+ French -> "Aucune catégorie."
+
+m l Category_Name =
+ case l of
+ English -> "Name"
+ French -> "Nom"
+
+m l Category_NotDeleted =
+ case l of
+ English -> "The category could not have been deleted."
+ French -> "La catégorie n’a pas pu être supprimé."
+
+m l Category_Title =
+ case l of
+ English -> "Categories"
+ French -> "Catégories"
+
+m l Category_Used =
+ case l of
+ English -> "This category is currently being used"
+ French -> "Cette catégorie est actuellement utilisée"
+
+m l (Date_Short day month year) =
+ case l of
+ English ->
+ T.intercalate "-" [ padded year 4, padded month 2, padded day 2 ]
+ French ->
+ T.intercalate "/" [ padded day 2, padded month 2, padded year 4 ]
+ where padded num pad =
+ let str = show num
+ in T.pack $ replicate (pad - length str) '0' ++ str
+
+m l (Date_ShortMonthAndYear month year) =
+ case l of
+ English ->
+ T.intercalate "-" . map (T.pack . show) $ [ year, month ]
+ French ->
+ T.intercalate "/" . map (T.pack . show) $ [ month, year ]
+
+m l (Date_Long day month year) =
+ case l of
+ English ->
+ T.concat [ month, " " , T.pack . show $ day, ", ", T.pack . show $ year ]
+ French ->
+ T.intercalate " " [ T.pack . show $ day, month, T.pack . show $ year ]
+
+m l Dialog_Confirm =
+ case l of
+ English -> "Confirm"
+ French -> "Confirmer"
+
+m l Dialog_Undo =
+ case l of
+ English -> "Undo"
+ French -> "Annuler"
+
+m l Error_CategoryCreate =
+ case l of
+ English -> "Error at category creation"
+ French -> "Erreur lors de la création de la catégorie"
+
+m l Error_CategoryDelete =
+ case l of
+ English -> "Error at category deletion"
+ French -> "Erreur lors de la suppression de la catégorie"
+
+m l Error_CategoryEdit =
+ case l of
+ English -> "Error at category edition"
+ French -> "Erreur lors de la modification de la catégorie"
+
+m l Error_IncomeCreate =
+ case l of
+ English -> "Error at income creation"
+ French -> "Erreur lors de la création du revenu"
+
+m l Error_IncomeDelete =
+ case l of
+ English -> "Error at income deletion"
+ French -> "Erreur lors de la suppression du revenu"
+
+m l Error_IncomeEdit =
+ case l of
+ English -> "Error at income edition"
+ French -> "Erreur lors de la modification du revenu"
+
+m l Error_PaymentCreate =
+ case l of
+ English -> "Error at payment creation"
+ French -> "Erreur lors de la création du paiement"
+
+m l Error_PaymentDelete =
+ case l of
+ English -> "Error at payment deletion"
+ French -> "Erreur lors de la suppression du paiement"
+
+m l Error_PaymentEdit =
+ case l of
+ English -> "Error at payment edition"
+ French -> "Erreur lors de la modification du paiement"
+
+m l Error_SignOut =
+ case l of
+ English -> "Error at sign out"
+ French -> "Erreur lors de la déconnexion"
+
+m l Form_AlreadyExists =
+ case l of
+ English -> "Dupplicate field"
+ French -> "Doublon"
+
+m l Form_NonEmpty =
+ case l of
+ English -> "Required field"
+ French -> "Champ requis"
+
+m l (Form_MinChars number) =
+ case l of
+ English -> T.concat [ "This field must contains at least ", T.pack . show $ number, " characters" ]
+ French -> T.concat [ "Ce champ doit contenir au moins ", T.pack . show $ number, " caractères" ]
+
+m l Form_NonNullNumber =
+ case l of
+ English -> "Number must not be null"
+ French -> "Le nombre ne doit pas être nul"
+
+m l (Form_GreaterIntThan number) =
+ case l of
+ English -> T.concat [ "Integer smaller than ", T.pack . show $ number, " or equal required" ]
+ French -> T.concat [ "Entier inférieur ou égal à ", T.pack . show $ number, " requis" ]
+
+m l Form_InvalidCategory =
+ case l of
+ English -> "Invalid category"
+ French -> "Catégorie invalide"
+
+m l Form_InvalidColor =
+ case l of
+ English -> "Invalid color"
+ French -> "Couleur invalide"
+
+m l Form_InvalidDate =
+ case l of
+ English -> "Date required"
+ French -> "Date requise"
+
+m l Form_InvalidInt =
+ case l of
+ English -> "Integer required"
+ French -> "Entier requis"
+
+m l Form_InvalidString =
+ case l of
+ English -> "String required"
+ French -> "Chaîne de caractères requise"
+
+m l (Form_SmallerIntThan number) =
+ case l of
+ English -> T.concat [ "Integer bigger than ", T.pack . show $ number, " or equal required" ]
+ French -> T.concat [ "Entier supérieur ou égal à ", T.pack . show $ number, " requis" ]
+
+m l HttpError_BadPayload =
+ case l of
+ English -> "Bad payload server error"
+ French -> "Contenu inattendu en provenance du serveur"
+
+m l HttpError_BadUrl =
+ case l of
+ English -> "URL not valid"
+ French -> "l’URL n’est pas valide"
+
+m l HttpError_NetworkError =
+ case l of
+ English -> "Network can not be reached"
+ French -> "Le serveur n’est pas accessible"
+
+m l HttpError_Timeout =
+ case l of
+ English -> "Timeout server error"
+ French -> "Le serveur met trop de temps à répondre"
+
+m l Income_AddLong =
+ case l of
+ English -> "Add an income"
+ French -> "Ajouter un revenu"
+
+m l Income_AddShort =
+ case l of
+ English -> "Add"
+ French -> "Ajouter"
+
+m l Income_Name =
+ case l of
+ English -> "Name"
+ French -> "Nom"
+
+m l Income_Amount =
+ case l of
+ English -> "Income"
+ French -> "Revenu"
+
+m l Income_Clone =
+ case l of
+ English -> "Clone an income"
+ French -> "Cloner un revenu"
+
+m l (Income_CumulativeSince since) =
+ case l of
+ English -> T.concat [ "Cumulative incomes since ", since ]
+ French -> T.concat [ "Revenus nets cumulés depuis le ", since ]
+
+m l Income_Date =
+ case l of
+ English -> "Date"
+ French -> "Date"
+
+m l Income_DeleteConfirm =
+ case l of
+ English -> "Are you sure to delete this income ?"
+ French -> "Voulez-vous vraiment supprimer ce revenu ?"
+
+m l Income_Edit =
+ case l of
+ English -> "Edit an income"
+ French -> "Modifier un revenu"
+
+m l Income_Empty =
+ case l of
+ English -> "No income."
+ French -> "Aucun revenu."
+
+m l Income_MonthlyNet =
+ case l of
+ English -> "Net monthly incomes"
+ French -> "Revenus mensuels nets"
+
+m l Income_Title =
+ case l of
+ English -> "Income"
+ French -> "Revenu"
+
+m l Month_January =
+ case l of
+ English -> "january"
+ French -> "janvier"
+
+m l Month_February =
+ case l of
+ English -> "february"
+ French -> "février"
+
+m l Month_March =
+ case l of
+ English -> "march"
+ French -> "mars"
+
+m l Month_April =
+ case l of
+ English -> "april"
+ French -> "avril"
+
+m l Month_May =
+ case l of
+ English -> "may"
+ French -> "mai"
+
+m l Month_June =
+ case l of
+ English -> "june"
+ French -> "juin"
+
+m l Month_July =
+ case l of
+ English -> "july"
+ French -> "juillet"
+
+m l Month_August =
+ case l of
+ English -> "august"
+ French -> "août"
+
+m l Month_September =
+ case l of
+ English -> "september"
+ French -> "septembre"
+
+m l Month_October =
+ case l of
+ English -> "october"
+ French -> "octobre"
+
+m l Month_November =
+ case l of
+ English -> "november"
+ French -> "novembre"
+
+m l Month_December =
+ case l of
+ English -> "december"
+ French -> "décembre"
+
+m l PageNotFound_Title =
+ case l of
+ English -> "Page not found"
+ French -> "Page introuvable"
+
+m l Payment_Add =
+ case l of
+ English -> "Add a payment"
+ French -> "Ajouter un paiement"
+
+m l Payment_Balanced =
+ case l of
+ English -> "Payments are balanced."
+ French -> "Les paiements sont équilibrés."
+
+m l (Payment_By key value) =
+ case l of
+ English -> T.concat [ key, ": ", value ]
+ French -> T.concat [ key, " : ", value ]
+
+m l Payment_Category =
+ case l of
+ English -> "Category"
+ French -> "Catégorie"
+
+m l Payment_CloneLong =
+ case l of
+ English -> "Clone a payment"
+ French -> "Cloner un paiement"
+
+m l Payment_CloneShort =
+ case l of
+ English -> "Clone"
+ French -> "Cloner"
+
+m l Payment_Cost =
+ case l of
+ English -> "Cost"
+ French -> "Coût"
+
+m l Payment_Date =
+ case l of
+ English -> "Date"
+ French -> "Date"
+
+m l Payment_Delete =
+ case l of
+ English -> "Delete"
+ French -> "Supprimer"
+
+m l Payment_DeleteConfirm =
+ case l of
+ English -> "Are you sure to delete this payment ?"
+ French -> "Voulez-vous vraiment supprimer ce paiement ?"
+
+m l Payment_EditLong =
+ case l of
+ English -> "Edit a payment"
+ French -> "Modifier un paiement"
+
+m l Payment_EditShort =
+ case l of
+ English -> "Edit"
+ French -> "Modifier"
+
+m l Payment_Empty =
+ case l of
+ English -> "No payment found from your search criteria."
+ French -> "Aucun paiement ne correspond à vos critères de recherches."
+
+m l Payment_Frequency =
+ case l of
+ English -> "Frequency"
+ French -> "Fréquence"
+
+m l Payment_InvalidFrequency =
+ case l of
+ English -> "Invalid frequency"
+ French -> "Fréquence invalide"
+
+m l Payment_Many =
+ case l of
+ English -> "payments"
+ French -> "paiements"
+
+m l Payment_MonthlyFemale =
+ case l of
+ English -> "Monthly"
+ French -> "Mensuelle"
+
+m l Payment_MonthlyMale =
+ case l of
+ English -> "Monthly"
+ French -> "Mensuel"
+
+m l Payment_Name =
+ case l of
+ English -> "Name"
+ French -> "Nom"
+
+m l Payment_NotDeleted =
+ case l of
+ English -> "The payment could not have been deleted."
+ French -> "Le paiement n’a pas pu être supprimé."
+
+m l Payment_One =
+ case l of
+ English -> "payment"
+ French -> "paiement"
+
+m l Payment_PunctualFemale =
+ case l of
+ English -> "Punctual"
+ French -> "Ponctuelle"
+
+m l Payment_PunctualMale =
+ case l of
+ English -> "Punctual"
+ French -> "Ponctuel"
+
+m l Payment_Title =
+ case l of
+ English -> "Payments"
+ French -> "Paiements"
+
+m l Payment_User =
+ case l of
+ English -> "Payer"
+ French -> "Payeur"
+
+m l (Payment_Worth subject amount) =
+ case l of
+ English -> T.concat [ subject, " worth ", amount ]
+ French -> T.concat [ subject, " comptabilisant ", amount ]
+
+m l Search_Monthly =
+ case l of
+ English -> "Monthly"
+ French -> "Mensuel"
+
+m l Search_Name =
+ case l of
+ English -> "Search"
+ French -> "Recherche"
+
+m l Search_Punctual =
+ case l of
+ English -> "Punctual"
+ French -> "Ponctuel"
+
+m l Secure_Unauthorized =
+ case l of
+ English -> "You are not authorized to sign in."
+ French -> "Tu n’es pas autorisé à te connecter."
+
+m l Secure_Forbidden =
+ case l of
+ English -> "You need to be logged in to perform this action"
+ French -> "Tu dois te connecter pour effectuer cette action"
+
+m l SignIn_Button =
+ case l of
+ English -> "Sign in"
+ French -> "Connexion"
+
+m l SignIn_DisconnectSuccess =
+ case l of
+ English -> "You have successfully disconnected"
+ French -> "Vous êtes à présent déconnecté."
+
+m l SignIn_InvalidCredentials =
+ case l of
+ English -> "Your credentials are not valid."
+ French -> "Vos identifiants de connexion ne sont pas valides."
+
+m l SignIn_EmailLabel =
+ case l of
+ English -> "Email"
+ French -> "Courriel"
+
+m l SignIn_PasswordLabel =
+ case l of
+ English -> "Password"
+ French -> "Mot de passe"
+
+m l (Statistics_ByMonthsAndMean paymentMean incomeMean ) =
+ case l of
+ English ->
+ T.concat [ "Payments by category (mean ", paymentMean, ") and income (mean ", incomeMean, ") by month" ]
+ French ->
+ T.concat [ "Paiements par catégorie (moy. ", paymentMean, ") et revenu (moy. ", incomeMean, ") par mois" ]
+
+m l Statistics_Title =
+ case l of
+ English -> "Statistics"
+ French -> "Statistiques"
+
+m l Statistics_TotalPayments =
+ case l of
+ English -> "Payment total"
+ French -> "Total des paiements"
+
+m l Statistics_TotalIncomes =
+ case l of
+ English -> "Income total"
+ French -> "Total des revenus"
+
+m l WeeklyReport_Empty =
+ case l of
+ English -> "No activity the previous week."
+ French -> "Pas d’activité la semaine passée."
+
+m l (WeeklyReport_IncomesCreated count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " incomes created:" ]
+ French -> T.concat [ T.pack . show $ count, " revenus créés :" ]
+
+m l (WeeklyReport_IncomesDeleted count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " incomes deleted:" ]
+ French -> T.concat [ T.pack . show $ count, " revenus supprimés :" ]
+
+m l (WeeklyReport_IncomesEdited count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " incomes edited:" ]
+ French -> T.concat [ T.pack . show $ count, " revenus modifiés :" ]
+
+m l (WeeklyReport_IncomeCreated count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " income created:" ]
+ French -> T.concat [ T.pack . show $ count, " revenu créé :" ]
+
+m l (WeeklyReport_IncomeDeleted count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " income deleted:" ]
+ French -> T.concat [ T.pack . show $ count, " revenu supprimé :" ]
+
+m l (WeeklyReport_IncomeEdited count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " income edited:" ]
+ French -> T.concat [ T.pack . show $ count, " revenu modifié :" ]
+
+m l (WeeklyReport_PayedFor name amount for at) =
+ case l of
+ English -> T.concat [ name, " payed ", amount, " for “", for, "” at ", at ]
+ French -> T.concat [ name, " a payé ", amount, " concernant « ", for, " » le ", at ]
+
+m l (WeeklyReport_PayedForNot name amount for at) =
+ case l of
+ English -> T.concat [ name, " didn’t pay ", amount, " for “", for, "” at ", at ]
+ French -> T.concat [ name, " n’a pas payé ", amount, " concernant « ", for, " » le ", at ]
+
+m l (WeeklyReport_PayedFrom name amount for) =
+ case l of
+ English -> T.concat [ name, " is payed ", amount, " of net monthly income from ", for ]
+ French -> T.concat [ name, " est payé ", amount, " net par mois à partir du ", for ]
+
+m l (WeeklyReport_PayedFromNot name amount for) =
+ case l of
+ English -> T.concat [ name, " isn’t payed ", amount, " of net monthly income from ", for ]
+ French -> T.concat [ name, " n’est pas payé ", amount, " net par mois à partir du ", for ]
+
+m l (WeeklyReport_PaymentsCreated count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payments created:" ]
+ French -> T.concat [ T.pack . show $ count, " paiements créés :" ]
+
+m l (WeeklyReport_PaymentsDeleted count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payments deleted:" ]
+ French -> T.concat [ T.pack . show $ count, " paiements supprimés :" ]
+
+m l (WeeklyReport_PaymentsEdited count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payments edited:" ]
+ French -> T.concat [ T.pack . show $ count, " paiements modifiés :" ]
+
+m l (WeeklyReport_PaymentCreated count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payment created:" ]
+ French -> T.concat [ T.pack . show $ count, " paiement créé :" ]
+
+m l (WeeklyReport_PaymentDeleted count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payment deleted:" ]
+ French -> T.concat [ T.pack . show $ count, " paiement supprimé :" ]
+
+m l (WeeklyReport_PaymentEdited count) =
+ case l of
+ English -> T.concat [ T.pack . show $ count, " payment edited:" ]
+ French -> T.concat [ T.pack . show $ count, " paiement modifié :" ]
+
+m l WeeklyReport_Title =
+ case l of
+ English -> "Weekly report"
+ French -> "Rapport hebdomadaire"
+
+m l NotFound_Message =
+ case l of
+ English -> "There is nothing here!"
+ French -> "Il n’y a rien à voir ici."
+
+m l NotFound_LinkMessage =
+ case l of
+ English -> "Go back to the home page."
+ French -> "Retour à l’accueil."
diff --git a/common/src/Common/Model.hs b/common/src/Common/Model.hs
new file mode 100644
index 0000000..979d876
--- /dev/null
+++ b/common/src/Common/Model.hs
@@ -0,0 +1,26 @@
+module Common.Model (module X) where
+
+import Common.Model.Category as X
+import Common.Model.CategoryPage as X
+import Common.Model.CreateCategoryForm as X
+import Common.Model.CreateIncomeForm as X
+import Common.Model.CreatePaymentForm as X
+import Common.Model.Currency as X
+import Common.Model.EditCategoryForm as X
+import Common.Model.EditIncome as X
+import Common.Model.EditIncomeForm as X
+import Common.Model.EditPaymentForm as X
+import Common.Model.Email as X
+import Common.Model.ExceedingPayer as X
+import Common.Model.Frequency as X
+import Common.Model.Income as X
+import Common.Model.IncomeHeader as X
+import Common.Model.IncomePage as X
+import Common.Model.Init as X
+import Common.Model.Password as X
+import Common.Model.Payment as X
+import Common.Model.PaymentHeader as X
+import Common.Model.PaymentPage as X
+import Common.Model.SignInForm as X
+import Common.Model.Stats as X
+import Common.Model.User as X
diff --git a/common/src/Common/Model/Category.hs b/common/src/Common/Model/Category.hs
new file mode 100644
index 0000000..cc3f795
--- /dev/null
+++ b/common/src/Common/Model/Category.hs
@@ -0,0 +1,24 @@
+module Common.Model.Category
+ ( CategoryId
+ , Category(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Int (Int64)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+
+type CategoryId = Int64
+
+data Category = Category
+ { _category_id :: CategoryId
+ , _category_name :: Text
+ , _category_color :: Text
+ , _category_createdAt :: UTCTime
+ , _category_editedAt :: Maybe UTCTime
+ , _category_deletedAt :: Maybe UTCTime
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON Category
+instance ToJSON Category
diff --git a/common/src/Common/Model/CategoryPage.hs b/common/src/Common/Model/CategoryPage.hs
new file mode 100644
index 0000000..e20f49f
--- /dev/null
+++ b/common/src/Common/Model/CategoryPage.hs
@@ -0,0 +1,18 @@
+module Common.Model.CategoryPage
+ ( CategoryPage(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (Category, CategoryId)
+
+data CategoryPage = CategoryPage
+ { _categoryPage_page :: Int
+ , _categoryPage_categories :: [Category]
+ , _categoryPage_usedCategories :: [CategoryId]
+ , _categoryPage_totalCount :: Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON CategoryPage
+instance ToJSON CategoryPage
diff --git a/common/src/Common/Model/CreateCategoryForm.hs b/common/src/Common/Model/CreateCategoryForm.hs
new file mode 100644
index 0000000..4668ef4
--- /dev/null
+++ b/common/src/Common/Model/CreateCategoryForm.hs
@@ -0,0 +1,15 @@
+module Common.Model.CreateCategoryForm
+ ( CreateCategoryForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+data CreateCategoryForm = CreateCategoryForm
+ { _createCategoryForm_name :: Text
+ , _createCategoryForm_color :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON CreateCategoryForm
+instance ToJSON CreateCategoryForm
diff --git a/common/src/Common/Model/CreateIncomeForm.hs b/common/src/Common/Model/CreateIncomeForm.hs
new file mode 100644
index 0000000..e83bf0a
--- /dev/null
+++ b/common/src/Common/Model/CreateIncomeForm.hs
@@ -0,0 +1,15 @@
+module Common.Model.CreateIncomeForm
+ ( CreateIncomeForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+data CreateIncomeForm = CreateIncomeForm
+ { _createIncomeForm_amount :: Text
+ , _createIncomeForm_date :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON CreateIncomeForm
+instance ToJSON CreateIncomeForm
diff --git a/common/src/Common/Model/CreatePaymentForm.hs b/common/src/Common/Model/CreatePaymentForm.hs
new file mode 100644
index 0000000..60c5423
--- /dev/null
+++ b/common/src/Common/Model/CreatePaymentForm.hs
@@ -0,0 +1,21 @@
+module Common.Model.CreatePaymentForm
+ ( CreatePaymentForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (CategoryId)
+import Common.Model.Frequency (Frequency)
+
+data CreatePaymentForm = CreatePaymentForm
+ { _createPaymentForm_name :: Text
+ , _createPaymentForm_cost :: Text
+ , _createPaymentForm_date :: Text
+ , _createPaymentForm_category :: CategoryId
+ , _createPaymentForm_frequency :: Frequency
+ } deriving (Show, Generic)
+
+instance FromJSON CreatePaymentForm
+instance ToJSON CreatePaymentForm
diff --git a/common/src/Common/Model/Currency.hs b/common/src/Common/Model/Currency.hs
new file mode 100644
index 0000000..175aeba
--- /dev/null
+++ b/common/src/Common/Model/Currency.hs
@@ -0,0 +1,12 @@
+module Common.Model.Currency
+ ( Currency(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+newtype Currency = Currency Text deriving (Show, Generic)
+
+instance FromJSON Currency
+instance ToJSON Currency
diff --git a/common/src/Common/Model/EditCategoryForm.hs b/common/src/Common/Model/EditCategoryForm.hs
new file mode 100644
index 0000000..a2ceca0
--- /dev/null
+++ b/common/src/Common/Model/EditCategoryForm.hs
@@ -0,0 +1,18 @@
+module Common.Model.EditCategoryForm
+ ( EditCategoryForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (CategoryId)
+
+data EditCategoryForm = EditCategoryForm
+ { _editCategoryForm_id :: CategoryId
+ , _editCategoryForm_name :: Text
+ , _editCategoryForm_color :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON EditCategoryForm
+instance ToJSON EditCategoryForm
diff --git a/common/src/Common/Model/EditIncome.hs b/common/src/Common/Model/EditIncome.hs
new file mode 100644
index 0000000..0e65f12
--- /dev/null
+++ b/common/src/Common/Model/EditIncome.hs
@@ -0,0 +1,17 @@
+module Common.Model.EditIncome
+ ( EditIncome(..)
+ ) where
+
+import Data.Aeson (FromJSON)
+import Data.Time.Calendar (Day)
+import GHC.Generics (Generic)
+
+import Common.Model.Income (IncomeId)
+
+data EditIncome = EditIncome
+ { _editIncome_id :: IncomeId
+ , _editIncome_date :: Day
+ , _editIncome_amount :: Int
+ } deriving (Show, Generic)
+
+instance FromJSON EditIncome
diff --git a/common/src/Common/Model/EditIncomeForm.hs b/common/src/Common/Model/EditIncomeForm.hs
new file mode 100644
index 0000000..ff975fc
--- /dev/null
+++ b/common/src/Common/Model/EditIncomeForm.hs
@@ -0,0 +1,18 @@
+module Common.Model.EditIncomeForm
+ ( EditIncomeForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+import Common.Model.Income (IncomeId)
+
+data EditIncomeForm = EditIncomeForm
+ { _editIncomeForm_id :: IncomeId
+ , _editIncomeForm_amount :: Text
+ , _editIncomeForm_date :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON EditIncomeForm
+instance ToJSON EditIncomeForm
diff --git a/common/src/Common/Model/EditPaymentForm.hs b/common/src/Common/Model/EditPaymentForm.hs
new file mode 100644
index 0000000..168c9ff
--- /dev/null
+++ b/common/src/Common/Model/EditPaymentForm.hs
@@ -0,0 +1,23 @@
+module Common.Model.EditPaymentForm
+ ( EditPaymentForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (CategoryId)
+import Common.Model.Frequency (Frequency)
+import Common.Model.Payment (PaymentId)
+
+data EditPaymentForm = EditPaymentForm
+ { _editPaymentForm_id :: PaymentId
+ , _editPaymentForm_name :: Text
+ , _editPaymentForm_cost :: Text
+ , _editPaymentForm_date :: Text
+ , _editPaymentForm_category :: CategoryId
+ , _editPaymentForm_frequency :: Frequency
+ } deriving (Show, Generic)
+
+instance FromJSON EditPaymentForm
+instance ToJSON EditPaymentForm
diff --git a/common/src/Common/Model/Email.hs b/common/src/Common/Model/Email.hs
new file mode 100644
index 0000000..e938f83
--- /dev/null
+++ b/common/src/Common/Model/Email.hs
@@ -0,0 +1,12 @@
+module Common.Model.Email
+ ( Email(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+newtype Email = Email Text deriving (Show, Generic)
+
+instance FromJSON Email
+instance ToJSON Email
diff --git a/common/src/Common/Model/ExceedingPayer.hs b/common/src/Common/Model/ExceedingPayer.hs
new file mode 100644
index 0000000..b7d3efb
--- /dev/null
+++ b/common/src/Common/Model/ExceedingPayer.hs
@@ -0,0 +1,16 @@
+module Common.Model.ExceedingPayer
+ ( ExceedingPayer(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+import Common.Model.User (UserId)
+
+data ExceedingPayer = ExceedingPayer
+ { _exceedingPayer_userId :: UserId
+ , _exceedingPayer_amount :: Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON ExceedingPayer
+instance ToJSON ExceedingPayer
diff --git a/common/src/Common/Model/Frequency.hs b/common/src/Common/Model/Frequency.hs
new file mode 100644
index 0000000..48e75ea
--- /dev/null
+++ b/common/src/Common/Model/Frequency.hs
@@ -0,0 +1,14 @@
+module Common.Model.Frequency
+ ( Frequency(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+data Frequency =
+ Punctual
+ | Monthly
+ deriving (Eq, Read, Show, Generic, Ord)
+
+instance FromJSON Frequency
+instance ToJSON Frequency
diff --git a/common/src/Common/Model/Income.hs b/common/src/Common/Model/Income.hs
new file mode 100644
index 0000000..57d07f1
--- /dev/null
+++ b/common/src/Common/Model/Income.hs
@@ -0,0 +1,27 @@
+module Common.Model.Income
+ ( IncomeId
+ , Income(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Int (Int64)
+import Data.Time (UTCTime)
+import Data.Time.Calendar (Day)
+import GHC.Generics (Generic)
+
+import Common.Model.User (UserId)
+
+type IncomeId = Int64
+
+data Income = Income
+ { _income_id :: IncomeId
+ , _income_userId :: UserId
+ , _income_date :: Day
+ , _income_amount :: Int
+ , _income_createdAt :: UTCTime
+ , _income_editedAt :: Maybe UTCTime
+ , _income_deletedAt :: Maybe UTCTime
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON Income
+instance ToJSON Income
diff --git a/common/src/Common/Model/IncomeHeader.hs b/common/src/Common/Model/IncomeHeader.hs
new file mode 100644
index 0000000..7e712e8
--- /dev/null
+++ b/common/src/Common/Model/IncomeHeader.hs
@@ -0,0 +1,18 @@
+module Common.Model.IncomeHeader
+ ( IncomeHeader(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Map (Map)
+import Data.Time.Calendar (Day)
+import GHC.Generics (Generic)
+
+import Common.Model.User (UserId)
+
+data IncomeHeader = IncomeHeader
+ { _incomeHeader_since :: Maybe Day
+ , _incomeHeader_byUser :: Map UserId Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON IncomeHeader
+instance ToJSON IncomeHeader
diff --git a/common/src/Common/Model/IncomePage.hs b/common/src/Common/Model/IncomePage.hs
new file mode 100644
index 0000000..977b0ea
--- /dev/null
+++ b/common/src/Common/Model/IncomePage.hs
@@ -0,0 +1,19 @@
+module Common.Model.IncomePage
+ ( IncomePage(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+import Common.Model.Income (Income)
+import Common.Model.IncomeHeader (IncomeHeader)
+
+data IncomePage = IncomePage
+ { _incomePage_page :: Int
+ , _incomePage_header :: IncomeHeader
+ , _incomePage_incomes :: [Income]
+ , _incomePage_totalCount :: Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON IncomePage
+instance ToJSON IncomePage
diff --git a/common/src/Common/Model/Init.hs b/common/src/Common/Model/Init.hs
new file mode 100644
index 0000000..5ef1535
--- /dev/null
+++ b/common/src/Common/Model/Init.hs
@@ -0,0 +1,18 @@
+module Common.Model.Init
+ ( Init(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+import Common.Model.Currency (Currency)
+import Common.Model.User (User, UserId)
+
+data Init = Init
+ { _init_users :: [User]
+ , _init_currentUser :: UserId
+ , _init_currency :: Currency
+ } deriving (Show, Generic)
+
+instance FromJSON Init
+instance ToJSON Init
diff --git a/common/src/Common/Model/Password.hs b/common/src/Common/Model/Password.hs
new file mode 100644
index 0000000..1b51a47
--- /dev/null
+++ b/common/src/Common/Model/Password.hs
@@ -0,0 +1,12 @@
+module Common.Model.Password
+ ( Password(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+newtype Password = Password Text deriving (Show, Generic)
+
+instance FromJSON Password
+instance ToJSON Password
diff --git a/common/src/Common/Model/Payment.hs b/common/src/Common/Model/Payment.hs
new file mode 100644
index 0000000..733a145
--- /dev/null
+++ b/common/src/Common/Model/Payment.hs
@@ -0,0 +1,33 @@
+module Common.Model.Payment
+ ( PaymentId
+ , Payment(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Int (Int64)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import Data.Time.Calendar (Day)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (CategoryId)
+import Common.Model.Frequency
+import Common.Model.User (UserId)
+
+type PaymentId = Int64
+
+data Payment = Payment
+ { _payment_id :: PaymentId
+ , _payment_user :: UserId
+ , _payment_name :: Text
+ , _payment_cost :: Int
+ , _payment_date :: Day
+ , _payment_category :: CategoryId
+ , _payment_frequency :: Frequency
+ , _payment_createdAt :: UTCTime
+ , _payment_editedAt :: Maybe UTCTime
+ , _payment_deletedAt :: Maybe UTCTime
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON Payment
+instance ToJSON Payment
diff --git a/common/src/Common/Model/PaymentHeader.hs b/common/src/Common/Model/PaymentHeader.hs
new file mode 100644
index 0000000..35f5e1a
--- /dev/null
+++ b/common/src/Common/Model/PaymentHeader.hs
@@ -0,0 +1,18 @@
+module Common.Model.PaymentHeader
+ ( PaymentHeader(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Map (Map)
+import GHC.Generics (Generic)
+
+import Common.Model.ExceedingPayer (ExceedingPayer)
+import Common.Model.User (UserId)
+
+data PaymentHeader = PaymentHeader
+ { _paymentHeader_exceedingPayers :: [ExceedingPayer]
+ , _paymentHeader_repartition :: Map UserId Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON PaymentHeader
+instance ToJSON PaymentHeader
diff --git a/common/src/Common/Model/PaymentPage.hs b/common/src/Common/Model/PaymentPage.hs
new file mode 100644
index 0000000..88d9715
--- /dev/null
+++ b/common/src/Common/Model/PaymentPage.hs
@@ -0,0 +1,21 @@
+module Common.Model.PaymentPage
+ ( PaymentPage(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics (Generic)
+
+import Common.Model.Frequency (Frequency)
+import Common.Model.Payment (Payment)
+import Common.Model.PaymentHeader (PaymentHeader)
+
+data PaymentPage = PaymentPage
+ { _paymentPage_page :: Int
+ , _paymentPage_frequency :: Frequency
+ , _paymentPage_header :: PaymentHeader
+ , _paymentPage_payments :: [Payment]
+ , _paymentPage_totalCount :: Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON PaymentPage
+instance ToJSON PaymentPage
diff --git a/common/src/Common/Model/SignInForm.hs b/common/src/Common/Model/SignInForm.hs
new file mode 100644
index 0000000..7a25935
--- /dev/null
+++ b/common/src/Common/Model/SignInForm.hs
@@ -0,0 +1,15 @@
+module Common.Model.SignInForm
+ ( SignInForm(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+data SignInForm = SignInForm
+ { _signInForm_email :: Text
+ , _signInForm_password :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON SignInForm
+instance ToJSON SignInForm
diff --git a/common/src/Common/Model/Stats.hs b/common/src/Common/Model/Stats.hs
new file mode 100644
index 0000000..86e6ab9
--- /dev/null
+++ b/common/src/Common/Model/Stats.hs
@@ -0,0 +1,23 @@
+module Common.Model.Stats
+ ( Stats
+ , MonthStats(..)
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Map (Map)
+import Data.Time.Calendar (Day)
+import GHC.Generics (Generic)
+
+import Common.Model.Category (CategoryId)
+import Common.Model.User (UserId)
+
+type Stats = [MonthStats]
+
+data MonthStats = MonthStats
+ { _monthStats_start :: Day
+ , _monthStats_paymentsByCategory :: Map CategoryId Int
+ , _monthStats_incomeByUser :: Map UserId Int
+ } deriving (Eq, Show, Generic)
+
+instance FromJSON MonthStats
+instance ToJSON MonthStats
diff --git a/common/src/Common/Model/User.hs b/common/src/Common/Model/User.hs
new file mode 100644
index 0000000..a30d104
--- /dev/null
+++ b/common/src/Common/Model/User.hs
@@ -0,0 +1,27 @@
+module Common.Model.User
+ ( UserId
+ , User(..)
+ , findUser
+ ) where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Int (Int64)
+import qualified Data.List as L
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+
+type UserId = Int64
+
+data User = User
+ { _user_id :: UserId
+ , _user_creation :: UTCTime
+ , _user_email :: Text
+ , _user_name :: Text
+ } deriving (Show, Generic)
+
+instance FromJSON User
+instance ToJSON User
+
+findUser :: UserId -> [User] -> Maybe User
+findUser userId users = L.find ((== userId) . _user_id) users
diff --git a/common/src/Common/Msg.hs b/common/src/Common/Msg.hs
new file mode 100644
index 0000000..9e4cfe2
--- /dev/null
+++ b/common/src/Common/Msg.hs
@@ -0,0 +1,13 @@
+module Common.Msg
+ ( get
+ , Key(..)
+ ) where
+
+import Data.Text (Text)
+
+import Common.Message.Key (Key (..))
+import Common.Message.Lang (Lang (..))
+import qualified Common.Message.Translation as Translation
+
+get :: Key -> Text
+get = Translation.get French
diff --git a/common/src/Common/Util/Text.hs b/common/src/Common/Util/Text.hs
new file mode 100644
index 0000000..0f9c187
--- /dev/null
+++ b/common/src/Common/Util/Text.hs
@@ -0,0 +1,49 @@
+module Common.Util.Text
+ ( search
+ , formatSearch
+ , unaccent
+ ) where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+
+search :: Text -> Text -> Bool
+search s t = (formatSearch s) `T.isInfixOf` (formatSearch t)
+
+formatSearch :: Text -> Text
+formatSearch = T.toLower . unaccent
+
+unaccent :: Text -> Text
+unaccent = T.map unaccentChar
+
+unaccentChar :: Char -> Char
+unaccentChar c = case c of
+ 'à' -> 'a'
+ 'á' -> 'a'
+ 'â' -> 'a'
+ 'ã' -> 'a'
+ 'ä' -> 'a'
+ 'ç' -> 'c'
+ 'è' -> 'e'
+ 'é' -> 'e'
+ 'ê' -> 'e'
+ 'ë' -> 'e'
+ 'ì' -> 'i'
+ 'í' -> 'i'
+ 'î' -> 'i'
+ 'ï' -> 'i'
+ 'ñ' -> 'n'
+ 'ò' -> 'o'
+ 'ó' -> 'o'
+ 'ô' -> 'o'
+ 'õ' -> 'o'
+ 'ö' -> 'o'
+ 'š' -> 's'
+ 'ù' -> 'u'
+ 'ú' -> 'u'
+ 'û' -> 'u'
+ 'ü' -> 'u'
+ 'ý' -> 'y'
+ 'ÿ' -> 'y'
+ 'ž' -> 'z'
+ _ -> c
diff --git a/common/src/Common/Util/Time.hs b/common/src/Common/Util/Time.hs
new file mode 100644
index 0000000..6240720
--- /dev/null
+++ b/common/src/Common/Util/Time.hs
@@ -0,0 +1,26 @@
+module Common.Util.Time
+ ( timeToDay
+ , parseDay
+ ) where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time (UTCTime)
+import qualified Data.Time as Time
+import Data.Time.Calendar (Day)
+import Data.Time.LocalTime
+import qualified Text.Read as T
+
+timeToDay :: UTCTime -> IO Day
+timeToDay time = localDay . (flip utcToLocalTime time) <$> getTimeZone time
+
+parseDay :: Text -> Maybe Day
+parseDay str = do
+ (y, m, d) <-
+ case T.splitOn "-" str of
+ [y, m, d] -> Just (y, m, d)
+ _ -> Nothing
+ d' <- T.readMaybe . T.unpack $ d
+ m' <- T.readMaybe . T.unpack $ m
+ y' <- T.readMaybe . T.unpack $ y
+ return $ Time.fromGregorian y' m' d'
diff --git a/common/src/Common/Util/Validation.hs b/common/src/Common/Util/Validation.hs
new file mode 100644
index 0000000..f195d95
--- /dev/null
+++ b/common/src/Common/Util/Validation.hs
@@ -0,0 +1,13 @@
+module Common.Util.Validation
+ ( isSuccess
+ , isFailure
+ ) where
+
+import Data.Validation (Validation (Failure, Success))
+
+isSuccess :: forall a b. Validation a b -> Bool
+isSuccess (Failure _) = False
+isSuccess (Success _) = True
+
+isFailure :: forall a b. Validation a b -> Bool
+isFailure = not . isSuccess
diff --git a/common/src/Common/Validation/Atomic.hs b/common/src/Common/Validation/Atomic.hs
new file mode 100644
index 0000000..9c21e14
--- /dev/null
+++ b/common/src/Common/Validation/Atomic.hs
@@ -0,0 +1,61 @@
+module Common.Validation.Atomic
+ ( color
+ , day
+ , minLength
+ , nonEmpty
+ , nonNullNumber
+ , number
+ , password
+ ) where
+
+import qualified Data.Char as Char
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar (Day)
+import Data.Validation (Validation)
+import qualified Data.Validation as V
+import qualified Text.Read as T
+
+import qualified Common.Msg as Msg
+import qualified Common.Util.Time as Time
+
+minLength :: Int -> Text -> Validation Text Text
+minLength l =
+ V.validate
+ (Msg.get (Msg.Form_MinChars l))
+ (\t -> if T.length t >= l then Just t else Nothing)
+
+nonEmpty :: Text -> Validation Text Text
+nonEmpty =
+ V.validate
+ (Msg.get Msg.Form_NonEmpty)
+ (\t -> if (not . T.null $ t) then Just t else Nothing)
+
+number :: Text -> Validation Text Int
+number input =
+ case (T.readMaybe . T.unpack $ input) of
+ Just n -> V.Success n
+ _ -> V.Failure (Msg.get Msg.Form_InvalidInt)
+
+nonNullNumber :: Int -> Validation Text Int
+nonNullNumber =
+ V.validate
+ (Msg.get Msg.Form_NonNullNumber)
+ (\n -> if n /= 0 then Just n else Nothing)
+
+day :: Text -> Validation Text Day
+day str =
+ case Time.parseDay str of
+ Just d -> V.Success d
+ Nothing -> V.Failure $ Msg.get Msg.Form_InvalidDate
+
+color :: Text -> Validation Text Text
+color str =
+ if T.take 1 str == "#" && T.all Char.isHexDigit (T.drop 1 str) then
+ V.Success str
+
+ else
+ V.Failure (Msg.get Msg.Form_InvalidColor)
+
+password :: Text -> Validation Text Text
+password = minLength 8
diff --git a/common/src/Common/Validation/Category.hs b/common/src/Common/Validation/Category.hs
new file mode 100644
index 0000000..f9e6ab4
--- /dev/null
+++ b/common/src/Common/Validation/Category.hs
@@ -0,0 +1,15 @@
+module Common.Validation.Category
+ ( name
+ , color
+ ) where
+
+import Data.Text (Text)
+import Data.Validation (Validation)
+
+import qualified Common.Validation.Atomic as Atomic
+
+name :: Text -> Validation Text Text
+name = Atomic.nonEmpty
+
+color :: Text -> Validation Text Text
+color = Atomic.color
diff --git a/common/src/Common/Validation/Income.hs b/common/src/Common/Validation/Income.hs
new file mode 100644
index 0000000..7a58bab
--- /dev/null
+++ b/common/src/Common/Validation/Income.hs
@@ -0,0 +1,17 @@
+module Common.Validation.Income
+ ( amount
+ , date
+ ) where
+
+import Data.Text (Text)
+import Data.Time.Calendar (Day)
+import Data.Validation (Validation)
+import qualified Data.Validation as V
+
+import qualified Common.Validation.Atomic as Atomic
+
+amount :: Text -> Validation Text Int
+amount input = V.bindValidation (Atomic.number input) Atomic.nonNullNumber
+
+date :: Text -> Validation Text Day
+date = Atomic.day
diff --git a/common/src/Common/Validation/Payment.hs b/common/src/Common/Validation/Payment.hs
new file mode 100644
index 0000000..e3c447a
--- /dev/null
+++ b/common/src/Common/Validation/Payment.hs
@@ -0,0 +1,31 @@
+module Common.Validation.Payment
+ ( name
+ , cost
+ , date
+ , category
+ ) where
+
+import Data.Text (Text)
+import Data.Time.Calendar (Day)
+import Data.Validation (Validation)
+import qualified Data.Validation as V
+
+import Common.Model (CategoryId)
+import qualified Common.Msg as Msg
+import qualified Common.Validation.Atomic as Atomic
+
+name :: Text -> Validation Text Text
+name = Atomic.nonEmpty
+
+cost :: Text -> Validation Text Int
+cost input = V.bindValidation (Atomic.number input) Atomic.nonNullNumber
+
+date :: Text -> Validation Text Day
+date = Atomic.day
+
+category :: [CategoryId] -> CategoryId -> Validation Text CategoryId
+category cs c =
+ if elem c cs then
+ V.Success c
+ else
+ V.Failure $ Msg.get Msg.Form_InvalidCategory
diff --git a/common/src/Common/Validation/SignIn.hs b/common/src/Common/Validation/SignIn.hs
new file mode 100644
index 0000000..ac9cc37
--- /dev/null
+++ b/common/src/Common/Validation/SignIn.hs
@@ -0,0 +1,17 @@
+module Common.Validation.SignIn
+ ( email
+ , password
+ ) where
+
+import Data.Text (Text)
+import Data.Validation (Validation)
+
+import Common.Model.Email (Email (..))
+import Common.Model.Password (Password (..))
+import qualified Common.Validation.Atomic as Atomic
+
+email :: Text -> Validation Text Email
+email = fmap Email . Atomic.minLength 5
+
+password :: Text -> Validation Text Password
+password = fmap Password . Atomic.minLength 8
diff --git a/common/src/Common/View/Format.hs b/common/src/Common/View/Format.hs
new file mode 100644
index 0000000..5d879fa
--- /dev/null
+++ b/common/src/Common/View/Format.hs
@@ -0,0 +1,78 @@
+module Common.View.Format
+ ( shortDay
+ , longDay
+ , price
+ , number
+ , monthAndYear
+ ) where
+
+import qualified Data.List as L
+import qualified Data.Maybe as Maybe
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time.Calendar (Day)
+import qualified Data.Time.Calendar as Calendar
+
+import Common.Model (Currency (..))
+import Common.Msg (Key)
+import qualified Common.Msg as Msg
+
+shortDay :: Day -> Text
+shortDay date =
+ Msg.get $ Msg.Date_Short
+ day
+ month
+ (fromIntegral year)
+ where (year, month, day) = Calendar.toGregorian date
+
+longDay :: Day -> Text
+longDay date =
+ Msg.get $ Msg.Date_Long
+ day
+ (Maybe.fromMaybe "−" . fmap Msg.get . monthToKey $ month)
+ (fromIntegral year)
+ where (year, month, day) = Calendar.toGregorian date
+
+monthAndYear :: Day -> Text
+monthAndYear date =
+ T.intercalate " "
+ [ Maybe.fromMaybe "" . fmap ((\t -> T.concat [t, " "]) . Msg.get) . monthToKey $ month
+ , T.pack . show $ year
+ ]
+ where (year, month, _) = Calendar.toGregorian date
+
+monthToKey :: Int -> Maybe Key
+monthToKey 1 = Just Msg.Month_January
+monthToKey 2 = Just Msg.Month_February
+monthToKey 3 = Just Msg.Month_March
+monthToKey 4 = Just Msg.Month_April
+monthToKey 5 = Just Msg.Month_May
+monthToKey 6 = Just Msg.Month_June
+monthToKey 7 = Just Msg.Month_July
+monthToKey 8 = Just Msg.Month_August
+monthToKey 9 = Just Msg.Month_September
+monthToKey 10 = Just Msg.Month_October
+monthToKey 11 = Just Msg.Month_November
+monthToKey 12 = Just Msg.Month_December
+monthToKey _ = Nothing
+
+price :: Currency -> Int -> Text
+price (Currency currency) amount = T.concat [ number amount, " ", currency ]
+
+number :: Int -> Text
+number n =
+ T.pack
+ . (++) (if n < 0 then "-" else "")
+ . reverse
+ . concat
+ . L.intersperse " "
+ . group 3
+ . reverse
+ . show
+ . abs $ n
+
+group :: Int -> [a] -> [[a]]
+group n xs =
+ if length xs <= n
+ then [xs]
+ else (take n xs) : (group n (drop n xs))