diff --git a/.gitignore b/.gitignore index ce4bc6c..f5f5f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 864320b..aaa158c 100644 --- a/README.md +++ b/README.md @@ -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:** diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6718561..5edd4a9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/include/i18n.h b/include/i18n.h new file mode 100644 index 0000000..f1d20db --- /dev/null +++ b/include/i18n.h @@ -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 */ diff --git a/include/support.h b/include/support.h index 4768aa4..cabc9b4 100644 --- a/include/support.h +++ b/include/support.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 */ diff --git a/src/commands.c b/src/commands.c index dffd56f..b20f79a 100644 --- a/src/commands.c +++ b/src/commands.c @@ -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; diff --git a/src/exec.c b/src/exec.c index 3df1d3d..37dfd37 100644 --- a/src/exec.c +++ b/src/exec.c @@ -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; } diff --git a/src/i18n.c b/src/i18n.c new file mode 100644 index 0000000..4157050 --- /dev/null +++ b/src/i18n.c @@ -0,0 +1,99 @@ +#include "i18n.h" + +#include + +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 ""; +} diff --git a/src/input.c b/src/input.c index bec45a1..707faea 100644 --- a/src/input.c +++ b/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 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; diff --git a/src/main.c b/src/main.c index 175ee42..16be8b8 100644 --- a/src/main.c +++ b/src/main.c @@ -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"); diff --git a/src/support.c b/src/support.c index 3bc55bb..a85b842 100644 --- a/src/support.c +++ b/src/support.c @@ -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 \n" + " 回到最新消息 G 或 End\n" + " 私聊某个人 :msg \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 \n" - " 回到最新消息 G 或 End\n" - " 私聊某个人 :msg \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 \n" + " Return to latest G or End\n" + " Whisper someone :msg \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" diff --git a/src/tui_status.c b/src/tui_status.c index 24e6d8b..795b490 100644 --- a/src/tui_status.c +++ b/src/tui_status.c @@ -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, diff --git a/tests/test_anonymous_access.sh b/tests/test_anonymous_access.sh index 8930049..f325d0b 100755 --- a/tests/test_anonymous_access.sh +++ b/tests/test_anonymous_access.sh @@ -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 diff --git a/tests/test_connection_limits.sh b/tests/test_connection_limits.sh index fa1deec..d0336dc 100755 --- a/tests/test_connection_limits.sh +++ b/tests/test_connection_limits.sh @@ -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=$! diff --git a/tests/test_exec_mode.sh b/tests/test_exec_mode.sh index a03d288..3974dff 100755 --- a/tests/test_exec_mode.sh +++ b/tests/test_exec_mode.sh @@ -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)) diff --git a/tests/test_interactive_input.sh b/tests/test_interactive_input.sh index bc5cc2a..89a485f 100755 --- a/tests/test_interactive_input.sh +++ b/tests/test_interactive_input.sh @@ -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 diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 333db75..c00be97 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -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 diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c new file mode 100644 index 0000000..c6a1bee --- /dev/null +++ b/tests/unit/test_i18n.c @@ -0,0 +1,72 @@ +/* Unit tests for i18n language selection and text lookup */ + +#include "../../include/i18n.h" +#include +#include +#include +#include + +#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; +} diff --git a/tnt.1 b/tnt.1 index 5ddd6c4..7ca1db9 100644 --- a/tnt.1 +++ b/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