kasl/libs/locale.rs
1//! Built-in localization for hourly (SiServer-style) daily reports.
2//!
3//! This module centralizes every user-facing string, month name, weekday name
4//! and date format used when rendering the hourly report. Previously all of
5//! these were hardcoded in Russian inside [`crate::libs::export`]; they now live
6//! in language-specific [`Locale`] tables so the report can be produced in
7//! different languages selected via `config.report.language`.
8//!
9//! ## Scope
10//!
11//! Only the hourly report (`kasl export report --hourly`) consumes these
12//! locales. Other exports (CSV/JSON/plain Excel) keep their existing English
13//! wording and are intentionally not affected.
14//!
15//! ## Usage
16//!
17//! ```rust,no_run
18//! use kasl::libs::locale::{Language, Locale};
19//!
20//! let locale = Locale::for_language(Language::from_code("en"));
21//! assert_eq!(locale.months[0], "January");
22//! ```
23
24/// Supported report languages.
25///
26/// The default (and fallback for unknown codes) is [`Language::En`]. Russian
27/// was the original default, but the shipped product speaks English; set
28/// `report.language = "ru"` in the config to get the previous wording back.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Language {
31 /// Russian - opt in through the report configuration.
32 Ru,
33 /// English - the default output.
34 En,
35}
36
37impl Language {
38 /// Parses a language code (case-insensitive) into a [`Language`].
39 ///
40 /// Recognizes `ru` and `en`. Any other value falls back to
41 /// [`Language::En`] so that a typo never breaks report generation.
42 pub fn from_code(code: &str) -> Language {
43 match code.trim().to_ascii_lowercase().as_str() {
44 "ru" | "rus" | "russian" => Language::Ru,
45 _ => Language::En,
46 }
47 }
48}
49
50/// A complete set of localized strings for one language.
51///
52/// All fields are `&'static str` because the tables are compiled into the
53/// binary. The `work_on_task` field contains a `{task}` placeholder that is
54/// replaced with the actual task name at render time.
55pub struct Locale {
56 /// Report title shown in the merged header cell (e.g. "Отчет за день").
57 pub report_title: &'static str,
58 /// Label for a regular working day (e.g. "рабочий").
59 pub day_type_working: &'static str,
60 /// Caption for the workday-length header cell.
61 pub workday_length: &'static str,
62 /// Column header spanning the start/end time columns (e.g. "День").
63 pub header_day: &'static str,
64 /// Header for the interval start-time column.
65 pub header_start: &'static str,
66 /// Header for the interval end-time column.
67 pub header_end: &'static str,
68 /// Header for the optional "hours" column.
69 pub header_hours: &'static str,
70 /// Header for the optional "result" column.
71 pub header_result: &'static str,
72 /// Label for the total worked-hours footer row.
73 pub total_worked: &'static str,
74 /// Label preceding the free-form comment box.
75 pub comment: &'static str,
76 /// Work description template; `{task}` is replaced with the task name.
77 pub work_on_task: &'static str,
78 /// Generic work label used when a task has no name.
79 pub work_generic: &'static str,
80 /// Label written for hours (or parts of hours) spent on a break/pause.
81 pub break_label: &'static str,
82 /// Nominative month names, indexed 0 (January) through 11 (December).
83 pub months: [&'static str; 12],
84 /// Weekday names, indexed 0 (Monday) through 6 (Sunday).
85 pub weekdays: [&'static str; 7],
86 /// `chrono` date format string used for the date header cell.
87 pub date_format: &'static str,
88}
89
90impl Locale {
91 /// Returns the static [`Locale`] table for the given language.
92 pub fn for_language(language: Language) -> &'static Locale {
93 match language {
94 Language::Ru => &RU,
95 Language::En => &EN,
96 }
97 }
98
99 /// Builds a work description for a task name using the `work_on_task`
100 /// template, falling back to [`Locale::work_generic`] for empty names.
101 pub fn work_text(&self, task_name: &str) -> String {
102 if task_name.trim().is_empty() {
103 self.work_generic.to_string()
104 } else {
105 self.work_on_task.replace("{task}", task_name)
106 }
107 }
108}
109
110/// Russian locale table.
111static RU: Locale = Locale {
112 report_title: "Отчет за день",
113 day_type_working: "рабочий",
114 workday_length: "Продолжительность рабочего дня",
115 header_day: "День",
116 header_start: "Начало",
117 header_end: "Конец",
118 header_hours: "Часы",
119 header_result: "Результат",
120 total_worked: "Отработано часов:",
121 comment: "Комментарий:",
122 work_on_task: "Работа по задаче [{task}]",
123 work_generic: "Работа",
124 break_label: "Перерыв",
125 months: [
126 "Январь",
127 "Февраль",
128 "Март",
129 "Апрель",
130 "Май",
131 "Июнь",
132 "Июль",
133 "Август",
134 "Сентябрь",
135 "Октябрь",
136 "Ноябрь",
137 "Декабрь",
138 ],
139 weekdays: ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"],
140 date_format: "%d.%m.%Y",
141};
142
143/// English locale table.
144static EN: Locale = Locale {
145 report_title: "Daily report",
146 day_type_working: "working",
147 workday_length: "Workday length",
148 header_day: "Day",
149 header_start: "Start",
150 header_end: "End",
151 header_hours: "Hours",
152 header_result: "Result",
153 total_worked: "Hours worked:",
154 comment: "Comment:",
155 work_on_task: "Work on task [{task}]",
156 work_generic: "Work",
157 break_label: "Break",
158 months: [
159 "January",
160 "February",
161 "March",
162 "April",
163 "May",
164 "June",
165 "July",
166 "August",
167 "September",
168 "October",
169 "November",
170 "December",
171 ],
172 weekdays: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
173 date_format: "%Y-%m-%d",
174};