Skip to main content

http_stat/
i18n.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Tiny i18n layer for the terminal renderer.
16//!
17//! Two locales are wired up — English (default, fallback) and Simplified
18//! Chinese. The CLI flag `--lang` overrides; otherwise we sniff the standard
19//! POSIX locale environment variables. JSON output stays in English keys —
20//! the contract for machine consumers must not move with the user's locale.
21
22use std::env;
23
24/// Supported display languages.
25#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Lang {
27    #[default]
28    En,
29    Zh,
30}
31
32impl Lang {
33    /// Detect the locale from `LC_ALL`, then `LC_MESSAGES`, then `LANG`.
34    /// A value starting with `zh` (case-insensitive, e.g. `zh_CN.UTF-8`,
35    /// `zh-Hans`, `zh_TW`) maps to [`Lang::Zh`]. Everything else falls back
36    /// to English.
37    pub fn detect() -> Self {
38        for key in ["LC_ALL", "LC_MESSAGES", "LANG"] {
39            if let Ok(v) = env::var(key) {
40                let v = v.trim().to_ascii_lowercase();
41                if v.starts_with("zh") {
42                    return Lang::Zh;
43                }
44                if !v.is_empty() {
45                    return Lang::En;
46                }
47            }
48        }
49        Lang::En
50    }
51
52    /// Parse the value passed via `--lang`. Accepts `en`, `english`, `zh`,
53    /// `zh-cn`, `zh_cn`, `cn`, `chinese` (case-insensitive). Unknown values
54    /// fall back to [`Lang::En`] — the user's intent is "show me something",
55    /// not "abort because of a typo".
56    pub fn parse_arg(s: &str) -> Self {
57        let s = s.trim().to_ascii_lowercase();
58        if s.starts_with("zh") || s == "cn" || s == "chinese" {
59            Lang::Zh
60        } else {
61            Lang::En
62        }
63    }
64
65    /// Return the static label table for this language.
66    pub fn strings(self) -> &'static Strings {
67        match self {
68            Lang::En => &EN,
69            Lang::Zh => &ZH,
70        }
71    }
72}
73
74/// All translatable labels. Each field is a single `&'static str`; nothing
75/// here uses `format!` arguments, so adding a new locale is mechanical.
76///
77/// Technical jargon that has no good Chinese equivalent (`cwnd`, `MSS`,
78/// `0-RTT`, `OCSP`, ALPN strings like `H2`/`HTTP/1.1`, status codes,
79/// duration formatting) intentionally stays in the original form.
80#[derive(Debug)]
81pub struct Strings {
82    // Connection line
83    pub connected_to: &'static str,
84    pub resolved_to: &'static str,
85    pub error_label: &'static str,
86    pub fail: &'static str,
87    pub grpc_ok: &'static str,
88
89    // Phases (timeline columns + waterfall rows)
90    pub dns_lookup: &'static str,
91    pub dns_connect: &'static str,
92    pub dns_query: &'static str,
93    pub tcp_connect: &'static str,
94    pub tls_handshake: &'static str,
95    pub quic_connect: &'static str,
96    pub request_send: &'static str,
97    pub server_processing: &'static str,
98    pub server_processing_short: &'static str,
99    pub content_transfer: &'static str,
100    pub content_transfer_short: &'static str,
101    pub total: &'static str,
102
103    // TLS block
104    pub tls_label: &'static str,
105    pub cipher: &'static str,
106    pub handshake: &'static str,
107    pub handshake_full: &'static str,
108    pub handshake_resumed: &'static str,
109    pub early_data: &'static str,
110    pub early_data_accepted: &'static str,
111    pub early_data_not_accepted: &'static str,
112    pub ocsp: &'static str,
113    pub ocsp_stapled: &'static str,
114    pub ocsp_not_stapled: &'static str,
115    pub not_before: &'static str,
116    pub not_after: &'static str,
117    pub subject: &'static str,
118    pub issuer: &'static str,
119    pub cert_domains: &'static str,
120    pub cert_chain: &'static str,
121
122    // Server-Timing summary line
123    pub server_timing_heading: &'static str,
124    pub st_sum_of: &'static str,
125    pub st_unaccounted: &'static str,
126
127    // Protocol advertisements (Alt-Svc / HSTS)
128    pub protocol_adv_heading: &'static str,
129    pub alt_svc_label: &'static str,
130    pub hsts_label: &'static str,
131
132    // Kernel TCP block
133    pub kernel_tcp_heading: &'static str,
134    pub tcp_post_connect_row: &'static str,
135    pub tcp_final_row: &'static str,
136    pub tcp_during: &'static str,
137    pub tcp_retransmit_word: &'static str,
138
139    // Body & throughput
140    pub body_size: &'static str,
141    pub body_discarded: &'static str,
142    pub saved_to: &'static str,
143    pub throughput: &'static str,
144    pub throughput_first_100k: &'static str,
145    pub throughput_then: &'static str,
146
147    // Benchmark summary
148    pub benchmark_results_prefix: &'static str,
149    pub benchmark_results_requests: &'static str,
150    pub success: &'static str,
151    pub cold_connect: &'static str,
152    pub min: &'static str,
153    pub max: &'static str,
154    pub avg: &'static str,
155}
156
157pub const EN: Strings = Strings {
158    connected_to: "Connected to",
159    resolved_to: "Resolved to",
160    error_label: "Error",
161    fail: "FAIL",
162    grpc_ok: "GRPC OK",
163
164    dns_lookup: "DNS Lookup",
165    dns_connect: "DNS Connect",
166    dns_query: "DNS Query",
167    tcp_connect: "TCP Connect",
168    tls_handshake: "TLS Handshake",
169    quic_connect: "QUIC Connect",
170    request_send: "Request Send",
171    server_processing: "Server Processing",
172    server_processing_short: "Server Process",
173    content_transfer: "Content Transfer",
174    content_transfer_short: "Content Xfer",
175    total: "Total",
176
177    tls_label: "Tls",
178    cipher: "Cipher",
179    handshake: "Handshake",
180    handshake_full: "Full",
181    handshake_resumed: "Resumed",
182    early_data: "Early Data",
183    early_data_accepted: "accepted (0-RTT)",
184    early_data_not_accepted: "not accepted",
185    ocsp: "OCSP",
186    ocsp_stapled: "stapled",
187    ocsp_not_stapled: "not stapled",
188    not_before: "Not Before",
189    not_after: "Not After",
190    subject: "Subject",
191    issuer: "Issuer",
192    cert_domains: "Certificate Domains",
193    cert_chain: "Certificate Chain",
194
195    server_timing_heading: "Server-Timing:",
196    st_sum_of: "of",
197    st_unaccounted: "unaccounted",
198
199    protocol_adv_heading: "Server Advertisements:",
200    alt_svc_label: "Alt-Svc:",
201    hsts_label: "HSTS:   ",
202
203    kernel_tcp_heading: "Kernel TCP:",
204    tcp_post_connect_row: "post-connect:",
205    tcp_final_row: "final:",
206    tcp_during: "during request:",
207    tcp_retransmit_word: "retransmit(s)",
208
209    body_size: "Body size",
210    body_discarded: "Body discarded",
211    saved_to: "saved to",
212    throughput: "Throughput:",
213    throughput_first_100k: "first 100KB:",
214    throughput_then: "then:",
215
216    benchmark_results_prefix: "--- Benchmark Results",
217    benchmark_results_requests: "requests",
218    success: "Success",
219    cold_connect: "Cold connect",
220    min: "min",
221    max: "max",
222    avg: "avg",
223};
224
225pub const ZH: Strings = Strings {
226    connected_to: "已连接到",
227    resolved_to: "已解析到",
228    error_label: "错误",
229    fail: "失败",
230    grpc_ok: "gRPC 正常",
231
232    dns_lookup: "DNS 解析",
233    dns_connect: "DNS 连接",
234    dns_query: "DNS 查询",
235    tcp_connect: "TCP 连接",
236    tls_handshake: "TLS 握手",
237    quic_connect: "QUIC 连接",
238    request_send: "请求发送",
239    server_processing: "服务端处理",
240    server_processing_short: "服务端处理",
241    content_transfer: "内容传输",
242    content_transfer_short: "内容传输",
243    total: "总耗时",
244
245    tls_label: "TLS",
246    cipher: "加密套件",
247    handshake: "握手",
248    handshake_full: "完整",
249    handshake_resumed: "复用",
250    early_data: "早期数据",
251    early_data_accepted: "已接受 (0-RTT)",
252    early_data_not_accepted: "未接受",
253    ocsp: "OCSP",
254    ocsp_stapled: "已 staple",
255    ocsp_not_stapled: "未 staple",
256    not_before: "生效时间",
257    not_after: "到期时间",
258    subject: "主体",
259    issuer: "颁发者",
260    cert_domains: "证书域名",
261    cert_chain: "证书链",
262
263    server_timing_heading: "Server-Timing:",
264    st_sum_of: "占",
265    st_unaccounted: "未统计",
266
267    protocol_adv_heading: "服务器声明:",
268    alt_svc_label: "Alt-Svc:",
269    hsts_label: "HSTS:   ",
270
271    kernel_tcp_heading: "内核 TCP:",
272    tcp_post_connect_row: "连接后:",
273    tcp_final_row: "完成时:",
274    tcp_during: "本次请求:",
275    tcp_retransmit_word: "次重传",
276
277    body_size: "响应体大小",
278    body_discarded: "响应体已丢弃",
279    saved_to: "已保存到",
280    throughput: "吞吐:",
281    throughput_first_100k: "首 100KB:",
282    throughput_then: "之后:",
283
284    benchmark_results_prefix: "--- 基准测试结果",
285    benchmark_results_requests: "次请求",
286    success: "成功",
287    cold_connect: "冷连接",
288    min: "最小",
289    max: "最大",
290    avg: "均值",
291};
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn parse_arg_maps_languages() {
299        assert_eq!(Lang::parse_arg("zh"), Lang::Zh);
300        assert_eq!(Lang::parse_arg("ZH-CN"), Lang::Zh);
301        assert_eq!(Lang::parse_arg("zh_TW"), Lang::Zh);
302        assert_eq!(Lang::parse_arg("cn"), Lang::Zh);
303        assert_eq!(Lang::parse_arg("chinese"), Lang::Zh);
304        assert_eq!(Lang::parse_arg("en"), Lang::En);
305        assert_eq!(Lang::parse_arg("fr"), Lang::En); // unknown → English
306        assert_eq!(Lang::parse_arg(""), Lang::En);
307    }
308}