mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:44:38 +08:00
i18n: select interactive language from locale
This commit is contained in:
parent
39f7f1c7c4
commit
0c27976763
19 changed files with 332 additions and 45 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ tests/unit/test_utf8
|
|||
tests/unit/test_message
|
||||
tests/unit/test_chat_room
|
||||
tests/unit/test_history_view
|
||||
tests/unit/test_i18n
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ TNT_STATE_DIR=/var/lib/tnt tnt
|
|||
|
||||
# Show the public SSH endpoint in startup logs
|
||||
TNT_PUBLIC_HOST=chat.m1ng.space tnt
|
||||
|
||||
# Choose interactive UI language (en or zh; defaults from locale)
|
||||
TNT_LANG=zh tnt
|
||||
```
|
||||
|
||||
**Rate limiting:**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
## 2026-05-21 - Message browsing polish
|
||||
|
||||
### Added
|
||||
- Added a first i18n boundary: `TNT_LANG` / locale detection now chooses the
|
||||
default interactive UI language (`en` or `zh`) for username prompts, status
|
||||
hints, help language, and `:support`.
|
||||
|
||||
### Changed
|
||||
- NORMAL mode now opens at the latest visible messages instead of the oldest
|
||||
in-memory message. Use `k`/PageUp to browse older history and `G`/End to
|
||||
|
|
|
|||
21
include/i18n.h
Normal file
21
include/i18n.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef I18N_H
|
||||
#define I18N_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
typedef enum {
|
||||
I18N_USERNAME_PROMPT,
|
||||
I18N_INVALID_USERNAME,
|
||||
I18N_ROOM_FULL,
|
||||
I18N_INSERT_HINT_WIDE,
|
||||
I18N_INSERT_HINT_NARROW,
|
||||
I18N_NORMAL_LATEST,
|
||||
I18N_NORMAL_NEW_MESSAGES
|
||||
} i18n_text_id_t;
|
||||
|
||||
help_lang_t i18n_parse_lang(const char *value, help_lang_t fallback);
|
||||
help_lang_t i18n_default_lang(void);
|
||||
const char *i18n_lang_code(help_lang_t lang);
|
||||
const char *i18n_text(help_lang_t lang, i18n_text_id_t id);
|
||||
|
||||
#endif /* I18N_H */
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
#include "common.h"
|
||||
|
||||
void support_append_interactive_panel(char *buffer, size_t buf_size,
|
||||
size_t *pos);
|
||||
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos);
|
||||
size_t *pos, help_lang_t lang);
|
||||
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang);
|
||||
|
||||
#endif /* SUPPORT_H */
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ void commands_dispatch(client_t *client) {
|
|||
"========================================\n");
|
||||
|
||||
} else if (strcmp(cmd, "support") == 0 || strcmp(cmd, "guide") == 0) {
|
||||
support_append_interactive_panel(output, sizeof(output), &pos);
|
||||
support_append_interactive_panel(output, sizeof(output), &pos,
|
||||
client->help_lang);
|
||||
|
||||
} else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
||||
char *rest = (cmd[0] == 'w') ? cmd + 2 : cmd + 4;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ static int exec_command_support(client_t *client) {
|
|||
char output[2048] = {0};
|
||||
size_t pos = 0;
|
||||
|
||||
support_append_exec_panel(output, sizeof(output), &pos);
|
||||
support_append_exec_panel(output, sizeof(output), &pos, client->help_lang);
|
||||
return client_send(client, output, pos) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
99
src/i18n.c
Normal file
99
src/i18n.c
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#include "i18n.h"
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
static bool starts_with_lang(const char *value, const char *prefix) {
|
||||
if (!value || !prefix) return false;
|
||||
|
||||
while (*prefix) {
|
||||
if (tolower((unsigned char)*value) !=
|
||||
tolower((unsigned char)*prefix)) {
|
||||
return false;
|
||||
}
|
||||
value++;
|
||||
prefix++;
|
||||
}
|
||||
|
||||
return *value == '\0' || *value == '_' || *value == '-' || *value == '.';
|
||||
}
|
||||
|
||||
help_lang_t i18n_parse_lang(const char *value, help_lang_t fallback) {
|
||||
if (!value || value[0] == '\0') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (starts_with_lang(value, "zh") ||
|
||||
starts_with_lang(value, "cn") ||
|
||||
starts_with_lang(value, "chinese")) {
|
||||
return LANG_ZH;
|
||||
}
|
||||
|
||||
if (starts_with_lang(value, "en") ||
|
||||
starts_with_lang(value, "c") ||
|
||||
starts_with_lang(value, "posix")) {
|
||||
return LANG_EN;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
help_lang_t i18n_default_lang(void) {
|
||||
const char *explicit_lang = getenv("TNT_LANG");
|
||||
if (explicit_lang && explicit_lang[0] != '\0') {
|
||||
return i18n_parse_lang(explicit_lang, LANG_EN);
|
||||
}
|
||||
|
||||
const char *locale = getenv("LC_ALL");
|
||||
if (!locale || locale[0] == '\0') {
|
||||
locale = getenv("LC_MESSAGES");
|
||||
}
|
||||
if (!locale || locale[0] == '\0') {
|
||||
locale = getenv("LANG");
|
||||
}
|
||||
|
||||
return i18n_parse_lang(locale, LANG_EN);
|
||||
}
|
||||
|
||||
const char *i18n_lang_code(help_lang_t lang) {
|
||||
return lang == LANG_ZH ? "zh" : "en";
|
||||
}
|
||||
|
||||
const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
|
||||
if (lang == LANG_ZH) {
|
||||
switch (id) {
|
||||
case I18N_USERNAME_PROMPT:
|
||||
return " 请输入用户名 (留空 anonymous): ";
|
||||
case I18N_INVALID_USERNAME:
|
||||
return "用户名无效,已改用 anonymous。\r\n";
|
||||
case I18N_ROOM_FULL:
|
||||
return "房间已满\r\n";
|
||||
case I18N_INSERT_HINT_WIDE:
|
||||
return "Enter 发送 · Esc 浏览 · :support";
|
||||
case I18N_INSERT_HINT_NARROW:
|
||||
return "Enter · Esc · :support";
|
||||
case I18N_NORMAL_LATEST:
|
||||
return "G 最新";
|
||||
case I18N_NORMAL_NEW_MESSAGES:
|
||||
return "新消息";
|
||||
}
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case I18N_USERNAME_PROMPT:
|
||||
return " Enter display name (blank for anonymous): ";
|
||||
case I18N_INVALID_USERNAME:
|
||||
return "Invalid username. Using 'anonymous' instead.\r\n";
|
||||
case I18N_ROOM_FULL:
|
||||
return "Room is full\r\n";
|
||||
case I18N_INSERT_HINT_WIDE:
|
||||
return "Enter send · Esc browse · :support";
|
||||
case I18N_INSERT_HINT_NARROW:
|
||||
return "Enter · Esc · :support";
|
||||
case I18N_NORMAL_LATEST:
|
||||
return "G latest";
|
||||
case I18N_NORMAL_NEW_MESSAGES:
|
||||
return "new";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
14
src/input.c
14
src/input.c
|
|
@ -5,6 +5,7 @@
|
|||
#include "common.h"
|
||||
#include "exec.h"
|
||||
#include "history_view.h"
|
||||
#include "i18n.h"
|
||||
#include "message.h"
|
||||
#include "ratelimit.h"
|
||||
#include "tui.h"
|
||||
|
|
@ -19,9 +20,11 @@
|
|||
#include <time.h>
|
||||
|
||||
static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT;
|
||||
static help_lang_t g_default_lang = LANG_EN;
|
||||
|
||||
void input_init(void) {
|
||||
g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400);
|
||||
g_default_lang = i18n_default_lang();
|
||||
}
|
||||
|
||||
static int read_username(client_t *client) {
|
||||
|
|
@ -30,7 +33,8 @@ static int read_username(client_t *client) {
|
|||
char buf[4];
|
||||
|
||||
tui_render_welcome(client);
|
||||
client_printf(client, " 请输入用户名 (留空 anonymous): ");
|
||||
client_printf(client, "%s", i18n_text(client->help_lang,
|
||||
I18N_USERNAME_PROMPT));
|
||||
|
||||
while (1) {
|
||||
int n = ssh_channel_read_timeout(client->channel, buf, 1, 0, 60000); /* 60 sec timeout */
|
||||
|
|
@ -112,7 +116,8 @@ static int read_username(client_t *client) {
|
|||
|
||||
/* Validate username for security */
|
||||
if (!is_valid_username(client->username)) {
|
||||
client_printf(client, "Invalid username. Using 'anonymous' instead.\r\n");
|
||||
client_printf(client, "%s", i18n_text(client->help_lang,
|
||||
I18N_INVALID_USERNAME));
|
||||
strcpy(client->username, "anonymous");
|
||||
} else {
|
||||
/* Truncate to 20 characters */
|
||||
|
|
@ -659,7 +664,7 @@ void input_run_session(client_t *client) {
|
|||
/* Terminal size already set from PTY request */
|
||||
client->mode = MODE_INSERT;
|
||||
client->follow_tail = true;
|
||||
client->help_lang = LANG_ZH;
|
||||
client->help_lang = g_default_lang;
|
||||
client->connected = true;
|
||||
client->command_history_count = 0;
|
||||
client->command_history_pos = 0;
|
||||
|
|
@ -683,7 +688,8 @@ void input_run_session(client_t *client) {
|
|||
|
||||
/* Add to room */
|
||||
if (room_add_client(g_room, client) < 0) {
|
||||
client_printf(client, "Room is full\n");
|
||||
client_printf(client, "%s", i18n_text(client->help_lang,
|
||||
I18N_ROOM_FULL));
|
||||
goto cleanup;
|
||||
}
|
||||
joined_room = true;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ int main(int argc, char **argv) {
|
|||
printf(" PORT Default listening port\n");
|
||||
printf(" TNT_STATE_DIR State directory\n");
|
||||
printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\n");
|
||||
printf(" TNT_LANG UI language: en or zh (default: locale)\n");
|
||||
printf(" TNT_MAX_CONNECTIONS Global connection limit (default: 64)\n");
|
||||
printf(" TNT_RATE_LIMIT Set to 0 to disable rate limiting\n");
|
||||
printf(" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n");
|
||||
|
|
|
|||
|
|
@ -1,39 +1,92 @@
|
|||
#include "support.h"
|
||||
|
||||
void support_append_interactive_panel(char *buffer, size_t buf_size,
|
||||
size_t *pos) {
|
||||
size_t *pos, help_lang_t lang) {
|
||||
if (!buffer || !pos) return;
|
||||
|
||||
if (lang == LANG_ZH) {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[1;36m支持 · support\033[0m\n"
|
||||
"\n"
|
||||
"\033[1;37m第一次进来\033[0m\n"
|
||||
" INSERT 输入消息,Enter 发送,ESC 进入 NORMAL\n"
|
||||
" NORMAL 浏览消息,G 回到最新,i 继续输入\n"
|
||||
" COMMAND 按 : 输入命令,q/ESC 关闭当前面板\n"
|
||||
"\n"
|
||||
"\033[1;37m我想...\033[0m\n"
|
||||
" 看谁在线 :users\n"
|
||||
" 看最近历史 :last 20\n"
|
||||
" 搜索聊天记录 :search <keyword>\n"
|
||||
" 回到最新消息 G 或 End\n"
|
||||
" 私聊某个人 :msg <user> <text>\n"
|
||||
" 查看私聊收件箱 :inbox\n"
|
||||
" 静音进出提示 :mute-joins\n"
|
||||
"\n"
|
||||
"\033[1;37m遇到问题\033[0m\n"
|
||||
" 看不到新消息: 在 NORMAL 按 G 或 End 回到最新\n"
|
||||
" 粘贴多行文本: 直接粘贴,TNT 会等 Enter 后一次发送\n"
|
||||
" 输入太长: 状态行接近限制时会提示,超出会响铃\n"
|
||||
" 命令不记得: 输入 :help 看列表,输入 :support 回到这里\n"
|
||||
" 连接断开: 可能是空闲超时、连接数限制或网络重连\n"
|
||||
"\n"
|
||||
"\033[2;37m更多: ? 打开完整按键帮助,:help 查看命令列表\033[0m\n");
|
||||
return;
|
||||
}
|
||||
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[1;36m支持 · support\033[0m\n"
|
||||
"\033[1;36mSupport\033[0m\n"
|
||||
"\n"
|
||||
"\033[1;37m第一次进来\033[0m\n"
|
||||
" INSERT 输入消息,Enter 发送,ESC 进入 NORMAL\n"
|
||||
" NORMAL 浏览消息,G 回到最新,i 继续输入\n"
|
||||
" COMMAND 按 : 输入命令,q/ESC 关闭当前面板\n"
|
||||
"\033[1;37mFirst minute\033[0m\n"
|
||||
" INSERT Type messages, Enter sends, ESC enters NORMAL\n"
|
||||
" NORMAL Browse history, G jumps latest, i continues typing\n"
|
||||
" COMMAND Press : for commands, q/ESC closes this panel\n"
|
||||
"\n"
|
||||
"\033[1;37m我想...\033[0m\n"
|
||||
" 看谁在线 :users\n"
|
||||
" 看最近历史 :last 20\n"
|
||||
" 搜索聊天记录 :search <keyword>\n"
|
||||
" 回到最新消息 G 或 End\n"
|
||||
" 私聊某个人 :msg <user> <text>\n"
|
||||
" 查看私聊收件箱 :inbox\n"
|
||||
" 静音进出提示 :mute-joins\n"
|
||||
"\033[1;37mI want to...\033[0m\n"
|
||||
" See who is online :users\n"
|
||||
" See recent history :last 20\n"
|
||||
" Search history :search <keyword>\n"
|
||||
" Return to latest G or End\n"
|
||||
" Whisper someone :msg <user> <text>\n"
|
||||
" Read whispers :inbox\n"
|
||||
" Mute join notices :mute-joins\n"
|
||||
"\n"
|
||||
"\033[1;37m遇到问题\033[0m\n"
|
||||
" 看不到新消息: 在 NORMAL 按 G 或 End 回到最新\n"
|
||||
" 粘贴多行文本: 直接粘贴,TNT 会等 Enter 后一次发送\n"
|
||||
" 输入太长: 状态行接近限制时会提示,超出会响铃\n"
|
||||
" 命令不记得: 输入 :help 看列表,输入 :support 回到这里\n"
|
||||
" 连接断开: 可能是空闲超时、连接数限制或网络重连\n"
|
||||
"\033[1;37mTroubleshooting\033[0m\n"
|
||||
" Missing new messages: press G or End in NORMAL\n"
|
||||
" Pasting many lines: paste normally, then Enter sends once\n"
|
||||
" Message too long: the status line warns near the limit\n"
|
||||
" Forgot a command: type :help or return here with :support\n"
|
||||
" Disconnected: check idle timeout, limits, or reconnect\n"
|
||||
"\n"
|
||||
"\033[2;37m更多: ? 打开完整按键帮助,:help 查看命令列表\033[0m\n");
|
||||
"\033[2;37mMore: ? opens full key help, :help lists commands\033[0m\n");
|
||||
}
|
||||
|
||||
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos) {
|
||||
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos,
|
||||
help_lang_t lang) {
|
||||
if (!buffer || !pos) return;
|
||||
|
||||
if (lang == LANG_ZH) {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"TNT 支持\n"
|
||||
"\n"
|
||||
"交互使用:\n"
|
||||
" ssh -p 2222 HOST\n"
|
||||
" INSERT: 输入消息并按 Enter 发送\n"
|
||||
" NORMAL: G 回到最新,k/PageUp 查看更早消息\n"
|
||||
" COMMAND: 按 : 后可运行 users, last, search, msg, inbox\n"
|
||||
"\n"
|
||||
"非交互检查:\n"
|
||||
" ssh -p 2222 HOST health\n"
|
||||
" ssh -p 2222 HOST stats --json\n"
|
||||
" ssh -p 2222 HOST users --json\n"
|
||||
" ssh -p 2222 HOST 'tail -n 20'\n"
|
||||
" ssh -p 2222 USER@HOST post 'message'\n"
|
||||
"\n"
|
||||
"排查:\n"
|
||||
" 连接过早关闭: 检查限流、空闲超时、连接容量、\n"
|
||||
" 单 IP 限制和防火墙规则。\n");
|
||||
return;
|
||||
}
|
||||
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"TNT support\n"
|
||||
"\n"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "tui_status.h"
|
||||
#include "i18n.h"
|
||||
#include "ssh_server.h"
|
||||
|
||||
void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
||||
|
|
@ -10,12 +11,16 @@ void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
|||
if (client->width >= 58) {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[2;37m›\033[0m "
|
||||
"\033[2;37mEnter send · Esc browse · :support\033[0m"
|
||||
"\033[K");
|
||||
"\033[2;37m%s\033[0m"
|
||||
"\033[K",
|
||||
i18n_text(client->help_lang,
|
||||
I18N_INSERT_HINT_WIDE));
|
||||
} else if (client->width >= 36) {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[2;37m›\033[0m "
|
||||
"\033[2;37mEnter · Esc · :support\033[0m\033[K");
|
||||
"\033[2;37m%s\033[0m\033[K",
|
||||
i18n_text(client->help_lang,
|
||||
I18N_INSERT_HINT_NARROW));
|
||||
} else {
|
||||
buffer_appendf(buffer, buf_size, pos, "\033[2;37m›\033[0m \033[K");
|
||||
}
|
||||
|
|
@ -29,14 +34,18 @@ void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
|||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[7;33m NORMAL \033[0m"
|
||||
" \033[2;37m%d-%d / %d\033[0m"
|
||||
" \033[33m▼ %d new · G latest\033[0m\033[K",
|
||||
range_start, range_end, total, unseen);
|
||||
" \033[33m▼ %d %s · %s\033[0m\033[K",
|
||||
range_start, range_end, total, unseen,
|
||||
i18n_text(client->help_lang,
|
||||
I18N_NORMAL_NEW_MESSAGES),
|
||||
i18n_text(client->help_lang, I18N_NORMAL_LATEST));
|
||||
} else {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
"\033[7;33m NORMAL \033[0m"
|
||||
" \033[2;37m%d-%d / %d\033[0m"
|
||||
" \033[2;37mG latest\033[0m\033[K",
|
||||
range_start, range_end, total);
|
||||
" \033[2;37m%s\033[0m\033[K",
|
||||
range_start, range_end, total,
|
||||
i18n_text(client->help_lang, I18N_NORMAL_LATEST));
|
||||
}
|
||||
} else if (client->mode == MODE_COMMAND) {
|
||||
buffer_appendf(buffer, buf_size, pos,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ if [ ! -f "$BIN" ]; then
|
|||
fi
|
||||
|
||||
echo "Starting TNT server on port $PORT..."
|
||||
$BIN -p $PORT > /dev/null 2>&1 &
|
||||
TNT_LANG=zh $BIN -p $PORT > /dev/null 2>&1 &
|
||||
SERVER_PID=$!
|
||||
sleep 2
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ wait_for_health() {
|
|||
|
||||
echo "=== TNT Connection Limit Tests ==="
|
||||
|
||||
TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=1 "$BIN" -p "$PORT" -d "$STATE_DIR" \
|
||||
TNT_LANG=zh TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=1 "$BIN" -p "$PORT" -d "$STATE_DIR" \
|
||||
>"$STATE_DIR/concurrent.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ SERVER_PID=""
|
|||
RATE_PORT=$((PORT + 1))
|
||||
SSH_RATE_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $RATE_PORT"
|
||||
|
||||
TNT_MAX_CONN_PER_IP=10 TNT_MAX_CONN_RATE_PER_IP=2 "$BIN" -p "$RATE_PORT" -d "$STATE_DIR" \
|
||||
TNT_LANG=zh TNT_MAX_CONN_PER_IP=10 TNT_MAX_CONN_RATE_PER_IP=2 "$BIN" -p "$RATE_PORT" -d "$STATE_DIR" \
|
||||
>"$STATE_DIR/rate.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SSH_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o Batc
|
|||
|
||||
echo "=== TNT Exec Mode Tests ==="
|
||||
|
||||
TNT_RATE_LIMIT=0 $BIN -p "$PORT" -d "$STATE_DIR" >"${STATE_DIR}/server.log" 2>&1 &
|
||||
TNT_LANG=zh TNT_RATE_LIMIT=0 $BIN -p "$PORT" -d "$STATE_DIR" >"${STATE_DIR}/server.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
HEALTH_OUTPUT=""
|
||||
|
|
@ -76,8 +76,8 @@ else
|
|||
fi
|
||||
|
||||
SUPPORT_OUTPUT=$(ssh $SSH_OPTS localhost support 2>/dev/null || true)
|
||||
printf '%s\n' "$SUPPORT_OUTPUT" | grep -q '^TNT support$' &&
|
||||
printf '%s\n' "$SUPPORT_OUTPUT" | grep -q '^Troubleshooting:'
|
||||
printf '%s\n' "$SUPPORT_OUTPUT" | grep -Eq '^TNT (support|支持)$' &&
|
||||
printf '%s\n' "$SUPPORT_OUTPUT" | grep -Eq '^(Troubleshooting|排查):'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ support returns quick guide"
|
||||
PASS=$((PASS + 1))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ SSH_OPTS="-e none -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o
|
|||
|
||||
echo "=== TNT Interactive Input Tests ==="
|
||||
|
||||
TNT_RATE_LIMIT=0 "$BIN" -p "$PORT" -d "$STATE_DIR" >"$STATE_DIR/server.log" 2>&1 &
|
||||
TNT_LANG=zh TNT_RATE_LIMIT=0 "$BIN" -p "$PORT" -d "$STATE_DIR" >"$STATE_DIR/server.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
SERVER_READY=0
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ MESSAGE_SRC = ../../src/message.c
|
|||
COMMON_SRC = ../../src/common.c
|
||||
CHAT_ROOM_SRC = ../../src/chat_room.c
|
||||
HISTORY_VIEW_SRC = ../../src/history_view.c
|
||||
I18N_SRC = ../../src/i18n.c
|
||||
|
||||
TESTS = test_utf8 test_message test_chat_room test_history_view
|
||||
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n
|
||||
|
||||
.PHONY: all clean run
|
||||
|
||||
|
|
@ -34,6 +35,9 @@ test_chat_room: test_chat_room.c $(CHAT_ROOM_SRC) $(MESSAGE_SRC) $(UTF8_SRC) $(C
|
|||
test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
test_i18n: test_i18n.c $(I18N_SRC)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
run: all
|
||||
@echo "=== Running UTF-8 Tests ==="
|
||||
./test_utf8
|
||||
|
|
@ -46,6 +50,9 @@ run: all
|
|||
@echo ""
|
||||
@echo "=== Running History View Tests ==="
|
||||
./test_history_view
|
||||
@echo ""
|
||||
@echo "=== Running i18n Tests ==="
|
||||
./test_i18n
|
||||
|
||||
clean:
|
||||
rm -f $(TESTS) *.o test_messages.log
|
||||
|
|
|
|||
72
tests/unit/test_i18n.c
Normal file
72
tests/unit/test_i18n.c
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* Unit tests for i18n language selection and text lookup */
|
||||
|
||||
#include "../../include/i18n.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TEST(name) static void test_##name()
|
||||
#define RUN_TEST(name) do { \
|
||||
printf("Running %s... ", #name); \
|
||||
test_##name(); \
|
||||
printf("✓\n"); \
|
||||
tests_passed++; \
|
||||
} while(0)
|
||||
|
||||
static int tests_passed = 0;
|
||||
|
||||
TEST(parse_explicit_languages) {
|
||||
assert(i18n_parse_lang("zh", LANG_EN) == LANG_ZH);
|
||||
assert(i18n_parse_lang("zh_CN.UTF-8", LANG_EN) == LANG_ZH);
|
||||
assert(i18n_parse_lang("cn", LANG_EN) == LANG_ZH);
|
||||
assert(i18n_parse_lang("en", LANG_ZH) == LANG_EN);
|
||||
assert(i18n_parse_lang("en_US.UTF-8", LANG_ZH) == LANG_EN);
|
||||
}
|
||||
|
||||
TEST(parse_unknown_uses_fallback) {
|
||||
assert(i18n_parse_lang(NULL, LANG_ZH) == LANG_ZH);
|
||||
assert(i18n_parse_lang("", LANG_EN) == LANG_EN);
|
||||
assert(i18n_parse_lang("fr_FR.UTF-8", LANG_ZH) == LANG_ZH);
|
||||
}
|
||||
|
||||
TEST(default_prefers_tnt_lang) {
|
||||
setenv("TNT_LANG", "zh_CN.UTF-8", 1);
|
||||
setenv("LC_ALL", "en_US.UTF-8", 1);
|
||||
assert(i18n_default_lang() == LANG_ZH);
|
||||
|
||||
setenv("TNT_LANG", "en", 1);
|
||||
setenv("LC_ALL", "zh_CN.UTF-8", 1);
|
||||
assert(i18n_default_lang() == LANG_EN);
|
||||
}
|
||||
|
||||
TEST(default_uses_locale_when_no_tnt_lang) {
|
||||
unsetenv("TNT_LANG");
|
||||
setenv("LC_ALL", "zh_CN.UTF-8", 1);
|
||||
assert(i18n_default_lang() == LANG_ZH);
|
||||
|
||||
setenv("LC_ALL", "C", 1);
|
||||
assert(i18n_default_lang() == LANG_EN);
|
||||
}
|
||||
|
||||
TEST(text_lookup_matches_language) {
|
||||
assert(strstr(i18n_text(LANG_EN, I18N_USERNAME_PROMPT),
|
||||
"display name") != NULL);
|
||||
assert(strstr(i18n_text(LANG_ZH, I18N_USERNAME_PROMPT),
|
||||
"用户名") != NULL);
|
||||
assert(strcmp(i18n_lang_code(LANG_EN), "en") == 0);
|
||||
assert(strcmp(i18n_lang_code(LANG_ZH), "zh") == 0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Running i18n unit tests...\n\n");
|
||||
|
||||
RUN_TEST(parse_explicit_languages);
|
||||
RUN_TEST(parse_unknown_uses_fallback);
|
||||
RUN_TEST(default_prefers_tnt_lang);
|
||||
RUN_TEST(default_uses_locale_when_no_tnt_lang);
|
||||
RUN_TEST(text_lookup_matches_language);
|
||||
|
||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
||||
return 0;
|
||||
}
|
||||
8
tnt.1
8
tnt.1
|
|
@ -156,6 +156,14 @@ Directory for host key and message log (default: current directory).
|
|||
If set, clients must supply this string as their SSH password.
|
||||
Compared in constant time.
|
||||
.TP
|
||||
.B TNT_LANG
|
||||
Default interactive UI language.
|
||||
Accepts
|
||||
.B en
|
||||
or
|
||||
.BR zh .
|
||||
When unset, TNT detects the process locale and falls back to English.
|
||||
.TP
|
||||
.B TNT_MAX_CONNECTIONS
|
||||
Global connection limit (default: 64, max: 1024).
|
||||
.TP
|
||||
|
|
|
|||
Loading…
Reference in a new issue