Skip to main content

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};