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::Ru`], which
27/// preserves the original Russian wording of the report.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Language {
30 /// Russian — the historical default output.
31 Ru,
32 /// English.
33 En,
34}
35
36impl Language {
37 /// Parses a language code (case-insensitive) into a [`Language`].
38 ///
39 /// Recognizes `ru` and `en`. Any other value falls back to
40 /// [`Language::Ru`] so that a typo never breaks report generation.
41 pub fn from_code(code: &str) -> Language {
42 match code.trim().to_ascii_lowercase().as_str() {
43 "en" | "eng" | "english" => Language::En,
44 _ => Language::Ru,
45 }
46 }
47}
48
49/// A complete set of localized strings for one language.
50///
51/// All fields are `&'static str` because the tables are compiled into the
52/// binary. The `work_on_task` field contains a `{task}` placeholder that is
53/// replaced with the actual task name at render time.
54pub struct Locale {
55 /// Report title shown in the merged header cell (e.g. "Отчет за день").
56 pub report_title: &'static str,
57 /// Label for a regular working day (e.g. "рабочий").
58 pub day_type_working: &'static str,
59 /// Caption for the workday-length header cell.
60 pub workday_length: &'static str,
61 /// Column header spanning the start/end time columns (e.g. "День").
62 pub header_day: &'static str,
63 /// Header for the interval start-time column.
64 pub header_start: &'static str,
65 /// Header for the interval end-time column.
66 pub header_end: &'static str,
67 /// Header for the optional "hours" column.
68 pub header_hours: &'static str,
69 /// Header for the optional "result" column.
70 pub header_result: &'static str,
71 /// Label for the total worked-hours footer row.
72 pub total_worked: &'static str,
73 /// Label preceding the free-form comment box.
74 pub comment: &'static str,
75 /// Work description template; `{task}` is replaced with the task name.
76 pub work_on_task: &'static str,
77 /// Generic work label used when a task has no name.
78 pub work_generic: &'static str,
79 /// Label written for hours (or parts of hours) spent on a break/pause.
80 pub break_label: &'static str,
81 /// Nominative month names, indexed 0 (January) through 11 (December).
82 pub months: [&'static str; 12],
83 /// Weekday names, indexed 0 (Monday) through 6 (Sunday).
84 pub weekdays: [&'static str; 7],
85 /// `chrono` date format string used for the date header cell.
86 pub date_format: &'static str,
87}
88
89impl Locale {
90 /// Returns the static [`Locale`] table for the given language.
91 pub fn for_language(language: Language) -> &'static Locale {
92 match language {
93 Language::Ru => &RU,
94 Language::En => &EN,
95 }
96 }
97
98 /// Builds a work description for a task name using the `work_on_task`
99 /// template, falling back to [`Locale::work_generic`] for empty names.
100 pub fn work_text(&self, task_name: &str) -> String {
101 if task_name.trim().is_empty() {
102 self.work_generic.to_string()
103 } else {
104 self.work_on_task.replace("{task}", task_name)
105 }
106 }
107}
108
109/// Russian locale table (default).
110static RU: Locale = Locale {
111 report_title: "Отчет за день",
112 day_type_working: "рабочий",
113 workday_length: "Продолжительность рабочего дня",
114 header_day: "День",
115 header_start: "Начало",
116 header_end: "Конец",
117 header_hours: "Часы",
118 header_result: "Результат",
119 total_worked: "Отработано часов:",
120 comment: "Комментарий:",
121 work_on_task: "Работа по задаче [{task}]",
122 work_generic: "Работа",
123 break_label: "Перерыв",
124 months: [
125 "Январь",
126 "Февраль",
127 "Март",
128 "Апрель",
129 "Май",
130 "Июнь",
131 "Июль",
132 "Август",
133 "Сентябрь",
134 "Октябрь",
135 "Ноябрь",
136 "Декабрь",
137 ],
138 weekdays: ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"],
139 date_format: "%d.%m.%Y",
140};
141
142/// English locale table.
143static EN: Locale = Locale {
144 report_title: "Daily report",
145 day_type_working: "working",
146 workday_length: "Workday length",
147 header_day: "Day",
148 header_start: "Start",
149 header_end: "End",
150 header_hours: "Hours",
151 header_result: "Result",
152 total_worked: "Hours worked:",
153 comment: "Comment:",
154 work_on_task: "Work on task [{task}]",
155 work_generic: "Work",
156 break_label: "Break",
157 months: [
158 "January",
159 "February",
160 "March",
161 "April",
162 "May",
163 "June",
164 "July",
165 "August",
166 "September",
167 "October",
168 "November",
169 "December",
170 ],
171 weekdays: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
172 date_format: "%Y-%m-%d",
173};