1pub const ALLOWED_LOCALES: &[&str] = &[
13 "af-ZA", "am-ET", "ar-AE", "ar-EG", "ar-SA", "az-AZ", "be-BY", "bg-BG", "bn-BD", "bn-IN",
14 "bs-BA", "ca-ES", "cs-CZ", "cy-GB", "da-DK", "de-AT", "de-CH", "de-DE", "el-GR", "en", "en-AU",
15 "en-CA", "en-GB", "en-IE", "en-IN", "en-NZ", "en-US", "en-ZA", "es-AR", "es-CL", "es-CO",
16 "es-ES", "es-MX", "es-PE", "et-EE", "eu-ES", "fa-IR", "fi-FI", "fil-PH", "fr-BE", "fr-CA",
17 "fr-CH", "fr-FR", "ga-IE", "gl-ES", "gu-IN", "he-IL", "hi-IN", "hr-HR", "hu-HU", "hy-AM",
18 "id-ID", "is-IS", "it-CH", "it-IT", "ja-JP", "ka-GE", "kk-KZ", "km-KH", "kn-IN", "ko-KR",
19 "lo-LA", "lt-LT", "lv-LV", "mk-MK", "ml-IN", "mn-MN", "mr-IN", "ms-MY", "mt-MT", "my-MM",
20 "nb-NO", "ne-NP", "nl-BE", "nl-NL", "pa-IN", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "si-LK",
21 "sk-SK", "sl-SI", "sq-AL", "sr-RS", "sv-SE", "sw-KE", "ta-IN", "te-IN", "th-TH", "tr-TR", "uk",
22 "uk-UA", "ur-PK", "uz-UZ", "vi-VN", "zh-CN", "zh-HK", "zh-TW", "zu-ZA",
23];
24
25pub const ALLOWED_TIMEZONES: &[&str] = &[
28 "UTC",
29 "Africa/Abidjan",
30 "Africa/Accra",
31 "Africa/Addis_Ababa",
32 "Africa/Algiers",
33 "Africa/Cairo",
34 "Africa/Casablanca",
35 "Africa/Dar_es_Salaam",
36 "Africa/Johannesburg",
37 "Africa/Lagos",
38 "Africa/Nairobi",
39 "Africa/Tunis",
40 "America/Anchorage",
41 "America/Argentina/Buenos_Aires",
42 "America/Bogota",
43 "America/Cancun",
44 "America/Chicago",
45 "America/Denver",
46 "America/Edmonton",
47 "America/Halifax",
48 "America/Havana",
49 "America/Lima",
50 "America/Los_Angeles",
51 "America/Manaus",
52 "America/Mexico_City",
53 "America/New_York",
54 "America/Panama",
55 "America/Phoenix",
56 "America/Santiago",
57 "America/Sao_Paulo",
58 "America/St_Johns",
59 "America/Toronto",
60 "America/Vancouver",
61 "America/Winnipeg",
62 "Asia/Almaty",
63 "Asia/Amman",
64 "Asia/Baghdad",
65 "Asia/Baku",
66 "Asia/Bangkok",
67 "Asia/Beirut",
68 "Asia/Colombo",
69 "Asia/Dhaka",
70 "Asia/Dubai",
71 "Asia/Ho_Chi_Minh",
72 "Asia/Hong_Kong",
73 "Asia/Istanbul",
74 "Asia/Jakarta",
75 "Asia/Jerusalem",
76 "Asia/Kabul",
77 "Asia/Karachi",
78 "Asia/Kathmandu",
79 "Asia/Kolkata",
80 "Asia/Kuala_Lumpur",
81 "Asia/Manila",
82 "Asia/Riyadh",
83 "Asia/Seoul",
84 "Asia/Shanghai",
85 "Asia/Singapore",
86 "Asia/Taipei",
87 "Asia/Tashkent",
88 "Asia/Tbilisi",
89 "Asia/Tehran",
90 "Asia/Tokyo",
91 "Asia/Ulaanbaatar",
92 "Asia/Yangon",
93 "Asia/Yerevan",
94 "Atlantic/Azores",
95 "Atlantic/Reykjavik",
96 "Australia/Adelaide",
97 "Australia/Brisbane",
98 "Australia/Darwin",
99 "Australia/Hobart",
100 "Australia/Melbourne",
101 "Australia/Perth",
102 "Australia/Sydney",
103 "Europe/Amsterdam",
104 "Europe/Athens",
105 "Europe/Belgrade",
106 "Europe/Berlin",
107 "Europe/Brussels",
108 "Europe/Bucharest",
109 "Europe/Budapest",
110 "Europe/Copenhagen",
111 "Europe/Dublin",
112 "Europe/Helsinki",
113 "Europe/Istanbul",
114 "Europe/Kyiv",
115 "Europe/Lisbon",
116 "Europe/London",
117 "Europe/Luxembourg",
118 "Europe/Madrid",
119 "Europe/Oslo",
120 "Europe/Paris",
121 "Europe/Prague",
122 "Europe/Riga",
123 "Europe/Rome",
124 "Europe/Sofia",
125 "Europe/Stockholm",
126 "Europe/Tallinn",
127 "Europe/Vienna",
128 "Europe/Vilnius",
129 "Europe/Warsaw",
130 "Europe/Zurich",
131 "Indian/Maldives",
132 "Indian/Mauritius",
133 "Pacific/Auckland",
134 "Pacific/Fiji",
135 "Pacific/Guam",
136 "Pacific/Honolulu",
137];
138
139pub fn is_allowed_locale(locale: &str) -> bool {
141 ALLOWED_LOCALES.contains(&locale)
142}
143
144pub fn is_allowed_timezone(tz: &str) -> bool {
146 ALLOWED_TIMEZONES.contains(&tz)
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum BackendLocale {
151 En,
152 Uk,
153}
154
155pub fn resolve_backend_locale(locale: Option<&str>) -> BackendLocale {
156 match locale
157 .map(str::trim)
158 .filter(|value| !value.is_empty())
159 .map(|value| value.to_ascii_lowercase())
160 {
161 Some(value) if value == "uk" || value.starts_with("uk-") => BackendLocale::Uk,
162 _ => BackendLocale::En,
163 }
164}
165
166pub struct BackendStrings {
167 pub working: &'static str,
168 pub current_directory: &'static str,
169 pub and_more_actions: &'static str,
170 pub with_errors_one: &'static str,
171 pub with_errors_many: &'static str,
172 pub completed_tool_batch_one: &'static str,
173 pub completed_tool_batch_many: &'static str,
174}
175
176const EN_STRINGS: BackendStrings = BackendStrings {
177 working: "Working",
178 current_directory: "current directory",
179 and_more_actions: "and {count} more actions",
180 with_errors_one: " with 1 error",
181 with_errors_many: " with {count} errors",
182 completed_tool_batch_one: "Completed tool batch with 1 error",
183 completed_tool_batch_many: "Completed tool batch with {count} errors",
184};
185
186const UK_STRINGS: BackendStrings = BackendStrings {
187 working: "Працюю",
188 current_directory: "поточній директорії",
189 and_more_actions: "і ще {count} дій",
190 with_errors_one: " з 1 помилкою",
191 with_errors_many: " з {count} помилками",
192 completed_tool_batch_one: "Пакет інструментів завершено з 1 помилкою",
193 completed_tool_batch_many: "Пакет інструментів завершено з {count} помилками",
194};
195
196pub fn backend_strings(locale: Option<&str>) -> &'static BackendStrings {
197 match resolve_backend_locale(locale) {
198 BackendLocale::En => &EN_STRINGS,
199 BackendLocale::Uk => &UK_STRINGS,
200 }
201}
202
203pub fn format_error_suffix(locale: Option<&str>, error_count: u32) -> String {
204 let strings = backend_strings(locale);
205 if error_count == 1 {
206 strings.with_errors_one.to_string()
207 } else {
208 strings
209 .with_errors_many
210 .replace("{count}", &error_count.to_string())
211 }
212}
213
214pub fn format_completed_tool_batch(locale: Option<&str>, error_count: u32) -> String {
215 let strings = backend_strings(locale);
216 if error_count == 1 {
217 strings.completed_tool_batch_one.to_string()
218 } else {
219 strings
220 .completed_tool_batch_many
221 .replace("{count}", &error_count.to_string())
222 }
223}
224
225pub fn format_more_actions(locale: Option<&str>, count: usize) -> String {
226 match resolve_backend_locale(locale) {
227 BackendLocale::En => backend_strings(locale)
228 .and_more_actions
229 .replace("{count}", &count.to_string()),
230 BackendLocale::Uk => {
231 let template = match count {
232 1 => "і ще 1 дію",
233 2..=4 => "і ще {count} дії",
234 _ => "і ще {count} дій",
235 };
236 template.replace("{count}", &count.to_string())
237 }
238 }
239}
240
241pub fn localized_tool_display_name(
242 tool_name: &str,
243 fallback_display_name: Option<&str>,
244 locale: Option<&str>,
245) -> Option<String> {
246 if resolve_backend_locale(locale) != BackendLocale::Uk {
247 return fallback_display_name.map(ToOwned::to_owned);
248 }
249
250 let localized = match tool_name {
251 "bash" => Some("Командний рядок"),
252 "read_file" | "session_read_file" => Some("Читання файла"),
253 "read_many_files" => Some("Читання файлів"),
254 "list_directory" | "list_files" => Some("Список файлів"),
255 "grep_files" => Some("Пошук у файлах"),
256 "search" | "search_web" => Some("Пошук у вебі"),
257 name if name.ends_with("__search") => Some("Пошук"),
258 "write_file" => Some("Запис файла"),
259 "edit_file" | "replace_in_file" => Some("Редагування файла"),
260 "append_file" => Some("Дописування у файл"),
261 "move_file" => Some("Переміщення файла"),
262 "delete_file" => Some("Видалення файла"),
263 "mkdir" => Some("Створення директорії"),
264 "stat_file" => Some("Інформація про файл"),
265 "secret_store" => Some("Сховище секретів"),
266 "kv_store" => Some("Сховище значень"),
267 "spawn_agent" => Some("Запуск агента"),
268 "write_todos" => Some("Список задач"),
269 "setup_connection" => Some("Налаштування підключення"),
270 _ => None,
271 };
272
273 localized
274 .map(str::to_string)
275 .or_else(|| fallback_display_name.map(ToOwned::to_owned))
276}