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 tab_tree: &'static str,
133 pub tab_network: &'static str,
134 pub tab_connection: &'static str,
135 pub no_selected_process: &'static str,
136
137 pub process_not_found: &'static str,
139
140 pub iface_address: &'static str,
142 pub iface_interface: &'static str,
143 pub iface_protocol: &'static str,
144 pub iface_bind: &'static str,
145 pub iface_localhost_only: &'static str,
146 pub iface_all_interfaces: &'static str,
147 pub iface_specific: &'static str,
148 pub iface_loopback: &'static str,
149 pub iface_all: &'static str,
150
151 pub conn_local: &'static str,
153 pub conn_remote: &'static str,
154 pub conn_state: &'static str,
155 pub conn_process: &'static str,
156 pub conn_cmdline: &'static str,
157
158 pub help_text: &'static str,
160 pub kill_cancel: &'static str,
161 pub copied: &'static str,
162 pub refreshed: &'static str,
163 pub clipboard_unavailable: &'static str,
164 pub scan_error: &'static str,
165 pub cancelled: &'static str,
166 pub lang_switched: &'static str,
167
168 pub sudo_prompt_title: &'static str,
170 pub sudo_password_label: &'static str,
171 pub sudo_confirm_hint: &'static str,
172 pub sudo_failed: &'static str,
173 pub sudo_wrong_password: &'static str,
174 pub sudo_elevated: &'static str,
175
176 pub hint_help: &'static str,
178 pub hint_search: &'static str,
179 pub hint_kill: &'static str,
180 pub hint_sudo: &'static str,
181 pub hint_quit: &'static str,
182 pub hint_lang: &'static str,
183
184 pub help_title: &'static str,
186}
187
188impl Strings {
189 pub fn fmt_connections(&self, n: usize) -> String {
190 format!("{n} {}", self.connections)
191 }
192
193 pub fn fmt_kill_confirm(&self, name: &str, pid: u32) -> String {
194 match lang() {
195 Lang::En => format!("Kill {name} (pid {pid})?"),
196 Lang::Ru => format!("Завершить {name} (pid {pid})?"),
197 Lang::Zh => format!("终止 {name} (pid {pid})?"),
198 }
199 }
200
201 pub fn fmt_kill_sent(&self, sig: &str, name: &str, pid: u32) -> String {
202 match lang() {
203 Lang::En => format!("sent {sig} to {name} (pid {pid})"),
204 Lang::Ru => format!("отправлен {sig} → {name} (pid {pid})"),
205 Lang::Zh => format!("已发送 {sig} → {name} (pid {pid})"),
206 }
207 }
208
209 pub fn fmt_kill_failed(&self, err: &str) -> String {
210 match lang() {
211 Lang::En => format!("kill failed: {err}"),
212 Lang::Ru => format!("ошибка завершения: {err}"),
213 Lang::Zh => format!("终止失败: {err}"),
214 }
215 }
216
217 pub fn fmt_scan_error(&self, err: &str) -> String {
218 format!("{}: {err}", self.scan_error)
219 }
220
221 pub fn fmt_all_ports(&self, n: usize) -> String {
222 match lang() {
223 Lang::En => format!("--- All ports of process ({n}) ---"),
224 Lang::Ru => format!("--- Все порты процесса ({n}) ---"),
225 Lang::Zh => format!("--- 进程所有端口 ({n}) ---"),
226 }
227 }
228
229 pub fn fmt_sudo_error(&self, err: &str) -> String {
230 match lang() {
231 Lang::En => format!("sudo: {err}"),
232 Lang::Ru => format!("sudo ошибка: {err}"),
233 Lang::Zh => format!("sudo 错误: {err}"),
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn lang_next_cycles_all() {
244 let cases = [
245 (Lang::En, Lang::Ru),
246 (Lang::Ru, Lang::Zh),
247 (Lang::Zh, Lang::En),
248 ];
249 for (from, expected) in cases {
250 assert_eq!(from.next(), expected, "{:?}.next()", from);
251 }
252 }
253
254 #[test]
255 fn lang_next_full_cycle() {
256 let start = Lang::En;
257 let after_3 = start.next().next().next();
258 assert_eq!(after_3, start);
259 }
260
261 #[test]
262 fn lang_label() {
263 let cases = [(Lang::En, "EN"), (Lang::Ru, "RU"), (Lang::Zh, "ZH")];
264 for (lang, expected) in cases {
265 assert_eq!(lang.label(), expected);
266 }
267 }
268
269 #[test]
270 fn lang_from_u8_table() {
271 let cases = [
272 (0, Lang::En),
273 (1, Lang::Ru),
274 (2, Lang::Zh),
275 (99, Lang::En),
276 (255, Lang::En),
277 ];
278 for (val, expected) in cases {
279 assert_eq!(Lang::from_u8(val), expected, "from_u8({val})");
280 }
281 }
282
283 #[test]
284 fn parse_lang_table() {
285 let cases = [
286 ("en", Lang::En),
287 ("ru", Lang::Ru),
288 ("russian", Lang::Ru),
289 ("zh", Lang::Zh),
290 ("cn", Lang::Zh),
291 ("chinese", Lang::Zh),
292 ("EN", Lang::En),
293 ("RU", Lang::Ru),
294 ("ZH", Lang::Zh),
295 ("unknown", Lang::En),
296 ("", Lang::En),
297 ("fr", Lang::En),
298 ];
299 for (input, expected) in cases {
300 assert_eq!(parse_lang(input), expected, "parse_lang({input:?})");
301 }
302 }
303
304 #[test]
305 fn set_and_get_lang() {
306 set_lang(Lang::Ru);
307 assert_eq!(lang(), Lang::Ru);
308 set_lang(Lang::Zh);
309 assert_eq!(lang(), Lang::Zh);
310 set_lang(Lang::En);
311 assert_eq!(lang(), Lang::En);
312 }
313
314 #[test]
315 fn strings_returns_correct_lang() {
316 set_lang(Lang::En);
317 assert_eq!(strings().app_name, "PRT");
318 set_lang(Lang::Ru);
319 assert_eq!(strings().app_name, "PRT");
320 assert_eq!(strings().hint_quit, "выход");
322 set_lang(Lang::En);
323 assert_eq!(strings().hint_quit, "quit");
324 }
325
326 #[test]
327 fn strings_all_languages_have_non_empty_fields() {
328 for l in [Lang::En, Lang::Ru, Lang::Zh] {
329 set_lang(l);
330 let s = strings();
331 assert!(!s.app_name.is_empty(), "{:?} app_name empty", l);
332 assert!(!s.connections.is_empty(), "{:?} connections empty", l);
333 assert!(!s.help_text.is_empty(), "{:?} help_text empty", l);
334 assert!(!s.hint_help.is_empty(), "{:?} hint_help empty", l);
335 assert!(!s.hint_lang.is_empty(), "{:?} hint_lang empty", l);
336 assert!(!s.lang_switched.is_empty(), "{:?} lang_switched empty", l);
337 }
338 set_lang(Lang::En); }
340
341 #[test]
342 fn fmt_connections_contains_count() {
343 set_lang(Lang::En);
344 let s = strings();
345 assert!(s.fmt_connections(42).contains("42"));
346 }
347
348 #[test]
349 fn fmt_kill_confirm_contains_name_and_pid() {
350 for l in [Lang::En, Lang::Ru, Lang::Zh] {
351 set_lang(l);
352 let s = strings();
353 let msg = s.fmt_kill_confirm("nginx", 1234);
354 assert!(msg.contains("nginx"), "{:?}: {msg}", l);
355 assert!(msg.contains("1234"), "{:?}: {msg}", l);
356 }
357 set_lang(Lang::En);
358 }
359}