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    // Kernel TCP block
128    pub kernel_tcp_heading: &'static str,
129    pub tcp_post_connect_row: &'static str,
130    pub tcp_final_row: &'static str,
131    pub tcp_during: &'static str,
132    pub tcp_retransmit_word: &'static str,
133
134    // Body & throughput
135    pub body_size: &'static str,
136    pub body_discarded: &'static str,
137    pub saved_to: &'static str,
138    pub throughput: &'static str,
139    pub throughput_first_100k: &'static str,
140    pub throughput_then: &'static str,
141
142    // Benchmark summary
143    pub benchmark_results_prefix: &'static str,
144    pub benchmark_results_requests: &'static str,
145    pub success: &'static str,
146    pub cold_connect: &'static str,
147    pub min: &'static str,
148    pub max: &'static str,
149    pub avg: &'static str,
150}
151
152pub const EN: Strings = Strings {
153    connected_to: "Connected to",
154    resolved_to: "Resolved to",
155    error_label: "Error",
156    fail: "FAIL",
157    grpc_ok: "GRPC OK",
158
159    dns_lookup: "DNS Lookup",
160    dns_connect: "DNS Connect",
161    dns_query: "DNS Query",
162    tcp_connect: "TCP Connect",
163    tls_handshake: "TLS Handshake",
164    quic_connect: "QUIC Connect",
165    request_send: "Request Send",
166    server_processing: "Server Processing",
167    server_processing_short: "Server Process",
168    content_transfer: "Content Transfer",
169    content_transfer_short: "Content Xfer",
170    total: "Total",
171
172    tls_label: "Tls",
173    cipher: "Cipher",
174    handshake: "Handshake",
175    handshake_full: "Full",
176    handshake_resumed: "Resumed",
177    early_data: "Early Data",
178    early_data_accepted: "accepted (0-RTT)",
179    early_data_not_accepted: "not accepted",
180    ocsp: "OCSP",
181    ocsp_stapled: "stapled",
182    ocsp_not_stapled: "not stapled",
183    not_before: "Not Before",
184    not_after: "Not After",
185    subject: "Subject",
186    issuer: "Issuer",
187    cert_domains: "Certificate Domains",
188    cert_chain: "Certificate Chain",
189
190    server_timing_heading: "Server-Timing:",
191    st_sum_of: "of",
192    st_unaccounted: "unaccounted",
193
194    kernel_tcp_heading: "Kernel TCP:",
195    tcp_post_connect_row: "post-connect:",
196    tcp_final_row: "final:",
197    tcp_during: "during request:",
198    tcp_retransmit_word: "retransmit(s)",
199
200    body_size: "Body size",
201    body_discarded: "Body discarded",
202    saved_to: "saved to",
203    throughput: "Throughput:",
204    throughput_first_100k: "first 100KB:",
205    throughput_then: "then:",
206
207    benchmark_results_prefix: "--- Benchmark Results",
208    benchmark_results_requests: "requests",
209    success: "Success",
210    cold_connect: "Cold connect",
211    min: "min",
212    max: "max",
213    avg: "avg",
214};
215
216pub const ZH: Strings = Strings {
217    connected_to: "已连接到",
218    resolved_to: "已解析到",
219    error_label: "错误",
220    fail: "失败",
221    grpc_ok: "gRPC 正常",
222
223    dns_lookup: "DNS 解析",
224    dns_connect: "DNS 连接",
225    dns_query: "DNS 查询",
226    tcp_connect: "TCP 连接",
227    tls_handshake: "TLS 握手",
228    quic_connect: "QUIC 连接",
229    request_send: "请求发送",
230    server_processing: "服务端处理",
231    server_processing_short: "服务端处理",
232    content_transfer: "内容传输",
233    content_transfer_short: "内容传输",
234    total: "总耗时",
235
236    tls_label: "TLS",
237    cipher: "加密套件",
238    handshake: "握手",
239    handshake_full: "完整",
240    handshake_resumed: "复用",
241    early_data: "早期数据",
242    early_data_accepted: "已接受 (0-RTT)",
243    early_data_not_accepted: "未接受",
244    ocsp: "OCSP",
245    ocsp_stapled: "已 staple",
246    ocsp_not_stapled: "未 staple",
247    not_before: "生效时间",
248    not_after: "到期时间",
249    subject: "主体",
250    issuer: "颁发者",
251    cert_domains: "证书域名",
252    cert_chain: "证书链",
253
254    server_timing_heading: "Server-Timing:",
255    st_sum_of: "占",
256    st_unaccounted: "未统计",
257
258    kernel_tcp_heading: "内核 TCP:",
259    tcp_post_connect_row: "连接后:",
260    tcp_final_row: "完成时:",
261    tcp_during: "本次请求:",
262    tcp_retransmit_word: "次重传",
263
264    body_size: "响应体大小",
265    body_discarded: "响应体已丢弃",
266    saved_to: "已保存到",
267    throughput: "吞吐:",
268    throughput_first_100k: "首 100KB:",
269    throughput_then: "之后:",
270
271    benchmark_results_prefix: "--- 基准测试结果",
272    benchmark_results_requests: "次请求",
273    success: "成功",
274    cold_connect: "冷连接",
275    min: "最小",
276    max: "最大",
277    avg: "均值",
278};