diff --git a/README.md b/README.md index aaa158c..eec5d92 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Ctrl+C - Exit chat :search - Search full message history (case-insensitive) :mute-joins - Toggle join/leave system notifications :support - Show quick support guide +:lang - Switch UI language for this session :help - Show available commands :clear - Clear command output :q, :quit, :exit - Disconnect diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5edd4a9..ebbd15b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,8 @@ - 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`. +- Added `:lang ` so users can switch the interactive UI language for + their current session. ### Changed - NORMAL mode now opens at the latest visible messages instead of the oldest diff --git a/include/i18n.h b/include/i18n.h index f1d20db..8c6210e 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -13,6 +13,7 @@ typedef enum { I18N_NORMAL_NEW_MESSAGES } i18n_text_id_t; +bool i18n_try_parse_lang(const char *value, help_lang_t *lang); 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); diff --git a/src/commands.c b/src/commands.c index b20f79a..4ea1e36 100644 --- a/src/commands.c +++ b/src/commands.c @@ -8,6 +8,7 @@ #include "chat_room.h" #include "client.h" #include "common.h" +#include "i18n.h" #include "message.h" #include "support.h" #include "tui.h" @@ -82,7 +83,8 @@ static const char *suggest_command(const char *cmd) { static const char *commands[] = { "list", "users", "who", "nick", "name", "msg", "w", "inbox", "last", "search", "mute-joins", "mute", "support", "guide", - "help", "commands", "clear", "cls", "q", "quit", "exit" + "lang", "language", "help", "commands", "clear", "cls", + "q", "quit", "exit" }; const char *best = NULL; int best_distance = 99; @@ -177,6 +179,7 @@ void commands_dispatch(client_t *client) { "search - Search message history\n" "mute-joins - Toggle join/leave notices\n" "support - Show quick support guide\n" + "lang [en|zh] - Show or switch UI language\n" "help, commands - Show this help\n" "clear, cls - Clear command output\n" "q, quit, exit - Disconnect\n" @@ -191,6 +194,34 @@ void commands_dispatch(client_t *client) { support_append_interactive_panel(output, sizeof(output), &pos, client->help_lang); + } else if (strcmp(cmd, "lang") == 0 || strcmp(cmd, "language") == 0 || + strncmp(cmd, "lang ", 5) == 0 || + strncmp(cmd, "language ", 9) == 0) { + char *arg = NULL; + help_lang_t next_lang; + + if (strncmp(cmd, "lang ", 5) == 0) { + arg = cmd + 5; + } else if (strncmp(cmd, "language ", 9) == 0) { + arg = cmd + 9; + } + + if (!arg || arg[0] == '\0') { + buffer_appendf(output, sizeof(output), &pos, + "Current language: %s\n" + "Usage: lang \n", + i18n_lang_code(client->help_lang)); + } else if (i18n_try_parse_lang(arg, &next_lang)) { + client->help_lang = next_lang; + buffer_appendf(output, sizeof(output), &pos, + "Language set to: %s\n", + i18n_lang_code(client->help_lang)); + } else { + buffer_appendf(output, sizeof(output), &pos, + "Unsupported language: %s\n" + "Usage: lang \n", arg); + } + } else if (strncmp(cmd, "msg ", 4) == 0 || strncmp(cmd, "w ", 2) == 0) { char *rest = (cmd[0] == 'w') ? cmd + 2 : cmd + 4; while (*rest == ' ') rest++; diff --git a/src/i18n.c b/src/i18n.c index 4157050..3bc6c72 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -17,23 +17,33 @@ static bool starts_with_lang(const char *value, const char *prefix) { return *value == '\0' || *value == '_' || *value == '-' || *value == '.'; } -help_lang_t i18n_parse_lang(const char *value, help_lang_t fallback) { +bool i18n_try_parse_lang(const char *value, help_lang_t *lang) { if (!value || value[0] == '\0') { - return fallback; + return false; } if (starts_with_lang(value, "zh") || starts_with_lang(value, "cn") || starts_with_lang(value, "chinese")) { - return LANG_ZH; + if (lang) *lang = LANG_ZH; + return true; } if (starts_with_lang(value, "en") || starts_with_lang(value, "c") || starts_with_lang(value, "posix")) { - return LANG_EN; + if (lang) *lang = LANG_EN; + return true; } + return false; +} + +help_lang_t i18n_parse_lang(const char *value, help_lang_t fallback) { + help_lang_t lang; + if (i18n_try_parse_lang(value, &lang)) { + return lang; + } return fallback; } diff --git a/src/tui.c b/src/tui.c index 77948bf..31edf3a 100644 --- a/src/tui.c +++ b/src/tui.c @@ -784,6 +784,7 @@ const char* tui_get_help_text(help_lang_t lang) { " :search - Search message history\n" " :mute-joins - Toggle join/leave notices\n" " :support - Show quick support guide\n" + " :lang - Switch UI language\n" " :help - Show available commands\n" " :clear - Clear command output\n" " :q, :quit, :exit - Disconnect\n" @@ -838,6 +839,7 @@ const char* tui_get_help_text(help_lang_t lang) { " :search <关键词> - 搜索消息历史\n" " :mute-joins - 切换加入/离开提示\n" " :support - 显示快速支持指南\n" + " :lang - 切换界面语言\n" " :help - 显示可用命令\n" " :clear - 清空命令输出\n" " :q, :quit, :exit - 断开连接\n" diff --git a/tests/test_interactive_input.sh b/tests/test_interactive_input.sh index 89a485f..672c624 100755 --- a/tests/test_interactive_input.sh +++ b/tests/test_interactive_input.sh @@ -148,6 +148,13 @@ send -- "support\r" expect "支持" expect "Press any key" send -- "q" +expect "NORMAL" +send -- ":" +expect ":" +send -- "lang en\r" +expect "Language set to: en" +expect "Press any key" +send -- "q" sleep 0.2 send -- "\003" sleep 0.2 diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index c6a1bee..d39f301 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -17,11 +17,19 @@ static int tests_passed = 0; TEST(parse_explicit_languages) { + help_lang_t lang; + 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); + + assert(i18n_try_parse_lang("zh", &lang) == true); + assert(lang == LANG_ZH); + assert(i18n_try_parse_lang("en", &lang) == true); + assert(lang == LANG_EN); + assert(i18n_try_parse_lang("fr", &lang) == false); } TEST(parse_unknown_uses_fallback) { diff --git a/tnt.1 b/tnt.1 index 7ca1db9..d360eca 100644 --- a/tnt.1 +++ b/tnt.1 @@ -121,6 +121,7 @@ l l. :search \fIkeyword\fR Case\-insensitive search across full message history :mute\-joins Toggle join/leave system notifications on/off :support Show quick support guide +:lang \fIen|zh\fR Switch UI language for this session :help Show available commands :clear Clear command output :q, :quit, :exit Disconnect