1pub mod en;
22pub mod ru;
23pub mod zh;
24
25use std::sync::atomic::{AtomicU8, Ordering};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Lang {
32 En = 0,
33 Ru = 1,
34 Zh = 2,
35}
36
37impl Lang {
38 pub fn next(self) -> Self {
40 match self {
41 Self::En => Self::Ru,
42 Self::Ru => Self::Zh,
43 Self::Zh => Self::En,
44 }
45 }
46
47 pub fn label(self) -> &'static str {
49 match self {
50 Self::En => "EN",
51 Self::Ru => "RU",
52 Self::Zh => "ZH",
53 }
54 }
55
56 fn from_u8(v: u8) -> Self {
57 match v {
58 1 => Self::Ru,
59 2 => Self::Zh,
60 _ => Self::En,
61 }
62 }
63}
64
65static LANG: AtomicU8 = AtomicU8::new(0); pub fn set_lang(lang: Lang) {
69 LANG.store(lang as u8, Ordering::Relaxed);
70}
71
72pub fn lang() -> Lang {
74 Lang::from_u8(LANG.load(Ordering::Relaxed))
75}
76
77pub fn strings() -> &'static Strings {
80 match lang() {
81 Lang::En => &en::STRINGS,
82 Lang::Ru => &ru::STRINGS,
83 Lang::Zh => &zh::STRINGS,
84 }
85}
86
87pub fn detect_locale() -> Lang {
89 if let Ok(val) = std::env::var("PRT_LANG") {
90 return parse_lang(&val);
91 }
92
93 if let Some(locale) = sys_locale::get_locale() {
94 let lower = locale.to_lowercase();
95 if lower.starts_with("ru") {
96 return Lang::Ru;
97 }
98 if lower.starts_with("zh") {
99 return Lang::Zh;
100 }
101 }
102
103 Lang::En
104}
105
106pub fn parse_lang(s: &str) -> Lang {
109 match s.to_lowercase().as_str() {
110 "ru" | "russian" => Lang::Ru,
111 "zh" | "cn" | "chinese" => Lang::Zh,
112 _ => Lang::En,
113 }
114}
115
116pub struct Strings {
122 pub app_name: &'static str,
123
124 pub connections: &'static str,
126 pub no_root_warning: &'static str,
127 pub sudo_ok: &'static str,
128 pub filter_label: &'static str,
129 pub search_mode: &'static str,
130
131 pub detail_panel_title: &'static str,
133 pub detail_panel_tree_header: &'static str,
134 pub no_selected_process: &'static str,
135
136 pub section_connections: &'static str,
138 pub section_processes: &'static str,
139 pub section_ssh: &'static str,
140
141 pub view_topology: &'static str,
143 pub view_process: &'static str,
144
145 pub process_not_found: &'static str,
147
148 pub iface_address: &'static str,
150 pub iface_interface: &'static str,
151 pub iface_protocol: &'static str,
152 pub iface_bind: &'static str,
153 pub iface_localhost_only: &'static str,
154 pub iface_all_interfaces: &'static str,
155 pub iface_specific: &'static str,
156 pub iface_loopback: &'static str,
157 pub iface_all: &'static str,
158
159 pub conn_local: &'static str,
161 pub conn_remote: &'static str,
162 pub conn_state: &'static str,
163 pub conn_process: &'static str,
164 pub conn_cmdline: &'static str,
165
166 pub help_text: &'static str,
168 pub kill_cancel: &'static str,
169 pub copied: &'static str,
170 pub refreshed: &'static str,
171 pub clipboard_unavailable: &'static str,
172 pub scan_error: &'static str,
173 pub cancelled: &'static str,
174 pub lang_switched: &'static str,
175 pub paused: &'static str,
176 pub resumed: &'static str,
177 pub no_connections: &'static str,
178 pub no_filter_matches: &'static str,
179 pub more: &'static str,
180 pub col_age: &'static str,
181 pub col_remote: &'static str,
182
183 pub sudo_prompt_title: &'static str,
185 pub sudo_password_label: &'static str,
186 pub sudo_confirm_hint: &'static str,
187 pub sudo_failed: &'static str,
188 pub sudo_wrong_password: &'static str,
189 pub sudo_elevated: &'static str,
190
191 pub hint_help: &'static str,
193 pub hint_search: &'static str,
194 pub hint_kill: &'static str,
195 pub hint_sudo: &'static str,
196 pub hint_quit: &'static str,
197 pub hint_lang: &'static str,
198
199 pub hint_back: &'static str,
201 pub hint_details: &'static str,
202 pub hint_sort: &'static str,
203 pub hint_copy: &'static str,
204 pub hint_navigate: &'static str,
205 pub hint_section_next: &'static str,
206 pub hint_subtab: &'static str,
207 pub hint_action_menu: &'static str,
208 pub hint_edit_tunnel: &'static str,
209 pub hint_pause: &'static str,
210 pub hint_resume: &'static str,
211
212 pub action_menu_title: &'static str,
214 pub action_kill: &'static str,
215 pub action_copy: &'static str,
216 pub action_copy_pid: &'static str,
217 pub action_block: &'static str,
218 pub action_trace: &'static str,
219 pub action_forward: &'static str,
220 pub action_unavailable_no_remote: &'static str,
221 pub command_palette_title: &'static str,
222 pub command_palette_empty: &'static str,
223
224 pub esc_again_to_clear_filter: &'static str,
226 pub esc_again_to_discard_form: &'static str,
227
228 pub forward_prompt_title: &'static str,
230 pub forward_host_label: &'static str,
231 pub forward_confirm_hint: &'static str,
232
233 pub view_ssh_hosts: &'static str,
235 pub view_tunnels: &'static str,
236
237 pub ssh_col_alias: &'static str,
239 pub ssh_col_target: &'static str,
240 pub ssh_col_source: &'static str,
241 pub ssh_hosts_empty: &'static str,
242 pub ssh_hosts_reloaded: &'static str,
243
244 pub tunnel_col_name: &'static str,
246 pub tunnel_col_kind: &'static str,
247 pub tunnel_col_local: &'static str,
248 pub tunnel_col_remote: &'static str,
249 pub tunnel_col_host: &'static str,
250 pub tunnel_col_status: &'static str,
251 pub tunnel_status_alive: &'static str,
252 pub tunnel_status_dead: &'static str,
253 pub tunnel_status_starting: &'static str,
254 pub tunnel_status_failed: &'static str,
255 pub tunnel_form_edit_title: &'static str,
256 pub tunnel_form_field_required: &'static str,
257 pub tunnels_empty: &'static str,
258 pub tunnels_saved: &'static str,
259 pub tunnel_killed: &'static str,
260 pub tunnel_restarted: &'static str,
261 pub tunnel_create_failed: &'static str,
262 pub tunnel_kind_local: &'static str,
263 pub tunnel_kind_dynamic: &'static str,
264
265 pub tunnel_form_title: &'static str,
267 pub tunnel_form_kind: &'static str,
268 pub tunnel_form_local_port: &'static str,
269 pub tunnel_form_remote_host: &'static str,
270 pub tunnel_form_remote_port: &'static str,
271 pub tunnel_form_host_alias: &'static str,
272 pub tunnel_form_hint: &'static str,
273 pub tunnel_form_invalid: &'static str,
274
275 pub hint_new_tunnel: &'static str,
277 pub hint_kill_tunnel: &'static str,
278 pub hint_restart_tunnel: &'static str,
279 pub hint_save_tunnels: &'static str,
280 pub hint_reload: &'static str,
281 pub hint_open_tunnel: &'static str,
282
283 pub help_title: &'static str,
285}
286
287impl Strings {
288 pub fn fmt_connections(&self, n: usize) -> String {
289 format!("{n} {}", self.connections)
290 }
291
292 pub fn fmt_kill_confirm(&self, name: &str, pid: u32) -> String {
293 match lang() {
294 Lang::En => format!("Kill {name} (pid {pid})?"),
295 Lang::Ru => format!("Завершить {name} (pid {pid})?"),
296 Lang::Zh => format!("终止 {name} (pid {pid})?"),
297 }
298 }
299
300 pub fn fmt_kill_sent(&self, sig: &str, name: &str, pid: u32) -> String {
301 match lang() {
302 Lang::En => format!("sent {sig} to {name} (pid {pid})"),
303 Lang::Ru => format!("отправлен {sig} → {name} (pid {pid})"),
304 Lang::Zh => format!("已发送 {sig} → {name} (pid {pid})"),
305 }
306 }
307
308 pub fn fmt_kill_failed(&self, err: &str) -> String {
309 match lang() {
310 Lang::En => format!("kill failed: {err}"),
311 Lang::Ru => format!("ошибка завершения: {err}"),
312 Lang::Zh => format!("终止失败: {err}"),
313 }
314 }
315
316 pub fn fmt_scan_error(&self, err: &str) -> String {
317 format!("{}: {err}", self.scan_error)
318 }
319
320 pub fn fmt_all_ports(&self, n: usize) -> String {
321 match lang() {
322 Lang::En => format!("--- All ports of process ({n}) ---"),
323 Lang::Ru => format!("--- Все порты процесса ({n}) ---"),
324 Lang::Zh => format!("--- 进程所有端口 ({n}) ---"),
325 }
326 }
327
328 pub fn fmt_sudo_error(&self, err: &str) -> String {
329 match lang() {
330 Lang::En => format!("sudo: {err}"),
331 Lang::Ru => format!("sudo ошибка: {err}"),
332 Lang::Zh => format!("sudo 错误: {err}"),
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn lang_next_cycles_all() {
343 let cases = [
344 (Lang::En, Lang::Ru),
345 (Lang::Ru, Lang::Zh),
346 (Lang::Zh, Lang::En),
347 ];
348 for (from, expected) in cases {
349 assert_eq!(from.next(), expected, "{:?}.next()", from);
350 }
351 }
352
353 #[test]
354 fn lang_next_full_cycle() {
355 let start = Lang::En;
356 let after_3 = start.next().next().next();
357 assert_eq!(after_3, start);
358 }
359
360 #[test]
361 fn lang_label() {
362 let cases = [(Lang::En, "EN"), (Lang::Ru, "RU"), (Lang::Zh, "ZH")];
363 for (lang, expected) in cases {
364 assert_eq!(lang.label(), expected);
365 }
366 }
367
368 #[test]
369 fn lang_from_u8_table() {
370 let cases = [
371 (0, Lang::En),
372 (1, Lang::Ru),
373 (2, Lang::Zh),
374 (99, Lang::En),
375 (255, Lang::En),
376 ];
377 for (val, expected) in cases {
378 assert_eq!(Lang::from_u8(val), expected, "from_u8({val})");
379 }
380 }
381
382 #[test]
383 fn parse_lang_table() {
384 let cases = [
385 ("en", Lang::En),
386 ("ru", Lang::Ru),
387 ("russian", Lang::Ru),
388 ("zh", Lang::Zh),
389 ("cn", Lang::Zh),
390 ("chinese", Lang::Zh),
391 ("EN", Lang::En),
392 ("RU", Lang::Ru),
393 ("ZH", Lang::Zh),
394 ("unknown", Lang::En),
395 ("", Lang::En),
396 ("fr", Lang::En),
397 ];
398 for (input, expected) in cases {
399 assert_eq!(parse_lang(input), expected, "parse_lang({input:?})");
400 }
401 }
402
403 #[test]
404 fn set_and_get_lang() {
405 set_lang(Lang::Ru);
406 assert_eq!(lang(), Lang::Ru);
407 set_lang(Lang::Zh);
408 assert_eq!(lang(), Lang::Zh);
409 set_lang(Lang::En);
410 assert_eq!(lang(), Lang::En);
411 }
412
413 #[test]
414 fn strings_returns_correct_lang() {
415 set_lang(Lang::En);
416 assert_eq!(strings().app_name, "PRT");
417 set_lang(Lang::Ru);
418 assert_eq!(strings().app_name, "PRT");
419 assert_eq!(strings().hint_quit, "выход");
421 set_lang(Lang::En);
422 assert_eq!(strings().hint_quit, "quit");
423 }
424
425 #[test]
426 fn strings_all_languages_have_non_empty_fields() {
427 for l in [Lang::En, Lang::Ru, Lang::Zh] {
428 set_lang(l);
429 let s = strings();
430 assert!(!s.app_name.is_empty(), "{:?} app_name empty", l);
431 assert!(!s.connections.is_empty(), "{:?} connections empty", l);
432 assert!(!s.help_text.is_empty(), "{:?} help_text empty", l);
433 assert!(!s.hint_help.is_empty(), "{:?} hint_help empty", l);
434 assert!(!s.hint_lang.is_empty(), "{:?} hint_lang empty", l);
435 assert!(!s.lang_switched.is_empty(), "{:?} lang_switched empty", l);
436 }
437 set_lang(Lang::En); }
439
440 #[test]
441 fn fmt_connections_contains_count() {
442 set_lang(Lang::En);
443 let s = strings();
444 assert!(s.fmt_connections(42).contains("42"));
445 }
446
447 #[test]
448 fn fmt_kill_confirm_contains_name_and_pid() {
449 for l in [Lang::En, Lang::Ru, Lang::Zh] {
450 set_lang(l);
451 let s = strings();
452 let msg = s.fmt_kill_confirm("nginx", 1234);
453 assert!(msg.contains("nginx"), "{:?}: {msg}", l);
454 assert!(msg.contains("1234"), "{:?}: {msg}", l);
455 }
456 set_lang(Lang::En);
457 }
458}