i18n: module system event messages

This commit is contained in:
m1ngsama 2026-05-23 19:30:11 +08:00
parent 1d8fcea3fa
commit 07e47e65c8
12 changed files with 259 additions and 31 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ tests/unit/test_message
tests/unit/test_chat_room
tests/unit/test_history_view
tests/unit/test_i18n
tests/unit/test_system_message

View file

@ -22,6 +22,9 @@
the i18n table instead of being scattered through command flow logic.
- TUI title-bar status labels, including online count, mute marker, and help
hint, now follow the session UI language.
- Join, leave, and nickname-change system messages now use a dedicated
`system_message` module, follow the sender's session language, and keep
`:mute-joins` filtering compatible with both Chinese and English logs.
### Changed
- NORMAL mode now opens at the latest visible messages instead of the oldest

View file

@ -19,6 +19,10 @@ typedef enum {
I18N_TITLE_ONLINE_FORMAT,
I18N_TITLE_MUTED,
I18N_TITLE_HELP_HINT,
I18N_SYSTEM_USERNAME,
I18N_SYSTEM_JOIN_FORMAT,
I18N_SYSTEM_LEAVE_FORMAT,
I18N_SYSTEM_NICK_FORMAT,
I18N_USERS_TITLE,
I18N_MSG_USAGE,
I18N_MSG_SENT_FORMAT,

17
include/system_message.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef SYSTEM_MESSAGE_H
#define SYSTEM_MESSAGE_H
#include "common.h"
#include "message.h"
void system_message_make_join(message_t *msg, const char *username,
help_lang_t lang);
void system_message_make_leave(message_t *msg, const char *username,
help_lang_t lang);
void system_message_make_nick(message_t *msg, const char *old_name,
const char *new_name, help_lang_t lang);
bool system_message_is_system(const message_t *msg);
bool system_message_is_join_leave(const message_t *msg);
#endif /* SYSTEM_MESSAGE_H */

View file

@ -11,6 +11,7 @@
#include "i18n.h"
#include "message.h"
#include "support.h"
#include "system_message.h"
#include "tui.h"
#include "utf8.h"
#include <stdio.h>
@ -425,10 +426,9 @@ void commands_dispatch(client_t *client) {
i18n_text(client->help_lang,
I18N_NICK_UNCHANGED));
} else {
message_t nick_msg = { .timestamp = time(NULL) };
snprintf(nick_msg.username, MAX_USERNAME_LEN, "系统");
snprintf(nick_msg.content, MAX_MESSAGE_LEN,
"%s 更名为 %s", old_name, client->username);
message_t nick_msg;
system_message_make_nick(&nick_msg, old_name,
client->username, client->help_lang);
room_broadcast(g_room, &nick_msg);
message_save(&nick_msg);

View file

@ -101,6 +101,14 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
return "静音";
case I18N_TITLE_HELP_HINT:
return "? 帮助";
case I18N_SYSTEM_USERNAME:
return "系统";
case I18N_SYSTEM_JOIN_FORMAT:
return "%s 加入了聊天室";
case I18N_SYSTEM_LEAVE_FORMAT:
return "%s 离开了聊天室";
case I18N_SYSTEM_NICK_FORMAT:
return "%s 更名为 %s";
case I18N_USERS_TITLE:
return "在线用户";
case I18N_MSG_USAGE:
@ -174,6 +182,14 @@ const char *i18n_text(help_lang_t lang, i18n_text_id_t id) {
return "muted";
case I18N_TITLE_HELP_HINT:
return "? help";
case I18N_SYSTEM_USERNAME:
return "system";
case I18N_SYSTEM_JOIN_FORMAT:
return "%s joined the room";
case I18N_SYSTEM_LEAVE_FORMAT:
return "%s left the room";
case I18N_SYSTEM_NICK_FORMAT:
return "%s renamed to %s";
case I18N_USERS_TITLE:
return "Online users";
case I18N_MSG_USAGE:

View file

@ -8,6 +8,7 @@
#include "i18n.h"
#include "message.h"
#include "ratelimit.h"
#include "system_message.h"
#include "tui.h"
#include "utf8.h"
#include <libssh/callbacks.h>
@ -701,12 +702,8 @@ void input_run_session(client_t *client) {
bracketed_paste_enabled = true;
/* Broadcast join message */
message_t join_msg = {
.timestamp = time(NULL),
};
strncpy(join_msg.username, "系统", MAX_USERNAME_LEN - 1);
join_msg.username[MAX_USERNAME_LEN - 1] = '\0';
snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username);
message_t join_msg;
system_message_make_join(&join_msg, client->username, client->help_lang);
room_broadcast(g_room, &join_msg);
message_save(&join_msg);
@ -892,12 +889,9 @@ cleanup:
/* Broadcast leave message */
if (joined_room) {
message_t leave_msg = {
.timestamp = time(NULL),
};
strncpy(leave_msg.username, "系统", MAX_USERNAME_LEN - 1);
leave_msg.username[MAX_USERNAME_LEN - 1] = '\0';
snprintf(leave_msg.content, MAX_MESSAGE_LEN, "%s 离开了聊天室", client->username);
message_t leave_msg;
system_message_make_leave(&leave_msg, client->username,
client->help_lang);
client->connected = false;
room_remove_client(g_room, client);

72
src/system_message.c Normal file
View file

@ -0,0 +1,72 @@
#include "system_message.h"
#include "i18n.h"
#include <stdio.h>
#include <string.h>
#include <time.h>
static void system_message_init(message_t *msg, help_lang_t lang) {
if (!msg) {
return;
}
memset(msg, 0, sizeof(*msg));
msg->timestamp = time(NULL);
snprintf(msg->username, sizeof(msg->username), "%s",
i18n_text(lang, I18N_SYSTEM_USERNAME));
}
void system_message_make_join(message_t *msg, const char *username,
help_lang_t lang) {
system_message_init(msg, lang);
if (!msg) {
return;
}
snprintf(msg->content, sizeof(msg->content),
i18n_text(lang, I18N_SYSTEM_JOIN_FORMAT),
username ? username : "");
}
void system_message_make_leave(message_t *msg, const char *username,
help_lang_t lang) {
system_message_init(msg, lang);
if (!msg) {
return;
}
snprintf(msg->content, sizeof(msg->content),
i18n_text(lang, I18N_SYSTEM_LEAVE_FORMAT),
username ? username : "");
}
void system_message_make_nick(message_t *msg, const char *old_name,
const char *new_name, help_lang_t lang) {
system_message_init(msg, lang);
if (!msg) {
return;
}
snprintf(msg->content, sizeof(msg->content),
i18n_text(lang, I18N_SYSTEM_NICK_FORMAT),
old_name ? old_name : "", new_name ? new_name : "");
}
bool system_message_is_system(const message_t *msg) {
if (!msg) {
return false;
}
return strcmp(msg->username, "系统") == 0 ||
strcmp(msg->username, "system") == 0;
}
bool system_message_is_join_leave(const message_t *msg) {
if (!system_message_is_system(msg)) {
return false;
}
return strstr(msg->content, "加入了聊天室") != NULL ||
strstr(msg->content, "离开了聊天室") != NULL ||
strstr(msg->content, "joined the room") != NULL ||
strstr(msg->content, "left the room") != NULL;
}

View file

@ -4,16 +4,11 @@
#include "chat_room.h"
#include "history_view.h"
#include "i18n.h"
#include "system_message.h"
#include "tui_status.h"
#include "utf8.h"
#include <unistd.h>
static bool is_join_leave_msg(const message_t *msg) {
if (strcmp(msg->username, "系统") != 0) return false;
return strstr(msg->content, "加入了聊天室") != NULL ||
strstr(msg->content, "离开了聊天室") != NULL;
}
static const char *username_color(const char *name) {
static const char *colors[] = {
"\033[31m", "\033[32m", "\033[33m",
@ -37,7 +32,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
* marker so they can scan their own contributions when scrolling. */
bool is_self = false;
if (my_username && my_username[0] != '\0' &&
strcmp(msg->username, "系统") != 0) {
!system_message_is_system(msg)) {
if (strcmp(msg->username, "*") == 0) {
/* /me message: content starts with the actor's username */
size_t un_len = strlen(my_username);
@ -54,7 +49,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
bool mentioned = false;
if (my_username && my_username[0] != '\0' &&
strcmp(msg->username, "系统") != 0) {
!system_message_is_system(msg)) {
char mention[MAX_USERNAME_LEN + 2];
snprintf(mention, sizeof(mention), "@%s", my_username);
if (strstr(msg->content, mention) != NULL) {
@ -64,7 +59,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
const char *hl_start = mentioned ? "\033[1;33m" : "";
const char *hl_end = mentioned ? "\033[0m" : "";
if (strcmp(msg->username, "系统") == 0) {
if (system_message_is_system(msg)) {
snprintf(buffer, buf_size,
"%s\033[90m--> %s\033[0m", gutter, msg->content);
} else if (strcmp(msg->username, "*") == 0) {
@ -80,7 +75,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
/* Plain-text version for width calculation — gutter is 1 column. */
char plain[MAX_MESSAGE_LEN + 128];
if (strcmp(msg->username, "系统") == 0) {
if (system_message_is_system(msg)) {
snprintf(plain, sizeof(plain), " --> %s", msg->content);
} else if (strcmp(msg->username, "*") == 0) {
snprintf(plain, sizeof(plain), " %s * %s", time_str, msg->content);
@ -94,7 +89,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
* 1-column gutter so the budget math comes out right. */
int prefix_width;
char prefix_plain[256];
if (strcmp(msg->username, "系统") == 0) {
if (system_message_is_system(msg)) {
snprintf(prefix_plain, sizeof(prefix_plain), " --> ");
} else if (strcmp(msg->username, "*") == 0) {
snprintf(prefix_plain, sizeof(prefix_plain), " %s * ", time_str);
@ -107,7 +102,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
if (content_width < 4) content_width = 4;
char truncated_content[MAX_MESSAGE_LEN];
if (strcmp(msg->username, "系统") == 0) {
if (system_message_is_system(msg)) {
strncpy(truncated_content, msg->content, sizeof(truncated_content) - 1);
truncated_content[sizeof(truncated_content) - 1] = '\0';
} else if (strcmp(msg->username, "*") == 0) {
@ -118,7 +113,7 @@ static void format_message_colored(const message_t *msg, char *buffer,
}
utf8_truncate(truncated_content, content_width);
if (strcmp(msg->username, "系统") == 0) {
if (system_message_is_system(msg)) {
snprintf(buffer, buf_size,
"%s\033[90m--> %s\033[0m", gutter, truncated_content);
} else if (strcmp(msg->username, "*") == 0) {
@ -325,7 +320,7 @@ void tui_render_screen(client_t *client) {
if (client->mute_joins && msg_snapshot) {
int filtered = 0;
for (int i = 0; i < snapshot_count; i++) {
if (!is_join_leave_msg(&msg_snapshot[i])) {
if (!system_message_is_join_leave(&msg_snapshot[i])) {
msg_snapshot[filtered++] = msg_snapshot[i];
}
}

View file

@ -322,6 +322,48 @@ else
FAIL=$((FAIL + 1))
fi
SYSTEM_MESSAGES_SCRIPT="$STATE_DIR/system-messages.expect"
cat >"$SYSTEM_MESSAGES_SCRIPT" <<EOF
set timeout 10
spawn ssh $SSH_OPTS anonymous@127.0.0.1
sleep 1
send -- "systemuser\r"
expect ":support"
send -- "\033"
expect "NORMAL"
send -- ":"
expect ":"
send -- "lang en\r"
expect "Language set to: en"
expect "Press any key"
send -- "q"
expect "NORMAL"
send -- ":"
expect ":"
send -- "nick systemuser2\r"
expect "Nickname changed: systemuser -> systemuser2"
expect "Press any key"
send -- "q"
sleep 0.2
send -- "\003"
sleep 0.2
send -- "\003"
expect eof
EOF
if expect "$SYSTEM_MESSAGES_SCRIPT" >"$STATE_DIR/system-messages.log" 2>&1 &&
grep -q 'system|systemuser renamed to systemuser2' "$STATE_DIR/messages.log" &&
grep -q 'system|systemuser2 left the room' "$STATE_DIR/messages.log"; then
echo "✓ system messages follow session language"
PASS=$((PASS + 1))
else
echo "x localized system messages failed"
sed -n '1,220p' "$STATE_DIR/system-messages.log" 2>/dev/null || true
cat "$STATE_DIR/messages.log" 2>/dev/null || true
sed -n '1,120p' "$STATE_DIR/server.log"
FAIL=$((FAIL + 1))
fi
printf '维护窗口\n' >"$STATE_DIR/motd.txt"
MOTD_SCRIPT="$STATE_DIR/motd.expect"
cat >"$MOTD_SCRIPT" <<EOF

View file

@ -16,8 +16,9 @@ COMMON_SRC = ../../src/common.c
CHAT_ROOM_SRC = ../../src/chat_room.c
HISTORY_VIEW_SRC = ../../src/history_view.c
I18N_SRC = ../../src/i18n.c
SYSTEM_MESSAGE_SRC = ../../src/system_message.c
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message
.PHONY: all clean run
@ -38,6 +39,9 @@ test_history_view: test_history_view.c $(HISTORY_VIEW_SRC)
test_i18n: test_i18n.c $(I18N_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_system_message: test_system_message.c $(SYSTEM_MESSAGE_SRC) $(I18N_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
run: all
@echo "=== Running UTF-8 Tests ==="
./test_utf8
@ -53,6 +57,9 @@ run: all
@echo ""
@echo "=== Running i18n Tests ==="
./test_i18n
@echo ""
@echo "=== Running System Message Tests ==="
./test_system_message
clean:
rm -f $(TESTS) *.o test_messages.log

View file

@ -0,0 +1,77 @@
/* Unit tests for localized system event messages */
#include "../../include/system_message.h"
#include <assert.h>
#include <stdio.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(join_leave_follow_language) {
message_t msg;
system_message_make_join(&msg, "alice", LANG_ZH);
assert(strcmp(msg.username, "系统") == 0);
assert(strstr(msg.content, "alice") != NULL);
assert(strstr(msg.content, "加入了聊天室") != NULL);
assert(system_message_is_system(&msg));
assert(system_message_is_join_leave(&msg));
system_message_make_leave(&msg, "bob", LANG_EN);
assert(strcmp(msg.username, "system") == 0);
assert(strstr(msg.content, "bob") != NULL);
assert(strstr(msg.content, "left the room") != NULL);
assert(system_message_is_system(&msg));
assert(system_message_is_join_leave(&msg));
}
TEST(nick_messages_are_system_events_not_join_leave) {
message_t msg;
system_message_make_nick(&msg, "old", "new", LANG_EN);
assert(strcmp(msg.username, "system") == 0);
assert(strstr(msg.content, "old") != NULL);
assert(strstr(msg.content, "new") != NULL);
assert(strstr(msg.content, "renamed") != NULL);
assert(system_message_is_system(&msg));
assert(!system_message_is_join_leave(&msg));
system_message_make_nick(&msg, "", "", LANG_ZH);
assert(strcmp(msg.username, "系统") == 0);
assert(strstr(msg.content, "更名为") != NULL);
assert(system_message_is_system(&msg));
assert(!system_message_is_join_leave(&msg));
}
TEST(legacy_system_names_are_recognized) {
message_t msg = {0};
snprintf(msg.username, sizeof(msg.username), "系统");
snprintf(msg.content, sizeof(msg.content), "alice 离开了聊天室");
assert(system_message_is_system(&msg));
assert(system_message_is_join_leave(&msg));
snprintf(msg.username, sizeof(msg.username), "system");
snprintf(msg.content, sizeof(msg.content), "alice joined the room");
assert(system_message_is_system(&msg));
assert(system_message_is_join_leave(&msg));
}
int main(void) {
printf("Running system message unit tests...\n\n");
RUN_TEST(join_leave_follow_language);
RUN_TEST(nick_messages_are_system_events_not_join_leave);
RUN_TEST(legacy_system_names_are_recognized);
printf("\n✓ All %d tests passed!\n", tests_passed);
return 0;
}