use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/builtin_langs.rs"));
fn get_config_dir() -> PathBuf {
if let Some(mut dir) = dirs::config_dir() {
dir.push("nyado");
if dir.exists() && dir.is_dir() {
return dir;
}
}
if Path::new("config").exists() && Path::new("config").is_dir() {
return PathBuf::from("config");
}
let mut fallback = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
fallback.push("nyado");
fallback
}
#[derive(Debug, Deserialize, Clone)]
struct Localization {
ui: HashMap<String, String>,
pending: HashMap<String, String>,
done: HashMap<String, String>,
pinned: HashMap<String, String>,
total: HashMap<String, String>,
messages: HashMap<String, String>,
created_prefix: String,
done_prefix: String,
pinned_marker: String,
selected_header: String,
tags_header: String,
stats_header: String,
mood_all_done: String,
mood_empty: String,
mood_one: String,
mood_few: String,
mood_several: String,
mood_many: String,
popup_new_title: String,
popup_new_hint: String,
popup_edit_title: String,
popup_edit_hint: String,
popup_set_tag_title: String,
popup_set_tag_hint_existing: String,
popup_set_tag_hint_empty: String,
popup_delete_confirm: String,
popup_delete_all_confirm: String,
popup_delete_all_warning: String,
popup_search_title: String,
popup_search_hint: String,
statusbar_hint_wide: String,
statusbar_hint_medium: String,
statusbar_hint_narrow: String,
progress_label: String,
column_header: String,
scroll_up: String,
scroll_down: String,
topbar_title: String,
topbar_filter_prefix: String,
topbar_search_prefix: String,
topbar_date_format: String,
title: String,
right_title: String,
celebration_line1: String,
celebration_line2: String,
celebration_line3: String,
celebration_line4: String,
celebration_line5: String,
celebration_line6: String,
empty_list_line1: String,
empty_list_line2: String,
popup_due_date_title: String,
popup_due_date_hint: String,
popup_due_time_hint: String,
due_date_cleared: String,
due_date_set: String,
due_date_invalid: String,
#[serde(flatten)]
extra: HashMap<String, String>,
}
pub struct I18n {
languages: Vec<(String, Localization)>,
current_index: usize,
default_loc: Localization,
}
impl I18n {
pub fn new() -> Result<Self> {
let config_dir = get_config_dir();
let mut languages = Vec::new();
let mut builtin_map = HashMap::new();
for (code, content) in BUILTIN_LANGS {
builtin_map.insert(*code, *content);
}
if config_dir.exists() && config_dir.is_dir() {
for entry in fs::read_dir(&config_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.file_name().and_then(|n| n.to_str()).map_or(false, |name| name.starts_with("lang_") && name.ends_with(".toml")) {
let name = path.file_name().unwrap().to_str().unwrap();
let code = &name[5..name.len()-5];
let content = fs::read_to_string(&path)?;
let loc: Localization = toml::from_str(&content)?;
languages.push((code.to_string(), loc));
}
}
}
for (code, content) in builtin_map {
if !languages.iter().any(|(c, _)| c == code) {
let loc: Localization = toml::from_str(content)?;
languages.push((code.to_string(), loc));
}
}
if languages.is_empty() {
anyhow::bail!("No language files found");
}
languages.sort_by(|(code_a, _), (code_b, _)| {
if code_a == "en" {
std::cmp::Ordering::Less
} else if code_b == "en" {
std::cmp::Ordering::Greater
} else {
code_a.cmp(code_b)
}
});
let default_loc = languages
.iter()
.find(|(code, _)| code == "en")
.map(|(_, loc)| loc.clone())
.unwrap_or_else(|| languages[0].1.clone());
Ok(Self {
languages,
current_index: 0,
default_loc,
})
}
pub fn current_code(&self) -> &str {
&self.languages[self.current_index].0
}
pub fn set_language_by_code(&mut self, code: &str) -> bool {
for (i, (c, _)) in self.languages.iter().enumerate() {
if c == code {
self.current_index = i;
return true;
}
}
false
}
pub fn toggle_language(&mut self) {
self.current_index = (self.current_index + 1) % self.languages.len();
}
fn get_from_loc<'a>(&'a self, loc: &'a Localization, key: &str) -> Option<&'a str> {
if let Some(v) = loc.ui.get(key) {
return Some(v);
}
if let Some(stripped) = key.strip_prefix("pending.") {
if let Some(v) = loc.pending.get(stripped) {
return Some(v);
}
}
if let Some(stripped) = key.strip_prefix("done.") {
if let Some(v) = loc.done.get(stripped) {
return Some(v);
}
}
if let Some(stripped) = key.strip_prefix("pinned.") {
if let Some(v) = loc.pinned.get(stripped) {
return Some(v);
}
}
if let Some(stripped) = key.strip_prefix("total.") {
if let Some(v) = loc.total.get(stripped) {
return Some(v);
}
}
if let Some(stripped) = key.strip_prefix("messages.") {
if let Some(v) = loc.messages.get(stripped) {
return Some(v);
}
}
if let Some(v) = loc.extra.get(key) {
return Some(v);
}
match key {
"created_prefix" => Some(&loc.created_prefix),
"done_prefix" => Some(&loc.done_prefix),
"pinned_marker" => Some(&loc.pinned_marker),
"selected_header" => Some(&loc.selected_header),
"tags_header" => Some(&loc.tags_header),
"stats_header" => Some(&loc.stats_header),
"mood_all_done" => Some(&loc.mood_all_done),
"mood_empty" => Some(&loc.mood_empty),
"mood_one" => Some(&loc.mood_one),
"mood_few" => Some(&loc.mood_few),
"mood_several" => Some(&loc.mood_several),
"mood_many" => Some(&loc.mood_many),
"popup_new_title" => Some(&loc.popup_new_title),
"popup_new_hint" => Some(&loc.popup_new_hint),
"popup_edit_title" => Some(&loc.popup_edit_title),
"popup_edit_hint" => Some(&loc.popup_edit_hint),
"popup_set_tag_title" => Some(&loc.popup_set_tag_title),
"popup_set_tag_hint_existing" => Some(&loc.popup_set_tag_hint_existing),
"popup_set_tag_hint_empty" => Some(&loc.popup_set_tag_hint_empty),
"popup_delete_confirm" => Some(&loc.popup_delete_confirm),
"popup_delete_all_confirm" => Some(&loc.popup_delete_all_confirm),
"popup_delete_all_warning" => Some(&loc.popup_delete_all_warning),
"popup_search_title" => Some(&loc.popup_search_title),
"popup_search_hint" => Some(&loc.popup_search_hint),
"statusbar_hint_wide" => Some(&loc.statusbar_hint_wide),
"statusbar_hint_medium" => Some(&loc.statusbar_hint_medium),
"statusbar_hint_narrow" => Some(&loc.statusbar_hint_narrow),
"progress_label" => Some(&loc.progress_label),
"column_header" => Some(&loc.column_header),
"scroll_up" => Some(&loc.scroll_up),
"scroll_down" => Some(&loc.scroll_down),
"topbar_title" => Some(&loc.topbar_title),
"topbar_filter_prefix" => Some(&loc.topbar_filter_prefix),
"topbar_search_prefix" => Some(&loc.topbar_search_prefix),
"topbar_date_format" => Some(&loc.topbar_date_format),
"title" => Some(&loc.title),
"right_title" => Some(&loc.right_title),
"celebration_line1" => Some(&loc.celebration_line1),
"celebration_line2" => Some(&loc.celebration_line2),
"celebration_line3" => Some(&loc.celebration_line3),
"celebration_line4" => Some(&loc.celebration_line4),
"celebration_line5" => Some(&loc.celebration_line5),
"celebration_line6" => Some(&loc.celebration_line6),
"empty_list_line1" => Some(&loc.empty_list_line1),
"empty_list_line2" => Some(&loc.empty_list_line2),
"popup_due_date_title" => Some(&loc.popup_due_date_title),
"popup_due_date_hint" => Some(&loc.popup_due_date_hint),
"popup_due_time_hint" => Some(&loc.popup_due_time_hint),
"due_date_cleared" => Some(&loc.due_date_cleared),
"due_date_set" => Some(&loc.due_date_set),
"due_date_invalid" => Some(&loc.due_date_invalid),
_ => None,
}
}
pub fn get<'a>(&'a self, key: &'a str) -> &'a str {
let current_loc = &self.languages[self.current_index].1;
if let Some(val) = self.get_from_loc(current_loc, key) {
return val;
}
if let Some(val) = self.get_from_loc(&self.default_loc, key) {
return val;
}
"???"
}
}