mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 11:14:39 +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_message
|
||||||
tests/unit/test_chat_room
|
tests/unit/test_chat_room
|
||||||
tests/unit/test_history_view
|
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
|
# Show the public SSH endpoint in startup logs
|
||||||
TNT_PUBLIC_HOST=chat.m1ng.space tnt
|
TNT_PUBLIC_HOST=chat.m1ng.space tnt
|
||||||
|
|
||||||
|
# Choose interactive UI language (en or zh; defaults from locale)
|
||||||
|
TNT_LANG=zh tnt
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rate limiting:**
|
**Rate limiting:**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
## 2026-05-21 - Message browsing polish
|
## 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
|
### Changed
|
||||||
- NORMAL mode now opens at the latest visible messages instead of the oldest
|
- 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
|
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"
|
#include "common.h"
|
||||||
|
|
||||||
void support_append_interactive_panel(char *buffer, size_t buf_size,
|
void support_append_interactive_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);
|
void support_append_exec_panel(char *buffer, size_t buf_size, size_t *pos,
|
||||||
|
help_lang_t lang);
|
||||||
|
|
||||||
#endif /* SUPPORT_H */
|
#endif /* SUPPORT_H */
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,8 @@ void commands_dispatch(client_t *client) {
|
||||||
"========================================\n");
|
"========================================\n");
|
||||||
|
|
||||||
} else if (strcmp(cmd, "support") == 0 || strcmp(cmd, "guide") == 0) {
|
} 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) {
|
} else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) {
|
||||||
char *rest = (cmd[0] == 'w') ? cmd + 2 : cmd + 4;
|
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};
|
char output[2048] = {0};
|
||||||
size_t pos = 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;
|
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 "common.h"
|
||||||
#include "exec.h"
|
#include "exec.h"
|
||||||
#include "history_view.h"
|
#include "history_view.h"
|
||||||
|
#include "i18n.h"
|
||||||
#include "message.h"
|
#include "message.h"
|
||||||
#include "ratelimit.h"
|
#include "ratelimit.h"
|
||||||
#include "tui.h"
|
#include "tui.h"
|
||||||
|
|
@ -19,9 +20,11 @@
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT;
|
static int g_idle_timeout = DEFAULT_IDLE_TIMEOUT;
|
||||||
|
static help_lang_t g_default_lang = LANG_EN;
|
||||||
|
|
||||||
void input_init(void) {
|
void input_init(void) {
|
||||||
g_idle_timeout = env_int("TNT_IDLE_TIMEOUT", DEFAULT_IDLE_TIMEOUT, 0, 86400);
|
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) {
|
static int read_username(client_t *client) {
|
||||||
|
|
@ -30,7 +33,8 @@ static int read_username(client_t *client) {
|
||||||
char buf[4];
|
char buf[4];
|
||||||
|
|
||||||
tui_render_welcome(client);
|
tui_render_welcome(client);
|
||||||
client_printf(client, " 请输入用户名 (留空 anonymous): ");
|
client_printf(client, "%s", i18n_text(client->help_lang,
|
||||||
|
I18N_USERNAME_PROMPT));
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
int n = ssh_channel_read_timeout(client->channel, buf, 1, 0, 60000); /* 60 sec timeout */
|
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 */
|
/* Validate username for security */
|
||||||
if (!is_valid_username(client->username)) {
|
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");
|
strcpy(client->username, "anonymous");
|
||||||
} else {
|
} else {
|
||||||
/* Truncate to 20 characters */
|
/* Truncate to 20 characters */
|
||||||
|
|
@ -659,7 +664,7 @@ void input_run_session(client_t *client) {
|
||||||
/* Terminal size already set from PTY request */
|
/* Terminal size already set from PTY request */
|
||||||
client->mode = MODE_INSERT;
|
client->mode = MODE_INSERT;
|
||||||
client->follow_tail = true;
|
client->follow_tail = true;
|
||||||
client->help_lang = LANG_ZH;
|
client->help_lang = g_default_lang;
|
||||||
client->connected = true;
|
client->connected = true;
|
||||||
client->command_history_count = 0;
|
client->command_history_count = 0;
|
||||||
client->command_history_pos = 0;
|
client->command_history_pos = 0;
|
||||||
|
|
@ -683,7 +688,8 @@ void input_run_session(client_t *client) {
|
||||||
|
|
||||||
/* Add to room */
|
/* Add to room */
|
||||||
if (room_add_client(g_room, client) < 0) {
|
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;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
joined_room = true;
|
joined_room = true;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ int main(int argc, char **argv) {
|
||||||
printf(" PORT Default listening port\n");
|
printf(" PORT Default listening port\n");
|
||||||
printf(" TNT_STATE_DIR State directory\n");
|
printf(" TNT_STATE_DIR State directory\n");
|
||||||
printf(" TNT_ACCESS_TOKEN Require this password for SSH auth\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_MAX_CONNECTIONS Global connection limit (default: 64)\n");
|
||||||
printf(" TNT_RATE_LIMIT Set to 0 to disable rate limiting\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");
|
printf(" TNT_IDLE_TIMEOUT Idle disconnect timeout in seconds (default: 1800)\n");
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
#include "support.h"
|
#include "support.h"
|
||||||
|
|
||||||
void support_append_interactive_panel(char *buffer, size_t buf_size,
|
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 (!buffer || !pos) return;
|
||||||
|
|
||||||
|
if (lang == LANG_ZH) {
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[1;36m支持 · support\033[0m\n"
|
"\033[1;36m支持 · support\033[0m\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
@ -29,11 +30,63 @@ void support_append_interactive_panel(char *buffer, size_t buf_size,
|
||||||
" 连接断开: 可能是空闲超时、连接数限制或网络重连\n"
|
" 连接断开: 可能是空闲超时、连接数限制或网络重连\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\033[2;37m更多: ? 打开完整按键帮助,:help 查看命令列表\033[0m\n");
|
"\033[2;37m更多: ? 打开完整按键帮助,:help 查看命令列表\033[0m\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
|
"\033[1;36mSupport\033[0m\n"
|
||||||
|
"\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;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;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;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 (!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,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"TNT support\n"
|
"TNT support\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "tui_status.h"
|
#include "tui_status.h"
|
||||||
|
#include "i18n.h"
|
||||||
#include "ssh_server.h"
|
#include "ssh_server.h"
|
||||||
|
|
||||||
void tui_status_append(char *buffer, size_t buf_size, size_t *pos,
|
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) {
|
if (client->width >= 58) {
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[2;37m›\033[0m "
|
"\033[2;37m›\033[0m "
|
||||||
"\033[2;37mEnter send · Esc browse · :support\033[0m"
|
"\033[2;37m%s\033[0m"
|
||||||
"\033[K");
|
"\033[K",
|
||||||
|
i18n_text(client->help_lang,
|
||||||
|
I18N_INSERT_HINT_WIDE));
|
||||||
} else if (client->width >= 36) {
|
} else if (client->width >= 36) {
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[2;37m›\033[0m "
|
"\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 {
|
} else {
|
||||||
buffer_appendf(buffer, buf_size, pos, "\033[2;37m›\033[0m \033[K");
|
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,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[7;33m NORMAL \033[0m"
|
"\033[7;33m NORMAL \033[0m"
|
||||||
" \033[2;37m%d-%d / %d\033[0m"
|
" \033[2;37m%d-%d / %d\033[0m"
|
||||||
" \033[33m▼ %d new · G latest\033[0m\033[K",
|
" \033[33m▼ %d %s · %s\033[0m\033[K",
|
||||||
range_start, range_end, total, unseen);
|
range_start, range_end, total, unseen,
|
||||||
|
i18n_text(client->help_lang,
|
||||||
|
I18N_NORMAL_NEW_MESSAGES),
|
||||||
|
i18n_text(client->help_lang, I18N_NORMAL_LATEST));
|
||||||
} else {
|
} else {
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
"\033[7;33m NORMAL \033[0m"
|
"\033[7;33m NORMAL \033[0m"
|
||||||
" \033[2;37m%d-%d / %d\033[0m"
|
" \033[2;37m%d-%d / %d\033[0m"
|
||||||
" \033[2;37mG latest\033[0m\033[K",
|
" \033[2;37m%s\033[0m\033[K",
|
||||||
range_start, range_end, total);
|
range_start, range_end, total,
|
||||||
|
i18n_text(client->help_lang, I18N_NORMAL_LATEST));
|
||||||
}
|
}
|
||||||
} else if (client->mode == MODE_COMMAND) {
|
} else if (client->mode == MODE_COMMAND) {
|
||||||
buffer_appendf(buffer, buf_size, pos,
|
buffer_appendf(buffer, buf_size, pos,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ if [ ! -f "$BIN" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting TNT server on port $PORT..."
|
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=$!
|
SERVER_PID=$!
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ wait_for_health() {
|
||||||
|
|
||||||
echo "=== TNT Connection Limit Tests ==="
|
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 &
|
>"$STATE_DIR/concurrent.log" 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ SERVER_PID=""
|
||||||
RATE_PORT=$((PORT + 1))
|
RATE_PORT=$((PORT + 1))
|
||||||
SSH_RATE_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $RATE_PORT"
|
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 &
|
>"$STATE_DIR/rate.log" 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ SSH_OPTS="-n -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o Batc
|
||||||
|
|
||||||
echo "=== TNT Exec Mode Tests ==="
|
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=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
HEALTH_OUTPUT=""
|
HEALTH_OUTPUT=""
|
||||||
|
|
@ -76,8 +76,8 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SUPPORT_OUTPUT=$(ssh $SSH_OPTS localhost support 2>/dev/null || true)
|
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 -Eq '^TNT (support|支持)$' &&
|
||||||
printf '%s\n' "$SUPPORT_OUTPUT" | grep -q '^Troubleshooting:'
|
printf '%s\n' "$SUPPORT_OUTPUT" | grep -Eq '^(Troubleshooting|排查):'
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✓ support returns quick guide"
|
echo "✓ support returns quick guide"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ SSH_OPTS="-e none -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o
|
||||||
|
|
||||||
echo "=== TNT Interactive Input Tests ==="
|
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_PID=$!
|
||||||
|
|
||||||
SERVER_READY=0
|
SERVER_READY=0
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ MESSAGE_SRC = ../../src/message.c
|
||||||
COMMON_SRC = ../../src/common.c
|
COMMON_SRC = ../../src/common.c
|
||||||
CHAT_ROOM_SRC = ../../src/chat_room.c
|
CHAT_ROOM_SRC = ../../src/chat_room.c
|
||||||
HISTORY_VIEW_SRC = ../../src/history_view.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
|
.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)
|
test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
test_i18n: test_i18n.c $(I18N_SRC)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
@echo "=== Running UTF-8 Tests ==="
|
@echo "=== Running UTF-8 Tests ==="
|
||||||
./test_utf8
|
./test_utf8
|
||||||
|
|
@ -46,6 +50,9 @@ run: all
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "=== Running History View Tests ==="
|
@echo "=== Running History View Tests ==="
|
||||||
./test_history_view
|
./test_history_view
|
||||||
|
@echo ""
|
||||||
|
@echo "=== Running i18n Tests ==="
|
||||||
|
./test_i18n
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(TESTS) *.o test_messages.log
|
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.
|
If set, clients must supply this string as their SSH password.
|
||||||
Compared in constant time.
|
Compared in constant time.
|
||||||
.TP
|
.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
|
.B TNT_MAX_CONNECTIONS
|
||||||
Global connection limit (default: 64, max: 1024).
|
Global connection limit (default: 64, max: 1024).
|
||||||
.TP
|
.TP
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue