i18n: select interactive language from locale

This commit is contained in:
m1ngsama 2026-05-23 18:06:39 +08:00
parent 39f7f1c7c4
commit 0c27976763
19 changed files with 332 additions and 45 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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:**

View file

@ -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
View 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 */

View file

@ -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 */

View file

@ -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;

View file

@ -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
View 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 "";
}

View file

@ -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;

View file

@ -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");

View file

@ -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"

View file

@ -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,

View file

@ -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

View file

@ -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=$!

View file

@ -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))

View file

@ -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

View file

@ -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
View 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
View file

@ -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