feat: internationalization #43

Merged
matous-volf merged 8 commits from feat/internationalization into main 2024-09-10 14:44:47 +00:00
17 changed files with 314 additions and 42 deletions

View File

@ -1 +1,2 @@
DATABASE_URL=postgres://app:app@db/todo_baggins DATABASE_URL=postgres://app:app@db/todo_baggins
LANGUAGE_CODE=en-US

87
Cargo.lock generated
View File

@ -1021,6 +1021,22 @@ dependencies = [
"tracing", "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]] [[package]]
name = "dioxus-signals" name = "dioxus-signals"
version = "0.5.7" version = "0.5.7"
@ -1096,6 +1112,17 @@ dependencies = [
"syn 2.0.74", "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]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@ -1384,8 +1411,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -2772,6 +2801,12 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "stfu8"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -2872,6 +2907,15 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.0"
@ -2903,6 +2947,7 @@ dependencies = [
"dioxus", "dioxus",
"dioxus-logger", "dioxus-logger",
"dioxus-query", "dioxus-query",
"dioxus-sdk",
"dotenvy", "dotenvy",
"serde", "serde",
"serde_json", "serde_json",
@ -2910,7 +2955,9 @@ dependencies = [
"time", "time",
"tracing", "tracing",
"tracing-wasm", "tracing-wasm",
"unic-langid-impl",
"validator", "validator",
"voca_rs",
] ]
[[package]] [[package]]
@ -3138,6 +3185,25 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 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]] [[package]]
name = "unicase" name = "unicase"
version = "2.7.0" version = "2.7.0"
@ -3203,6 +3269,16 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 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]] [[package]]
name = "validator" name = "validator"
version = "0.18.1" version = "0.18.1"
@ -3257,6 +3333,17 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 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]] [[package]]
name = "waker-fn" name = "waker-fn"
version = "1.2.0" version = "1.2.0"

View File

@ -24,6 +24,9 @@ serde_with = { version = "3.9.0", features = ["chrono_0_4"] }
async-std = "1.12.0" async-std = "1.12.0"
dioxus-query = "0.5.1" dioxus-query = "0.5.1"
time = "0.3.36" time = "0.3.36"
dioxus-sdk = { version = "0.5.0", features = ["i18n"] }
unic-langid-impl = "0.9.5"
voca_rs = "1.15.2"
[features] [features]
default = [] default = []

View File

@ -4,11 +4,17 @@ use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::{use_init_query_client}; use dioxus_query::prelude::{use_init_query_client};
use crate::query::{QueryErrors, QueryKey, QueryValue}; 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] #[component]
pub(crate) fn App() -> Element { pub(crate) fn App() -> Element {
use_init_query_client::<QueryValue, QueryErrors, QueryKey>(); use_init_query_client::<QueryValue, QueryErrors, QueryKey>();
let language_identifier = use_server_future(get_language_identifier)?.unwrap().unwrap();
coderabbitai[bot] commented 2024-09-10 14:34:32 +00:00 (Migrated from github.com)
Review

Improve error handling when unwrapping the language_identifier.

The language_identifier is being force unwrapped using unwrap() at line 15. This can lead to a panic if the value is None.

Consider using match or if let to handle the None case gracefully:

let language_identifier = match use_server_future(get_language_identifier)?.unwrap() {
    Some(identifier) => identifier,
    None => {
        // Handle the case when language identifier is not available
        // For example, you can use a default language identifier
        "en-US".to_string()
    }
};
**Improve error handling when unwrapping the `language_identifier`.** The `language_identifier` is being force unwrapped using `unwrap()` at line 15. This can lead to a panic if the value is `None`. Consider using `match` or `if let` to handle the `None` case gracefully: ```rust let language_identifier = match use_server_future(get_language_identifier)?.unwrap() { Some(identifier) => identifier, None => { // Handle the case when language identifier is not available // For example, you can use a default language identifier "en-US".to_string() } }; ``` <!-- This is an auto-generated comment by CodeRabbit -->
use_init_i18n(language_identifier.clone(), language_identifier, get_languages);
rsx! { rsx! {
div { div {
class: "min-h-screen text-zinc-200 bg-zinc-800 pt-4 pb-36", class: "min-h-screen text-zinc-200 bg-zinc-800 pt-4 pb-36",

View File

@ -1,13 +1,16 @@
use crate::components::task_list::TaskList;
use crate::internationalization::LocaleFromLanguageIdentifier;
use crate::models::category::Category; 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::core_macro::rsx;
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult; use dioxus_query::prelude::QueryResult;
use crate::components::task_list::TaskList; use dioxus_sdk::i18n::use_i18;
use crate::query::QueryValue; use dioxus_sdk::translate;
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
use crate::models::task::{TaskWithSubtasks};
const CALENDAR_LENGTH_DAYS: usize = 366 * 3; const CALENDAR_LENGTH_DAYS: usize = 366 * 3;
@ -20,6 +23,8 @@ pub(crate) fn CategoryCalendarPage() -> Element {
}); });
let tasks_query_result = tasks.result(); let tasks_query_result = tasks.result();
let i18 = use_i18();
rsx! { rsx! {
match tasks_query_result.value() { match tasks_query_result.value() {
QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
@ -37,14 +42,17 @@ pub(crate) fn CategoryCalendarPage() -> Element {
div { div {
class: "pt-1", class: "pt-1",
{ {
date_current date_current.format_localized(translate!(
.format_localized( i18,
format!( if date_current.year() == Local::now().year() {
"%A %-d. %B{}", "formats.date_weekday_format"
if date_current.year() != today_date.year() } else {
{" %Y"} else {""} "formats.date_weekday_year_format"
}
).as_str(), ).as_str(),
Locale::en_US LocaleFromLanguageIdentifier::from(
&(i18.selected_language)()
).into()
) )
.to_string() .to_string()
} }

View File

@ -1,12 +1,16 @@
use crate::components::task_list::TaskList; 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::category::Category;
use crate::models::task::TaskWithSubtasks; 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 crate::query::QueryValue;
use chrono::{Local, Locale}; use chrono::Local;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult; 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] #[component]
pub(crate) fn CategoryTodayPage() -> Element { 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 = use_tasks_with_subtasks_in_category_query(Category::LongTerm);
let long_term_tasks_query_result = long_term_tasks_query.result(); let long_term_tasks_query_result = long_term_tasks_query.result();
let i18 = use_i18();
rsx! { rsx! {
div { div {
class: "pt-4 flex flex-col gap-8", class: "pt-4 flex flex-col gap-8",
@ -40,7 +46,7 @@ pub(crate) fn CategoryTodayPage() -> Element {
} }
div { div {
class: "mt-1", class: "mt-1",
"Long-term" {translate!(i18, "long_term")._upper_first()}
} }
} }
div { div {
@ -103,7 +109,7 @@ pub(crate) fn CategoryTodayPage() -> Element {
} }
div { div {
class: "mt-1", class: "mt-1",
"Overdue" {translate!(i18, "overdue")._upper_first()}
} }
} }
TaskList { TaskList {
@ -122,9 +128,23 @@ pub(crate) fn CategoryTodayPage() -> Element {
div { div {
class: "mt-1", 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 today_date
.format_localized("Today, %A %-d. %B", Locale::en_US) }
.to_string() )
} }
} }
} }

View File

@ -1,5 +1,6 @@
use crate::components::category_input::CategoryInput; use crate::components::category_input::CategoryInput;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::components::subtasks_form::SubtasksForm;
use crate::models::category::{CalendarTime, Category, Reoccurrence}; use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask; use crate::models::task::NewTask;
use crate::models::task::Task; use crate::models::task::Task;
@ -7,12 +8,14 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::route::Route; use crate::route::Route;
use crate::server::projects::get_projects; use crate::server::projects::get_projects;
use crate::server::tasks::{create_task, delete_task, edit_task}; use crate::server::tasks::{create_task, delete_task, edit_task};
use chrono::{Duration}; use chrono::Duration;
use dioxus::core_macro::{component, rsx}; use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client; 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] = [ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
None, 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 query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let task_for_submit = task.clone(); let task_for_submit = task.clone();
let i18 = use_i18();
rsx! { rsx! {
div { div {
class: "p-4 flex flex-col gap-4", 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", id: "input_project",
option { option {
value: 0, value: 0,
"None" {translate!(i18, "none")}
}, },
for project in projects { for project in projects {
option { option {
@ -330,13 +335,14 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
label { label {
r#for: "category_calendar_reminder_offset_index", r#for: "category_calendar_reminder_offset_index",
class: "pr-3 min-w-16 text-right", 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 { |offset| if offset.num_hours() < 1 {
format!("{} min", offset.num_minutes()) format!("{} min", offset.num_minutes())
} else { } else {
format!("{} h", offset.num_hours()) format!("{} h", offset.num_hours())
} }
).unwrap_or_else(|| "none".to_string())} ).unwrap_or_else(|| translate!(i18, "none"))}
} }
} }
} }

View File

@ -1,12 +1,18 @@
use chrono::{Datelike, Local}; use crate::internationalization::LocaleFromLanguageIdentifier;
use crate::models::category::Category; use crate::models::category::Category;
use crate::models::task::TaskWithSubtasks; use crate::models::task::TaskWithSubtasks;
use chrono::{Datelike, Local};
use dioxus::core_macro::rsx; use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_sdk::i18n::use_i18;
use dioxus_sdk::translate;
use voca_rs::Voca;
#[component] #[component]
pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element { pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
let i18 = use_i18();
rsx! { rsx! {
div { div {
class: "flex flex-col", class: "flex flex-col",
@ -22,11 +28,47 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
i { i {
class: "fa-solid fa-bomb" 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 { } else {
" %m. %-d. %Y" deadline
}).to_string()} }
} 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() { if let Category::Calendar { time, .. } = task.task().category() {
@ -36,7 +78,10 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element {
i { i {
class: "fa-solid fa-clock" 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()))
}
} }
} }
} }

View 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"
}
}
}

View 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"
}
}
}

View 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)
}
}

View File

@ -6,6 +6,7 @@ mod schema;
mod server; mod server;
mod query; mod query;
mod utils; mod utils;
mod internationalization;
use components::app::App; use components::app::App;
use dioxus::prelude::*; use dioxus::prelude::*;

View File

@ -4,9 +4,9 @@ use dotenvy::dotenv;
use std::env; use std::env;
pub(crate) fn establish_database_connection() -> ConnectionResult<PgConnection> { pub(crate) fn establish_database_connection() -> ConnectionResult<PgConnection> {
dotenv().ok(); dotenv().expect("Could not load environment variables.");
let database_url = 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) PgConnection::establish(&database_url)
} }

View 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>()?)
}
coderabbitai[bot] commented 2024-09-10 14:34:32 +00:00 (Migrated from github.com)
Review

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.

**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 -->

View File

@ -2,3 +2,4 @@ mod database_connection;
pub(crate) mod projects; pub(crate) mod projects;
pub(crate) mod tasks; pub(crate) mod tasks;
pub(crate) mod subtasks; pub(crate) mod subtasks;
pub(crate) mod internationalization;

View File

@ -1,10 +1,18 @@
use std::cmp::Ordering; 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 /* The default ordering of `Option`s is `None` being less than `Some`. The purpose of this struct is
to reverse that. */ to reverse that. */
#[derive(PartialEq)] #[derive(PartialEq)]
pub(crate) struct ReverseOrdOption<'a, T>(&'a Option<T>); 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> Eq for ReverseOrdOption<'a, T> {}
impl<'a, T: Ord> PartialOrd<Self> 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> { impl<'a, T: Ord> Ord for ReverseOrdOption<'a, T> {
fn cmp(&self, other: &Self) -> Ordering { 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, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater, (None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less, (Some(_), None) => Ordering::Less,