#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Lang {
En,
De,
}
pub struct Text {
pub person: &'static str,
pub given_name: &'static str,
pub family_name: &'static str,
pub additional_names: &'static str,
pub websites: &'static str,
pub country: &'static str,
pub affiliations: &'static str,
pub started: &'static str,
pub ended: &'static str,
pub filters: &'static str,
pub facet_year: &'static str,
pub facet_type: &'static str,
pub open_all: &'static str,
pub close_all: &'static str,
pub clear_filters: &'static str,
pub search: &'static str,
pub search_placeholder: &'static str,
pub copy_citation: &'static str,
pub published: &'static str,
pub in_container: &'static str,
pub license: &'static str,
pub no_results: &'static str,
pub no_works: &'static str,
pub pagination: &'static str,
pub previous_page: &'static str,
pub next_page: &'static str,
pub language: &'static str,
pub apply: &'static str,
pub featured_only: &'static str,
pub clear: &'static str,
pub export_format: &'static str,
pub citation_style: &'static str,
pub formatted_citation: &'static str,
pub export: &'static str,
pub copy: &'static str,
pub copied: &'static str,
}
const EN: Text = Text {
person: "Person",
given_name: "Given name",
family_name: "Family name",
additional_names: "Also known as",
websites: "Websites & social links",
country: "Country",
affiliations: "Affiliations",
started: "Started",
ended: "Ended at",
filters: "Filters",
facet_year: "Publication year",
facet_type: "Content type",
open_all: "Open all",
close_all: "Close all",
clear_filters: "Clear filters",
search: "Search",
search_placeholder: "Search these works",
copy_citation: "Copy citation",
published: "published",
in_container: "in",
license: "Distributed under the terms of the",
no_results: "No results found.",
no_works: "No works on this page.",
pagination: "Pagination",
previous_page: "Previous page",
next_page: "Next page",
language: "Language",
apply: "Apply",
featured_only: "Featured only",
clear: "Clear",
export_format: "Export format",
citation_style: "Citation style",
formatted_citation: "Formatted Citation",
export: "Export",
copy: "Copy",
copied: "Copied",
};
const DE: Text = Text {
person: "Person",
given_name: "Vorname",
family_name: "Familienname",
additional_names: "Auch bekannt als",
websites: "Webseiten & soziale Netzwerke",
country: "Land",
affiliations: "Zugehörigkeiten",
started: "Beginn",
ended: "Ende",
filters: "Filter",
facet_year: "Erscheinungsjahr",
facet_type: "Inhaltstyp",
open_all: "Alle öffnen",
close_all: "Alle schließen",
clear_filters: "Filter zurücksetzen",
search: "Suche",
search_placeholder: "Diese Werke durchsuchen",
copy_citation: "Zitat kopieren",
published: "veröffentlicht",
in_container: "in",
license: "Veröffentlicht unter den Bedingungen der",
no_results: "Keine Ergebnisse gefunden.",
no_works: "Keine Werke auf dieser Seite.",
pagination: "Seitennummerierung",
previous_page: "Vorherige Seite",
next_page: "Nächste Seite",
language: "Sprache",
apply: "Anwenden",
featured_only: "Nur hervorgehobene",
clear: "Löschen",
export_format: "Exportformat",
citation_style: "Zitierstil",
formatted_citation: "Formatiertes Zitat",
export: "Exportieren",
copy: "Kopieren",
copied: "Kopiert",
};
pub struct LayoutText {
pub lang: &'static str,
pub toggle_dark: &'static str,
pub sign_out: &'static str,
pub footer: &'static str,
pub built_with: &'static str,
pub by: &'static str,
pub software: &'static str,
pub mit_license: &'static str,
}
pub const LAYOUT_EN: LayoutText = LayoutText {
lang: "en",
toggle_dark: "Toggle dark mode",
sign_out: "Sign out",
footer: "Self-hosted Zotero sync and file storage",
built_with: "Built with",
by: "by",
software: "software provided under an",
mit_license: "MIT license",
};
const LAYOUT_DE: LayoutText = LayoutText {
lang: "de",
toggle_dark: "Dunkelmodus umschalten",
sign_out: "Abmelden",
footer: "Selbst gehostete Zotero-Synchronisierung und Dateiablage",
built_with: "Erstellt mit",
by: "von",
software: "Software bereitgestellt unter einer",
mit_license: "MIT-Lizenz",
};
const MONTHS_EN: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const MONTHS_DE: [&str; 12] = [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember",
];
const ITEM_TYPES_DE: &[(&str, &str)] = &[
("artwork", "Kunstwerk"),
("audioRecording", "Tonaufnahme"),
("bill", "Gesetzentwurf"),
("blogPost", "Blogbeitrag"),
("book", "Buch"),
("bookSection", "Buchkapitel"),
("case", "Gerichtsentscheidung"),
("computerProgram", "Software"),
("conferencePaper", "Konferenzbeitrag"),
("dataset", "Datensatz"),
("dictionaryEntry", "Wörterbucheintrag"),
("document", "Dokument"),
("email", "E-Mail"),
("encyclopediaArticle", "Lexikonartikel"),
("film", "Film"),
("forumPost", "Forumsbeitrag"),
("hearing", "Anhörung"),
("instantMessage", "Sofortnachricht"),
("interview", "Interview"),
("journalArticle", "Zeitschriftenartikel"),
("letter", "Brief"),
("magazineArticle", "Magazinartikel"),
("manuscript", "Manuskript"),
("map", "Karte"),
("newspaperArticle", "Zeitungsartikel"),
("patent", "Patent"),
("podcast", "Podcast"),
("preprint", "Preprint"),
("presentation", "Präsentation"),
("radioBroadcast", "Radiosendung"),
("report", "Bericht"),
("standard", "Norm"),
("statute", "Gesetz"),
("thesis", "Abschlussarbeit"),
("tvBroadcast", "Fernsehsendung"),
("videoRecording", "Videoaufnahme"),
("webpage", "Webseite"),
];
const LANGUAGES: &[(&str, &str, &str)] = &[
("ar", "Arabic", "Arabisch"),
("cs", "Czech", "Tschechisch"),
("da", "Danish", "Dänisch"),
("de", "German", "Deutsch"),
("el", "Greek", "Griechisch"),
("en", "English", "Englisch"),
("es", "Spanish", "Spanisch"),
("fi", "Finnish", "Finnisch"),
("fr", "French", "Französisch"),
("he", "Hebrew", "Hebräisch"),
("hu", "Hungarian", "Ungarisch"),
("it", "Italian", "Italienisch"),
("ja", "Japanese", "Japanisch"),
("ko", "Korean", "Koreanisch"),
("nb", "Norwegian", "Norwegisch"),
("nl", "Dutch", "Niederländisch"),
("no", "Norwegian", "Norwegisch"),
("pl", "Polish", "Polnisch"),
("pt", "Portuguese", "Portugiesisch"),
("ru", "Russian", "Russisch"),
("sv", "Swedish", "Schwedisch"),
("tr", "Turkish", "Türkisch"),
("zh", "Chinese", "Chinesisch"),
];
impl Lang {
pub const ALL: [Lang; 2] = [Lang::En, Lang::De];
pub fn parse(code: &str) -> Option<Lang> {
match code.to_ascii_lowercase().as_str() {
"en" => Some(Lang::En),
"de" => Some(Lang::De),
_ => None,
}
}
pub fn from_accept_language(header: &str) -> Lang {
header
.split(',')
.filter_map(|tag| {
let tag = tag.split(';').next()?.trim();
Lang::parse(tag.split('-').next()?)
})
.next()
.unwrap_or(Lang::En)
}
pub fn code(&self) -> &'static str {
match self {
Lang::En => "en",
Lang::De => "de",
}
}
pub fn name(&self) -> &'static str {
match self {
Lang::En => "English",
Lang::De => "Deutsch",
}
}
pub fn citation_locale(self) -> &'static str {
match self {
Lang::En => "en-US",
Lang::De => "de-DE",
}
}
pub fn text(self) -> &'static Text {
match self {
Lang::En => &EN,
Lang::De => &DE,
}
}
pub fn layout(self) -> &'static LayoutText {
match self {
Lang::En => &LAYOUT_EN,
Lang::De => &LAYOUT_DE,
}
}
pub fn count(&self, n: usize) -> String {
let separator = match self {
Lang::En => ',',
Lang::De => '.',
};
let digits = n.to_string();
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
for (i, digit) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
out.push(separator);
}
out.push(digit);
}
out
}
pub fn works(self, n: usize) -> String {
let noun = match (self, n) {
(Lang::En, 1) => "work",
(Lang::En, _) => "works",
(_, 1) => "Werk",
(_, _) => "Werke",
};
format!("{} {noun}", self.count(n))
}
pub fn page_of(self, page: usize, pages: usize) -> String {
let (page, pages) = (self.count(page), self.count(pages));
match self {
Lang::En => format!("page {page} of {pages}"),
Lang::De => format!("Seite {page} von {pages}"),
}
}
pub fn date(self, date: &str) -> String {
let parts: Vec<&str> = date.trim().split('-').collect();
let months = match self {
Lang::En => &MONTHS_EN,
Lang::De => &MONTHS_DE,
};
let month = parts
.get(1)
.and_then(|m| m.parse::<usize>().ok())
.and_then(|m| months.get(m.wrapping_sub(1)));
let day = parts.get(2).and_then(|d| d.parse::<u32>().ok());
match (self, parts[0], month, day) {
(Lang::En, year, Some(month), Some(day)) => format!("{month} {day}, {year}"),
(_, year, Some(month), Some(day)) => format!("{day}. {month} {year}"),
(_, year, Some(month), None) => format!("{month} {year}"),
(_, year, _, _) => year.to_string(),
}
}
pub fn date_range(self, start: &str, end: &str) -> String {
let text = self.text();
match (start.trim().is_empty(), end.trim().is_empty()) {
(false, false) => format!("{} – {}", self.date(start), self.date(end)),
(false, true) => format!("{} {}", text.started, self.date(start)),
(true, false) => format!("{} {}", text.ended, self.date(end)),
(true, true) => String::new(),
}
}
pub fn item_type(self, item_type: &str) -> String {
if self != Lang::En
&& let Some((_, label)) = ITEM_TYPES_DE.iter().find(|(t, _)| *t == item_type)
{
return label.to_string();
}
let mut label = String::new();
for (i, c) in item_type.chars().enumerate() {
if i == 0 {
label.extend(c.to_uppercase());
} else if c.is_uppercase() {
label.push(' ');
label.extend(c.to_lowercase());
} else {
label.push(c);
}
}
label
}
pub fn language(self, language: &str) -> String {
let language = language.trim();
let primary = language
.split('-')
.next()
.unwrap_or("")
.to_ascii_lowercase();
match LANGUAGES.iter().find(|(code, _, _)| *code == primary) {
Some((_, en, _)) if self == Lang::En => en.to_string(),
Some((_, _, de)) => de.to_string(),
None => language.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn picks_a_language_from_the_browser() {
let accept = Lang::from_accept_language;
assert_eq!(accept("de-DE,de;q=0.9,en;q=0.8"), Lang::De);
assert_eq!(accept("de-AT,de;q=0.9"), Lang::De);
assert_eq!(accept("fr-FR,fr;q=0.9,de;q=0.8,en;q=0.7"), Lang::De);
assert_eq!(accept("en-GB,de;q=0.5"), Lang::En);
assert_eq!(accept("fr"), Lang::En);
assert_eq!(accept(""), Lang::En);
}
#[test]
fn formats_dates_and_ranges() {
assert_eq!(Lang::En.date("2015"), "2015");
assert_eq!(Lang::En.date("2015-08"), "August 2015");
assert_eq!(Lang::En.date("2021-08-01"), "August 1, 2021");
assert_eq!(Lang::De.date("2021-08-01"), "1. August 2021");
assert_eq!(Lang::De.date("2015-03"), "März 2015");
assert_eq!(Lang::De.date("2021-01-05"), "5. Januar 2021");
assert_eq!(Lang::En.date("May 2021"), "May 2021");
assert_eq!(
Lang::En.date_range("2021-08-01", ""),
"Started August 1, 2021"
);
assert_eq!(
Lang::De.date_range("2021-08-01", ""),
"Beginn 1. August 2021"
);
assert_eq!(
Lang::En.date_range("2015-08", "2021-07-31"),
"August 2015 – July 31, 2021"
);
assert_eq!(Lang::En.date_range("", ""), "");
}
#[test]
fn counts_in_the_page_language() {
assert_eq!(Lang::En.count(1043), "1,043");
assert_eq!(Lang::De.count(1043), "1.043");
assert_eq!(Lang::En.count(999), "999");
assert_eq!(Lang::En.works(1), "1 work");
assert_eq!(Lang::De.works(1043), "1.043 Werke");
assert_eq!(Lang::De.page_of(1, 105), "Seite 1 von 105");
}
#[test]
fn names_types_and_languages() {
assert_eq!(Lang::En.item_type("blogPost"), "Blog post");
assert_eq!(Lang::De.item_type("blogPost"), "Blogbeitrag");
assert_eq!(Lang::De.item_type("unknownType"), "Unknown type");
assert_eq!(Lang::En.language("en-US"), "English");
assert_eq!(Lang::De.language("en"), "Englisch");
assert_eq!(Lang::En.language("Klingon"), "Klingon");
}
}