feat: internationalization #43
							
								
								
									
										1
									
								
								.env.dev
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								.env.dev
									
									
									
									
									
								
							@@ -1 +1,2 @@
 | 
			
		||||
DATABASE_URL=postgres://app:app@db/todo_baggins
 | 
			
		||||
LANGUAGE_CODE=en-US
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										87
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1021,6 +1021,22 @@ dependencies = [
 | 
			
		||||
 "tracing",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dioxus-sdk"
 | 
			
		||||
version = "0.5.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "271adf41837fbbceb955fefd71816d4a3fbbab2829f8c0ea0364584b531ce999"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "dioxus",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "unic-langid",
 | 
			
		||||
 "uuid",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dioxus-signals"
 | 
			
		||||
version = "0.5.7"
 | 
			
		||||
@@ -1096,6 +1112,17 @@ dependencies = [
 | 
			
		||||
 "syn 2.0.74",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "displaydoc"
 | 
			
		||||
version = "0.2.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.74",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dotenvy"
 | 
			
		||||
version = "0.15.7"
 | 
			
		||||
@@ -1384,8 +1411,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "wasi",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -2772,6 +2801,12 @@ dependencies = [
 | 
			
		||||
 "lock_api",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "stfu8"
 | 
			
		||||
version = "0.2.7"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "strsim"
 | 
			
		||||
version = "0.11.1"
 | 
			
		||||
@@ -2872,6 +2907,15 @@ dependencies = [
 | 
			
		||||
 "time-core",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tinystr"
 | 
			
		||||
version = "0.7.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "displaydoc",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tinyvec"
 | 
			
		||||
version = "1.8.0"
 | 
			
		||||
@@ -2903,6 +2947,7 @@ dependencies = [
 | 
			
		||||
 "dioxus",
 | 
			
		||||
 "dioxus-logger",
 | 
			
		||||
 "dioxus-query",
 | 
			
		||||
 "dioxus-sdk",
 | 
			
		||||
 "dotenvy",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
@@ -2910,7 +2955,9 @@ dependencies = [
 | 
			
		||||
 "time",
 | 
			
		||||
 "tracing",
 | 
			
		||||
 "tracing-wasm",
 | 
			
		||||
 "unic-langid-impl",
 | 
			
		||||
 "validator",
 | 
			
		||||
 "voca_rs",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -3138,6 +3185,25 @@ version = "1.17.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unic-langid"
 | 
			
		||||
version = "0.9.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "unic-langid-impl",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unic-langid-impl"
 | 
			
		||||
version = "0.9.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
 "tinystr",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "unicase"
 | 
			
		||||
version = "2.7.0"
 | 
			
		||||
@@ -3203,6 +3269,16 @@ version = "0.7.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "uuid"
 | 
			
		||||
version = "1.10.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "getrandom",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "validator"
 | 
			
		||||
version = "0.18.1"
 | 
			
		||||
@@ -3257,6 +3333,17 @@ version = "0.9.5"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "voca_rs"
 | 
			
		||||
version = "1.15.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3e44efbf25e32768d5ecd22244feacc3d3b3eca72d318f5ef0a4764c2c158e18"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "regex",
 | 
			
		||||
 "stfu8",
 | 
			
		||||
 "unicode-segmentation",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "waker-fn"
 | 
			
		||||
version = "1.2.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ serde_with = { version = "3.9.0", features = ["chrono_0_4"] }
 | 
			
		||||
async-std = "1.12.0"
 | 
			
		||||
dioxus-query = "0.5.1"
 | 
			
		||||
time = "0.3.36"
 | 
			
		||||
dioxus-sdk = { version = "0.5.0", features = ["i18n"] }
 | 
			
		||||
unic-langid-impl = "0.9.5"
 | 
			
		||||
voca_rs = "1.15.2"
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = []
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,17 @@ use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_query::prelude::{use_init_query_client};
 | 
			
		||||
use crate::query::{QueryErrors, QueryKey, QueryValue};
 | 
			
		||||
use dioxus_sdk::i18n::{use_init_i18n};
 | 
			
		||||
use crate::internationalization::get_languages;
 | 
			
		||||
use crate::server::internationalization::get_language_identifier;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn App() -> Element {
 | 
			
		||||
    use_init_query_client::<QueryValue, QueryErrors, QueryKey>();
 | 
			
		||||
    
 | 
			
		||||
    let language_identifier = use_server_future(get_language_identifier)?.unwrap().unwrap();
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 | 
			||||
    use_init_i18n(language_identifier.clone(), language_identifier, get_languages);
 | 
			
		||||
    
 | 
			
		||||
    rsx! {
 | 
			
		||||
        div {
 | 
			
		||||
            class: "min-h-screen text-zinc-200 bg-zinc-800 pt-4 pb-36",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,16 @@
 | 
			
		||||
use crate::components::task_list::TaskList;
 | 
			
		||||
use crate::internationalization::LocaleFromLanguageIdentifier;
 | 
			
		||||
use crate::models::category::Category;
 | 
			
		||||
use chrono::{Datelike, Local, Locale};
 | 
			
		||||
use crate::models::task::TaskWithSubtasks;
 | 
			
		||||
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
 | 
			
		||||
use crate::query::QueryValue;
 | 
			
		||||
use chrono::{Datelike, Local};
 | 
			
		||||
use dioxus::core_macro::rsx;
 | 
			
		||||
use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_query::prelude::QueryResult;
 | 
			
		||||
use crate::components::task_list::TaskList;
 | 
			
		||||
use crate::query::QueryValue;
 | 
			
		||||
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
 | 
			
		||||
use crate::models::task::{TaskWithSubtasks};
 | 
			
		||||
use dioxus_sdk::i18n::use_i18;
 | 
			
		||||
use dioxus_sdk::translate;
 | 
			
		||||
 | 
			
		||||
const CALENDAR_LENGTH_DAYS: usize = 366 * 3;
 | 
			
		||||
 | 
			
		||||
@@ -20,6 +23,8 @@ pub(crate) fn CategoryCalendarPage() -> Element {
 | 
			
		||||
    });
 | 
			
		||||
    let tasks_query_result = tasks.result();
 | 
			
		||||
 | 
			
		||||
    let i18 = use_i18();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        match tasks_query_result.value() {
 | 
			
		||||
            QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
 | 
			
		||||
@@ -37,14 +42,17 @@ pub(crate) fn CategoryCalendarPage() -> Element {
 | 
			
		||||
                                    div {
 | 
			
		||||
                                        class: "pt-1",
 | 
			
		||||
                                        {
 | 
			
		||||
                                            date_current
 | 
			
		||||
                                            .format_localized(
 | 
			
		||||
                                                format!(
 | 
			
		||||
                                                    "%A %-d. %B{}", 
 | 
			
		||||
                                                    if date_current.year() != today_date.year()
 | 
			
		||||
                                                    {" %Y"} else {""}
 | 
			
		||||
                                            date_current.format_localized(translate!(
 | 
			
		||||
                                                    i18,
 | 
			
		||||
                                                    if date_current.year() == Local::now().year() {
 | 
			
		||||
                                                        "formats.date_weekday_format"
 | 
			
		||||
                                                    } else {
 | 
			
		||||
                                                        "formats.date_weekday_year_format"
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                ).as_str(),
 | 
			
		||||
                                                Locale::en_US
 | 
			
		||||
                                                LocaleFromLanguageIdentifier::from(
 | 
			
		||||
                                                    &(i18.selected_language)()
 | 
			
		||||
                                                ).into()
 | 
			
		||||
                                            )
 | 
			
		||||
                                            .to_string()
 | 
			
		||||
                                        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,16 @@
 | 
			
		||||
use crate::components::task_list::TaskList;
 | 
			
		||||
use crate::components::task_list_item::TaskListItem;
 | 
			
		||||
use crate::internationalization::LocaleFromLanguageIdentifier;
 | 
			
		||||
use crate::models::category::Category;
 | 
			
		||||
use crate::models::task::TaskWithSubtasks;
 | 
			
		||||
use crate::query::tasks::{use_tasks_with_subtasks_in_category_query};
 | 
			
		||||
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
 | 
			
		||||
use crate::query::QueryValue;
 | 
			
		||||
use chrono::{Local, Locale};
 | 
			
		||||
use chrono::Local;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_query::prelude::QueryResult;
 | 
			
		||||
use crate::components::task_list_item::TaskListItem;
 | 
			
		||||
use dioxus_sdk::i18n::use_i18;
 | 
			
		||||
use dioxus_sdk::translate;
 | 
			
		||||
use voca_rs::Voca;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn CategoryTodayPage() -> Element {
 | 
			
		||||
@@ -22,6 +26,8 @@ pub(crate) fn CategoryTodayPage() -> Element {
 | 
			
		||||
    let long_term_tasks_query = use_tasks_with_subtasks_in_category_query(Category::LongTerm);
 | 
			
		||||
    let long_term_tasks_query_result = long_term_tasks_query.result();
 | 
			
		||||
 | 
			
		||||
    let i18 = use_i18();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        div {
 | 
			
		||||
            class: "pt-4 flex flex-col gap-8",
 | 
			
		||||
@@ -40,7 +46,7 @@ pub(crate) fn CategoryTodayPage() -> Element {
 | 
			
		||||
                                }
 | 
			
		||||
                                div {
 | 
			
		||||
                                    class: "mt-1",
 | 
			
		||||
                                    "Long-term"
 | 
			
		||||
                                    {translate!(i18, "long_term")._upper_first()}
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            div {
 | 
			
		||||
@@ -103,7 +109,7 @@ pub(crate) fn CategoryTodayPage() -> Element {
 | 
			
		||||
                                    }
 | 
			
		||||
                                    div {
 | 
			
		||||
                                        class: "mt-1",
 | 
			
		||||
                                        "Overdue"
 | 
			
		||||
                                        {translate!(i18, "overdue")._upper_first()}
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                TaskList {
 | 
			
		||||
@@ -122,9 +128,23 @@ pub(crate) fn CategoryTodayPage() -> Element {
 | 
			
		||||
                                div {
 | 
			
		||||
                                    class: "mt-1",
 | 
			
		||||
                                    {
 | 
			
		||||
                                        let format = translate!(i18, "formats.date_weekday_format");
 | 
			
		||||
                                        let today_date = today_date.format_localized(
 | 
			
		||||
                                            format.as_str(),
 | 
			
		||||
                                            LocaleFromLanguageIdentifier::from(
 | 
			
		||||
                                                &(i18.selected_language)()
 | 
			
		||||
                                            ).into()
 | 
			
		||||
                                        ).to_string();
 | 
			
		||||
                                        format!(
 | 
			
		||||
                                            "{} – {}",
 | 
			
		||||
                                            translate!(i18, "today")._upper_first(),
 | 
			
		||||
                                            if translate!(i18, "formats.weekday_lowercase_first")
 | 
			
		||||
                                                .parse().unwrap() {
 | 
			
		||||
                                                today_date._lower_first()
 | 
			
		||||
                                            } else {
 | 
			
		||||
                                                today_date
 | 
			
		||||
                                        .format_localized("Today, %A %-d. %B", Locale::en_US)
 | 
			
		||||
                                        .to_string()
 | 
			
		||||
                                            }
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
use crate::components::category_input::CategoryInput;
 | 
			
		||||
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
 | 
			
		||||
use crate::components::subtasks_form::SubtasksForm;
 | 
			
		||||
use crate::models::category::{CalendarTime, Category, Reoccurrence};
 | 
			
		||||
use crate::models::task::NewTask;
 | 
			
		||||
use crate::models::task::Task;
 | 
			
		||||
@@ -7,12 +8,14 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
 | 
			
		||||
use crate::route::Route;
 | 
			
		||||
use crate::server::projects::get_projects;
 | 
			
		||||
use crate::server::tasks::{create_task, delete_task, edit_task};
 | 
			
		||||
use chrono::{Duration};
 | 
			
		||||
use chrono::Duration;
 | 
			
		||||
use dioxus::core_macro::{component, rsx};
 | 
			
		||||
use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_query::prelude::use_query_client;
 | 
			
		||||
use crate::components::subtasks_form::SubtasksForm;
 | 
			
		||||
use dioxus_sdk::i18n::use_i18;
 | 
			
		||||
use dioxus_sdk::translate;
 | 
			
		||||
use voca_rs::Voca;
 | 
			
		||||
 | 
			
		||||
const REMINDER_OFFSETS: [Option<Duration>; 17] = [
 | 
			
		||||
    None,
 | 
			
		||||
@@ -79,6 +82,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
 | 
			
		||||
    let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
 | 
			
		||||
    let task_for_submit = task.clone();
 | 
			
		||||
 | 
			
		||||
    let i18 = use_i18();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        div {
 | 
			
		||||
            class: "p-4 flex flex-col gap-4",
 | 
			
		||||
@@ -172,7 +177,7 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
 | 
			
		||||
                        id: "input_project",
 | 
			
		||||
                        option {
 | 
			
		||||
                            value: 0,
 | 
			
		||||
                            "None"
 | 
			
		||||
                            {translate!(i18, "none")}
 | 
			
		||||
                        },
 | 
			
		||||
                        for project in projects {
 | 
			
		||||
                            option {
 | 
			
		||||
@@ -330,13 +335,14 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
 | 
			
		||||
                                label {
 | 
			
		||||
                                    r#for: "category_calendar_reminder_offset_index",
 | 
			
		||||
                                    class: "pr-3 min-w-16 text-right",
 | 
			
		||||
                                    {REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map(
 | 
			
		||||
                                    {REMINDER_OFFSETS[category_calendar_reminder_offset_index()]
 | 
			
		||||
                                        .map(
 | 
			
		||||
                                            |offset| if offset.num_hours() < 1 {
 | 
			
		||||
                                                format!("{} min", offset.num_minutes())
 | 
			
		||||
                                            } else {
 | 
			
		||||
                                                format!("{} h", offset.num_hours())
 | 
			
		||||
                                            }
 | 
			
		||||
                                    ).unwrap_or_else(|| "none".to_string())}
 | 
			
		||||
                                        ).unwrap_or_else(|| translate!(i18, "none"))}
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,18 @@
 | 
			
		||||
use chrono::{Datelike, Local};
 | 
			
		||||
use crate::internationalization::LocaleFromLanguageIdentifier;
 | 
			
		||||
use crate::models::category::Category;
 | 
			
		||||
use crate::models::task::TaskWithSubtasks;
 | 
			
		||||
use chrono::{Datelike, Local};
 | 
			
		||||
use dioxus::core_macro::rsx;
 | 
			
		||||
use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_sdk::i18n::use_i18;
 | 
			
		||||
use dioxus_sdk::translate;
 | 
			
		||||
use voca_rs::Voca;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
 | 
			
		||||
    let i18 = use_i18();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        div {
 | 
			
		||||
            class: "flex flex-col",
 | 
			
		||||
@@ -22,11 +28,47 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
 | 
			
		||||
                        i {
 | 
			
		||||
                            class: "fa-solid fa-bomb"
 | 
			
		||||
                        },
 | 
			
		||||
                        {deadline.format(if deadline.year() == Local::now().year() {
 | 
			
		||||
                            " %m. %-d."
 | 
			
		||||
                        {
 | 
			
		||||
                            let today_date = Local::now().date_naive();
 | 
			
		||||
                            format!(
 | 
			
		||||
                                " {}",
 | 
			
		||||
                                if deadline == today_date - chrono::Days::new(1) {
 | 
			
		||||
                                    translate!(i18, "yesterday")
 | 
			
		||||
                                } else if deadline == today_date {
 | 
			
		||||
                                    translate!(i18, "today")
 | 
			
		||||
                                } else if deadline == today_date + chrono::Days::new(1) {
 | 
			
		||||
                                    translate!(i18, "tomorrow")
 | 
			
		||||
                                } else if deadline > today_date
 | 
			
		||||
                                    && deadline <= today_date + chrono::Days::new(7) {
 | 
			
		||||
                                    let deadline = deadline.format_localized(
 | 
			
		||||
                                        "%A",
 | 
			
		||||
                                        LocaleFromLanguageIdentifier::from(
 | 
			
		||||
                                            &(i18.selected_language)()
 | 
			
		||||
                                        ).into()
 | 
			
		||||
                                    ).to_string();
 | 
			
		||||
                                    if translate!(i18, "formats.weekday_lowercase_first")
 | 
			
		||||
                                        .parse().unwrap() {
 | 
			
		||||
                                        deadline._lower_first()
 | 
			
		||||
                                    } else {
 | 
			
		||||
                            " %m. %-d. %Y"
 | 
			
		||||
                        }).to_string()}
 | 
			
		||||
                                        deadline
 | 
			
		||||
                                    }
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    let format = translate!(i18,
 | 
			
		||||
                                        if deadline.year() == today_date.year() {
 | 
			
		||||
                                            "formats.date_format"
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            "formats.date_year_format"
 | 
			
		||||
                                        }
 | 
			
		||||
                                    );
 | 
			
		||||
                                    deadline.format_localized(
 | 
			
		||||
                                        format.as_str(),
 | 
			
		||||
                                        LocaleFromLanguageIdentifier::from(
 | 
			
		||||
                                            &(i18.selected_language)()
 | 
			
		||||
                                        ).into()
 | 
			
		||||
                                    ).to_string()
 | 
			
		||||
                                }
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if let Category::Calendar { time, .. } = task.task().category() {
 | 
			
		||||
@@ -36,7 +78,10 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
 | 
			
		||||
                            i {
 | 
			
		||||
                                class: "fa-solid fa-clock"
 | 
			
		||||
                            },
 | 
			
		||||
                            {calendar_time.time().format(" %k:%M").to_string()}
 | 
			
		||||
                            {
 | 
			
		||||
                                let format = translate!(i18, "formats.time_format");
 | 
			
		||||
                                format!(" {}",calendar_time.time().format(format.as_str()))
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								src/internationalization/cs_cz.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/internationalization/cs_cz.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "id": "cs-CZ",
 | 
			
		||||
  "texts": {
 | 
			
		||||
    "none": "žádný",
 | 
			
		||||
    "long_term": "dlouhodobé",
 | 
			
		||||
    "yesterday": "včera",
 | 
			
		||||
    "today": "dnes",
 | 
			
		||||
    "tomorrow": "zítra",
 | 
			
		||||
    "overdue": "zpožděné",
 | 
			
		||||
    "formats": {
 | 
			
		||||
      "date_format": "%-d. %B",
 | 
			
		||||
      "date_year_format": "%-d. %B %Y",
 | 
			
		||||
      "date_weekday_format": "%A %-d. %B",
 | 
			
		||||
      "date_weekday_year_format": "%A %-d. %B %Y",
 | 
			
		||||
      "weekday_lowercase_first": "true",
 | 
			
		||||
      "time_format": "%-H:%M"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/internationalization/en_us.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/internationalization/en_us.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "id": "en-US",
 | 
			
		||||
  "texts": {
 | 
			
		||||
    "none": "none",
 | 
			
		||||
    "long_term": "long-term",
 | 
			
		||||
    "yesterday": "yesterday",
 | 
			
		||||
    "today": "today",
 | 
			
		||||
    "tomorrow": "tomorrow",
 | 
			
		||||
    "overdue": "overdue",
 | 
			
		||||
    "formats": {
 | 
			
		||||
      "date_format": "%B %-d",
 | 
			
		||||
      "date_year_format": "%B %-d, %Y",
 | 
			
		||||
      "date_weekday_format": "%A, %B %-d",
 | 
			
		||||
      "date_weekday_year_format": "%A, %B %-d, %Y",
 | 
			
		||||
      "weekday_lowercase_first": "false",
 | 
			
		||||
      "time_format": "%-I:%M %P"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/internationalization/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/internationalization/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
use std::ops::Deref;
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
use chrono::Locale;
 | 
			
		||||
use dioxus_sdk::i18n::Language;
 | 
			
		||||
use unic_langid_impl::LanguageIdentifier;
 | 
			
		||||
 | 
			
		||||
const EN_US: &str = include_str!("en_us.json");
 | 
			
		||||
const CS_CZ: &str = include_str!("cs_cz.json");
 | 
			
		||||
 | 
			
		||||
pub(crate) fn get_languages() -> Vec<Language> {
 | 
			
		||||
    Vec::from([EN_US, CS_CZ]).into_iter().map(|texts| Language::from_str(texts).unwrap()).collect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct LocaleFromLanguageIdentifier<'a>(&'a LanguageIdentifier);
 | 
			
		||||
 | 
			
		||||
impl<'a> Deref for LocaleFromLanguageIdentifier<'a> {
 | 
			
		||||
    type Target = LanguageIdentifier;
 | 
			
		||||
 | 
			
		||||
    fn deref(&self) -> &Self::Target {
 | 
			
		||||
        self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<LocaleFromLanguageIdentifier<'a>> for Locale {
 | 
			
		||||
    fn from(language_identifier: LocaleFromLanguageIdentifier) -> Self {
 | 
			
		||||
        language_identifier.to_string().replace("-", "_").parse().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> From<&'a LanguageIdentifier> for LocaleFromLanguageIdentifier<'a> {
 | 
			
		||||
    fn from(language_identifier: &'a LanguageIdentifier) -> Self {
 | 
			
		||||
        LocaleFromLanguageIdentifier(language_identifier)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,7 @@ mod schema;
 | 
			
		||||
mod server;
 | 
			
		||||
mod query;
 | 
			
		||||
mod utils;
 | 
			
		||||
mod internationalization;
 | 
			
		||||
 | 
			
		||||
use components::app::App;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ use dotenvy::dotenv;
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
pub(crate) fn establish_database_connection() -> ConnectionResult<PgConnection> {
 | 
			
		||||
    dotenv().ok();
 | 
			
		||||
    dotenv().expect("Could not load environment variables.");
 | 
			
		||||
 | 
			
		||||
    let database_url =
 | 
			
		||||
        env::var("DATABASE_URL").expect("The environment variable DATABASE_URL must be set.");
 | 
			
		||||
        env::var("DATABASE_URL").expect("The environment variable DATABASE_URL has to be set.");
 | 
			
		||||
    PgConnection::establish(&database_url)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								src/server/internationalization.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/server/internationalization.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
use std::env;
 | 
			
		||||
use dioxus::prelude::ServerFnError;
 | 
			
		||||
use unic_langid_impl::LanguageIdentifier;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dotenvy::dotenv;
 | 
			
		||||
 | 
			
		||||
#[server]
 | 
			
		||||
pub(crate) async fn get_language_identifier() -> Result<LanguageIdentifier, ServerFnError> {
 | 
			
		||||
    dotenv().expect("Could not load environment variables from the .env file.");
 | 
			
		||||
    
 | 
			
		||||
    Ok(env::var("LANGUAGE_CODE")
 | 
			
		||||
        .expect("The environment variable LANGUAGE_CODE must be set.")
 | 
			
		||||
        .parse::<LanguageIdentifier>()?)
 | 
			
		||||
}
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 LGTM with nitpicks! The server-side code for retrieving the language identifier looks good. Just a couple of nitpicks: 
 **LGTM with nitpicks!**
The server-side code for retrieving the language identifier looks good. Just a couple of nitpicks:
1. If the `LANGUAGE_CODE` environment variable is always set by the deployment process, you can remove the `dotenv` import and the `dotenv()` call.
2. Consider changing the visibility modifier from `pub(crate)` to `pub` for consistency with other server functions, unless you have a specific reason to restrict the visibility to the current crate.
<!-- This is an auto-generated comment by CodeRabbit --> 
			
			
		 | 
			||||
@@ -2,3 +2,4 @@ mod database_connection;
 | 
			
		||||
pub(crate) mod projects;
 | 
			
		||||
pub(crate) mod tasks;
 | 
			
		||||
pub(crate) mod subtasks;
 | 
			
		||||
pub(crate) mod internationalization;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,18 @@
 | 
			
		||||
use std::cmp::Ordering;
 | 
			
		||||
 | 
			
		||||
use std::ops::Deref;
 | 
			
		||||
/* The default ordering of `Option`s is `None` being less than `Some`. The purpose of this struct is
 | 
			
		||||
   to reverse that. */
 | 
			
		||||
#[derive(PartialEq)]
 | 
			
		||||
pub(crate) struct ReverseOrdOption<'a, T>(&'a Option<T>);
 | 
			
		||||
 | 
			
		||||
impl<'a, T> Deref for ReverseOrdOption<'a, T> {
 | 
			
		||||
    type Target = Option<T>;
 | 
			
		||||
 | 
			
		||||
    fn deref(&self) -> &Self::Target {
 | 
			
		||||
        self.0
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, T: Ord> Eq for ReverseOrdOption<'a, T> {}
 | 
			
		||||
 | 
			
		||||
impl<'a, T: Ord> PartialOrd<Self> for ReverseOrdOption<'a, T> {
 | 
			
		||||
@@ -15,7 +23,7 @@ impl<'a, T: Ord> PartialOrd<Self> for ReverseOrdOption<'a, T> {
 | 
			
		||||
 | 
			
		||||
impl<'a, T: Ord> Ord for ReverseOrdOption<'a, T> {
 | 
			
		||||
    fn cmp(&self, other: &Self) -> Ordering {
 | 
			
		||||
        match (self.0.as_ref(), other.0.as_ref()) {
 | 
			
		||||
        match (self.as_ref(), other.as_ref()) {
 | 
			
		||||
            (None, None) => Ordering::Equal,
 | 
			
		||||
            (None, Some(_)) => Ordering::Greater,
 | 
			
		||||
            (Some(_), None) => Ordering::Less,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user
	
Improve error handling when unwrapping the
language_identifier.The
language_identifieris being force unwrapped usingunwrap()at line 15. This can lead to a panic if the value isNone.Consider using
matchorif letto handle theNonecase gracefully: