From 8fbd789dfb5c3f8a9c2139a72cf80cbb915554b7 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 24 May 2026 12:18:21 +0800 Subject: [PATCH] i18n: split text catalog from language parsing --- README.md | 3 +- docs/CHANGELOG.md | 3 + docs/Development-Guide.md | 10 +- docs/QUICKREF.md | 3 +- include/i18n.h | 3 +- include/tui.h | 1 - src/i18n.c | 266 ------------------------------------ src/i18n_text.c | 278 ++++++++++++++++++++++++++++++++++++++ tests/unit/Makefile | 5 +- tests/unit/test_i18n.c | 13 ++ 10 files changed, 308 insertions(+), 277 deletions(-) create mode 100644 src/i18n_text.c diff --git a/README.md b/README.md index 6c7e57b..35bb893 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,8 @@ TNT/ │ ├── help_text.c # full-screen key reference content │ ├── manual.c # concise manual panel rendering │ ├── manual_text.c # concise manual content -│ ├── i18n.c # language selection and shared UI text +│ ├── i18n.c # UI language and locale selection +│ ├── i18n_text.c # shared UI text catalog │ ├── ratelimit.c # connection limits and rate limiting │ ├── tui.c # terminal UI rendering │ ├── tui_status.c # status/input line rendering diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a4cc69f..6a5583a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,9 @@ ## Unreleased ### Changed +- Split UI-language parsing from localized text lookup: `src/i18n.c` now owns + locale/code parsing, while `src/i18n_text.c` owns the table-driven text + catalog with coverage checks for every message ID. - Renamed the internal language state from help-oriented names to UI-language names (`ui_lang_t`, `client->ui_lang`, and `i18n_*_ui_lang`) so future i18n work has a correctly named seam. diff --git a/docs/Development-Guide.md b/docs/Development-Guide.md index 6ffd6a8..81ea825 100644 --- a/docs/Development-Guide.md +++ b/docs/Development-Guide.md @@ -449,11 +449,11 @@ keys. ### Current Limitations -The current `src/i18n.c` implementation is a small-project translation table -implemented in C, not a full gettext catalog. It is acceptable for two -languages, but adding more languages should first split message lookup from -language parsing and move toward catalog-like storage. Do not grow the -current approach by adding ad hoc branches for every locale. +The current `src/i18n_text.c` implementation is a small-project translation +table implemented in C, not a full gettext catalog. It is acceptable for two +languages because message lookup is already split from language parsing in +`src/i18n.c`, but adding more languages should move toward catalog-like +storage instead of adding ad hoc branches for every locale. Relevant conventions: - POSIX locale variables: `LANG`, `LC_ALL`, `LC_MESSAGES`. diff --git a/docs/QUICKREF.md b/docs/QUICKREF.md index f289663..5a5056a 100644 --- a/docs/QUICKREF.md +++ b/docs/QUICKREF.md @@ -57,7 +57,8 @@ STRUCTURE src/help_text.c full-screen key reference text src/manual.c concise manual panel rendering src/manual_text.c concise manual content - src/i18n.c language selection and shared text + src/i18n.c UI language and locale selection + src/i18n_text.c shared UI text catalog src/ratelimit.c connection limits and rate limiting src/tui.c rendering src/tui_status.c status/input line rendering diff --git a/include/i18n.h b/include/i18n.h index c2c5989..4372bc4 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -60,7 +60,8 @@ typedef enum { I18N_EXEC_POST_USAGE, I18N_EXEC_POST_EMPTY, I18N_EXEC_POST_INVALID_UTF8, - I18N_EXEC_UNKNOWN_COMMAND_FORMAT + I18N_EXEC_UNKNOWN_COMMAND_FORMAT, + I18N_TEXT_COUNT } i18n_text_id_t; bool i18n_try_parse_ui_lang(const char *value, ui_lang_t *lang); diff --git a/include/tui.h b/include/tui.h index c6ddf74..7a33207 100644 --- a/include/tui.h +++ b/include/tui.h @@ -32,5 +32,4 @@ void tui_clear_screen(struct client *client); * itself afterwards. */ void tui_render_welcome(struct client *client); -/* Get help text based on language */ #endif /* TUI_H */ diff --git a/src/i18n.c b/src/i18n.c index f286eac..38b3a3d 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -84,269 +84,3 @@ ui_lang_t i18n_default_ui_lang(void) { const char *i18n_ui_lang_code(ui_lang_t lang) { return lang == UI_LANG_ZH ? "zh" : "en"; } - -const char *i18n_text(ui_lang_t lang, i18n_text_id_t id) { - if (lang == UI_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_WELCOME_SUBTITLE: - return "匿名聊天室 · SSH"; - case I18N_WELCOME_TAGLINE: - return "键盘友好的终端交流"; - case I18N_WELCOME_FALLBACK_FORMAT: - return "TNT %s - SSH 匿名聊天室\r\n\r\n"; - case I18N_INSERT_HINT_WIDE: - return "Enter 发送 · Esc 浏览 · :help"; - case I18N_INSERT_HINT_NARROW: - return "Enter · Esc · :help"; - case I18N_NORMAL_LATEST: - return "G 最新"; - case I18N_NORMAL_NEW_MESSAGES: - return "新消息"; - case I18N_HELP_TITLE: - return " 按键 "; - case I18N_HELP_STATUS_FORMAT: - return "-- 按键参考 -- (%d/%d) j/k:滚动 g/G:首尾 e/z:语言 q:关闭"; - case I18N_COMMAND_OUTPUT_TITLE: - return " 命令输出 "; - case I18N_COMMAND_OUTPUT_STATUS_FORMAT: - return "-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭"; - case I18N_MOTD_TITLE: - return " 公告 "; - case I18N_MOTD_CONTINUE_HINT: - return " 按任意键继续 "; - case I18N_TITLE_ONLINE_FORMAT: - return "在线 %d"; - case I18N_TITLE_MUTED: - return "静音"; - case I18N_TITLE_HELP_HINT: - return "? 按键"; - case I18N_IDLE_TIMEOUT_FORMAT: - return "\r\n\033[33m已断开: 空闲超时 (%d 分钟)\033[0m\r\n"; - 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: - return "用法: msg <用户名> <消息>\n" - " w <用户名> <消息>\n"; - case I18N_MSG_SENT_FORMAT: - return "悄悄话已发送给 %s\n"; - case I18N_MSG_USER_NOT_FOUND_FORMAT: - return "未找到用户 '%s'\n"; - case I18N_INBOX_TITLE: - return "悄悄话"; - case I18N_INBOX_EMPTY: - return "(空)"; - case I18N_NICK_USAGE: - return "用法: nick <新用户名>\n"; - case I18N_NICK_INVALID: - return "用户名无效\n"; - case I18N_NICK_TAKEN_FORMAT: - return "昵称 '%s' 已被使用\n"; - case I18N_NICK_UNCHANGED: - return "昵称未变化\n"; - case I18N_NICK_CHANGED_FORMAT: - return "昵称已修改: %s -> %s\n"; - case I18N_LAST_USAGE: - return "用法: last [N] (N: 1-50,默认 10)\n"; - case I18N_LAST_HEADER_FORMAT: - return "--- 最近 %d 条消息 ---\n"; - case I18N_SEARCH_USAGE: - return "用法: search <关键词>\n"; - case I18N_SEARCH_HEADER_FORMAT: - return "--- 搜索: \"%s\" (%d 条匹配) ---\n"; - case I18N_MUTE_JOINS_FORMAT: - return "加入/离开提示: %s\n"; - case I18N_MUTE_JOINS_MUTED: - return "已静音"; - case I18N_MUTE_JOINS_UNMUTED: - return "已开启"; - case I18N_CLEAR_DONE: - return "命令输出已清空\n"; - case I18N_LANG_CURRENT_FORMAT: - return "当前语言: %s\n" - "用法: lang \n"; - case I18N_LANG_SET_FORMAT: - return "语言已切换为: %s\n"; - case I18N_LANG_UNSUPPORTED_FORMAT: - return "不支持的语言: %s\n" - "用法: lang \n"; - case I18N_UNKNOWN_COMMAND_FORMAT: - return "未知命令: %s\n"; - case I18N_DID_YOU_MEAN_FORMAT: - return "你是想输入 :%s 吗?\n"; - case I18N_UNKNOWN_GUIDANCE: - return "输入 :help 查看帮助\n"; - case I18N_EXEC_HELP: - return "TNT exec 接口\n" - "命令:\n" - " help 显示此帮助\n" - " health 输出服务健康状态\n" - " users [--json] 列出在线用户\n" - " stats [--json] 输出房间统计\n" - " tail [N] 输出最近消息\n" - " tail -n N 输出最近消息\n" - " post MESSAGE 非交互发送消息\n" - " post \"/me act\" 发送动作消息\n" - " exit 成功退出\n"; - case I18N_EXEC_USERS_USAGE: - return "users: 用法: users [--json]\n"; - case I18N_EXEC_STATS_USAGE: - return "stats: 用法: stats [--json]\n"; - case I18N_EXEC_TAIL_USAGE: - return "tail: 用法: tail [N] | tail -n N\n"; - case I18N_EXEC_POST_USAGE: - return "post: 用法: post MESSAGE\n"; - case I18N_EXEC_POST_EMPTY: - return "post: 消息不能为空\n"; - case I18N_EXEC_POST_INVALID_UTF8: - return "post: 输入不是有效 UTF-8\n"; - case I18N_EXEC_UNKNOWN_COMMAND_FORMAT: - return "未知命令: %s\n"; - } - } - - 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_WELCOME_SUBTITLE: - return "anonymous chat · SSH"; - case I18N_WELCOME_TAGLINE: - return "keyboard-first terminal chat"; - case I18N_WELCOME_FALLBACK_FORMAT: - return "TNT %s - anonymous chat over SSH\r\n\r\n"; - case I18N_INSERT_HINT_WIDE: - return "Enter send · Esc browse · :help"; - case I18N_INSERT_HINT_NARROW: - return "Enter · Esc · :help"; - case I18N_NORMAL_LATEST: - return "G latest"; - case I18N_NORMAL_NEW_MESSAGES: - return "new"; - case I18N_HELP_TITLE: - return " KEYS "; - case I18N_HELP_STATUS_FORMAT: - return "-- KEY REFERENCE -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close"; - case I18N_COMMAND_OUTPUT_TITLE: - return " COMMAND OUTPUT "; - case I18N_COMMAND_OUTPUT_STATUS_FORMAT: - return "-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close"; - case I18N_MOTD_TITLE: - return " NOTICE "; - case I18N_MOTD_CONTINUE_HINT: - return " Press any key "; - case I18N_TITLE_ONLINE_FORMAT: - return "online %d"; - case I18N_TITLE_MUTED: - return "muted"; - case I18N_TITLE_HELP_HINT: - return "? keys"; - case I18N_IDLE_TIMEOUT_FORMAT: - return "\r\n\033[33mDisconnected: idle timeout (%d min)\033[0m\r\n"; - 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: - return "Usage: msg \n" - " w \n"; - case I18N_MSG_SENT_FORMAT: - return "Whisper sent to %s\n"; - case I18N_MSG_USER_NOT_FOUND_FORMAT: - return "User '%s' not found\n"; - case I18N_INBOX_TITLE: - return "Whispers"; - case I18N_INBOX_EMPTY: - return "(empty)"; - case I18N_NICK_USAGE: - return "Usage: nick \n"; - case I18N_NICK_INVALID: - return "Invalid username\n"; - case I18N_NICK_TAKEN_FORMAT: - return "Nickname '%s' is already taken\n"; - case I18N_NICK_UNCHANGED: - return "Nickname unchanged\n"; - case I18N_NICK_CHANGED_FORMAT: - return "Nickname changed: %s -> %s\n"; - case I18N_LAST_USAGE: - return "Usage: last [N] (N: 1-50, default 10)\n"; - case I18N_LAST_HEADER_FORMAT: - return "--- Last %d message(s) ---\n"; - case I18N_SEARCH_USAGE: - return "Usage: search \n"; - case I18N_SEARCH_HEADER_FORMAT: - return "--- Search: \"%s\" (%d match(es)) ---\n"; - case I18N_MUTE_JOINS_FORMAT: - return "Join/leave notifications: %s\n"; - case I18N_MUTE_JOINS_MUTED: - return "muted"; - case I18N_MUTE_JOINS_UNMUTED: - return "unmuted"; - case I18N_CLEAR_DONE: - return "Command output cleared\n"; - case I18N_LANG_CURRENT_FORMAT: - return "Current language: %s\n" - "Usage: lang \n"; - case I18N_LANG_SET_FORMAT: - return "Language set to: %s\n"; - case I18N_LANG_UNSUPPORTED_FORMAT: - return "Unsupported language: %s\n" - "Usage: lang \n"; - case I18N_UNKNOWN_COMMAND_FORMAT: - return "Unknown command: %s\n"; - case I18N_DID_YOU_MEAN_FORMAT: - return "Did you mean :%s?\n"; - case I18N_UNKNOWN_GUIDANCE: - return "Type :help for help\n"; - case I18N_EXEC_HELP: - return "TNT exec interface\n" - "Commands:\n" - " help Show this help\n" - " health Print service health\n" - " users [--json] List online users\n" - " stats [--json] Print room statistics\n" - " tail [N] Print recent messages\n" - " tail -n N Print recent messages\n" - " post MESSAGE Post a message non-interactively\n" - " post \"/me act\" Post an action message\n" - " exit Exit successfully\n"; - case I18N_EXEC_USERS_USAGE: - return "users: usage: users [--json]\n"; - case I18N_EXEC_STATS_USAGE: - return "stats: usage: stats [--json]\n"; - case I18N_EXEC_TAIL_USAGE: - return "tail: usage: tail [N] | tail -n N\n"; - case I18N_EXEC_POST_USAGE: - return "post: usage: post MESSAGE\n"; - case I18N_EXEC_POST_EMPTY: - return "post: message cannot be empty\n"; - case I18N_EXEC_POST_INVALID_UTF8: - return "post: invalid UTF-8 input\n"; - case I18N_EXEC_UNKNOWN_COMMAND_FORMAT: - return "Unknown command: %s\n"; - } - - return ""; -} diff --git a/src/i18n_text.c b/src/i18n_text.c new file mode 100644 index 0000000..742a969 --- /dev/null +++ b/src/i18n_text.c @@ -0,0 +1,278 @@ +#include "i18n.h" + +typedef struct { + const char *en; + const char *zh; +} i18n_text_entry_t; + +static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = { + [I18N_USERNAME_PROMPT] = { + " Enter display name (blank for anonymous): ", + " 请输入用户名 (留空 anonymous): " + }, + [I18N_INVALID_USERNAME] = { + "Invalid username. Using 'anonymous' instead.\r\n", + "用户名无效,已改用 anonymous。\r\n" + }, + [I18N_ROOM_FULL] = { + "Room is full\r\n", + "房间已满\r\n" + }, + [I18N_WELCOME_SUBTITLE] = { + "anonymous chat · SSH", + "匿名聊天室 · SSH" + }, + [I18N_WELCOME_TAGLINE] = { + "keyboard-first terminal chat", + "键盘友好的终端交流" + }, + [I18N_WELCOME_FALLBACK_FORMAT] = { + "TNT %s - anonymous chat over SSH\r\n\r\n", + "TNT %s - SSH 匿名聊天室\r\n\r\n" + }, + [I18N_INSERT_HINT_WIDE] = { + "Enter send · Esc browse · :help", + "Enter 发送 · Esc 浏览 · :help" + }, + [I18N_INSERT_HINT_NARROW] = { + "Enter · Esc · :help", + "Enter · Esc · :help" + }, + [I18N_NORMAL_LATEST] = { + "G latest", + "G 最新" + }, + [I18N_NORMAL_NEW_MESSAGES] = { + "new", + "新消息" + }, + [I18N_HELP_TITLE] = { + " KEYS ", + " 按键 " + }, + [I18N_HELP_STATUS_FORMAT] = { + "-- KEY REFERENCE -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close", + "-- 按键参考 -- (%d/%d) j/k:滚动 g/G:首尾 e/z:语言 q:关闭" + }, + [I18N_COMMAND_OUTPUT_TITLE] = { + " COMMAND OUTPUT ", + " 命令输出 " + }, + [I18N_COMMAND_OUTPUT_STATUS_FORMAT] = { + "-- COMMAND OUTPUT -- (%d/%d) j/k:scroll Ctrl-D/U:half g/G:top/bottom q:close", + "-- 命令输出 -- (%d/%d) j/k:滚动 Ctrl-D/U:半页 g/G:首尾 q:关闭" + }, + [I18N_MOTD_TITLE] = { + " NOTICE ", + " 公告 " + }, + [I18N_MOTD_CONTINUE_HINT] = { + " Press any key ", + " 按任意键继续 " + }, + [I18N_TITLE_ONLINE_FORMAT] = { + "online %d", + "在线 %d" + }, + [I18N_TITLE_MUTED] = { + "muted", + "静音" + }, + [I18N_TITLE_HELP_HINT] = { + "? keys", + "? 按键" + }, + [I18N_IDLE_TIMEOUT_FORMAT] = { + "\r\n\033[33mDisconnected: idle timeout (%d min)\033[0m\r\n", + "\r\n\033[33m已断开: 空闲超时 (%d 分钟)\033[0m\r\n" + }, + [I18N_SYSTEM_USERNAME] = { + "system", + "系统" + }, + [I18N_SYSTEM_JOIN_FORMAT] = { + "%s joined the room", + "%s 加入了聊天室" + }, + [I18N_SYSTEM_LEAVE_FORMAT] = { + "%s left the room", + "%s 离开了聊天室" + }, + [I18N_SYSTEM_NICK_FORMAT] = { + "%s renamed to %s", + "%s 更名为 %s" + }, + [I18N_USERS_TITLE] = { + "Online users", + "在线用户" + }, + [I18N_MSG_USAGE] = { + "Usage: msg \n" + " w \n", + "用法: msg <用户名> <消息>\n" + " w <用户名> <消息>\n" + }, + [I18N_MSG_SENT_FORMAT] = { + "Whisper sent to %s\n", + "悄悄话已发送给 %s\n" + }, + [I18N_MSG_USER_NOT_FOUND_FORMAT] = { + "User '%s' not found\n", + "未找到用户 '%s'\n" + }, + [I18N_INBOX_TITLE] = { + "Whispers", + "悄悄话" + }, + [I18N_INBOX_EMPTY] = { + "(empty)", + "(空)" + }, + [I18N_NICK_USAGE] = { + "Usage: nick \n", + "用法: nick <新用户名>\n" + }, + [I18N_NICK_INVALID] = { + "Invalid username\n", + "用户名无效\n" + }, + [I18N_NICK_TAKEN_FORMAT] = { + "Nickname '%s' is already taken\n", + "昵称 '%s' 已被使用\n" + }, + [I18N_NICK_UNCHANGED] = { + "Nickname unchanged\n", + "昵称未变化\n" + }, + [I18N_NICK_CHANGED_FORMAT] = { + "Nickname changed: %s -> %s\n", + "昵称已修改: %s -> %s\n" + }, + [I18N_LAST_USAGE] = { + "Usage: last [N] (N: 1-50, default 10)\n", + "用法: last [N] (N: 1-50,默认 10)\n" + }, + [I18N_LAST_HEADER_FORMAT] = { + "--- Last %d message(s) ---\n", + "--- 最近 %d 条消息 ---\n" + }, + [I18N_SEARCH_USAGE] = { + "Usage: search \n", + "用法: search <关键词>\n" + }, + [I18N_SEARCH_HEADER_FORMAT] = { + "--- Search: \"%s\" (%d match(es)) ---\n", + "--- 搜索: \"%s\" (%d 条匹配) ---\n" + }, + [I18N_MUTE_JOINS_FORMAT] = { + "Join/leave notifications: %s\n", + "加入/离开提示: %s\n" + }, + [I18N_MUTE_JOINS_MUTED] = { + "muted", + "已静音" + }, + [I18N_MUTE_JOINS_UNMUTED] = { + "unmuted", + "已开启" + }, + [I18N_CLEAR_DONE] = { + "Command output cleared\n", + "命令输出已清空\n" + }, + [I18N_LANG_CURRENT_FORMAT] = { + "Current language: %s\n" + "Usage: lang \n", + "当前语言: %s\n" + "用法: lang \n" + }, + [I18N_LANG_SET_FORMAT] = { + "Language set to: %s\n", + "语言已切换为: %s\n" + }, + [I18N_LANG_UNSUPPORTED_FORMAT] = { + "Unsupported language: %s\n" + "Usage: lang \n", + "不支持的语言: %s\n" + "用法: lang \n" + }, + [I18N_UNKNOWN_COMMAND_FORMAT] = { + "Unknown command: %s\n", + "未知命令: %s\n" + }, + [I18N_DID_YOU_MEAN_FORMAT] = { + "Did you mean :%s?\n", + "你是想输入 :%s 吗?\n" + }, + [I18N_UNKNOWN_GUIDANCE] = { + "Type :help for help\n", + "输入 :help 查看帮助\n" + }, + [I18N_EXEC_HELP] = { + "TNT exec interface\n" + "Commands:\n" + " help Show this help\n" + " health Print service health\n" + " users [--json] List online users\n" + " stats [--json] Print room statistics\n" + " tail [N] Print recent messages\n" + " tail -n N Print recent messages\n" + " post MESSAGE Post a message non-interactively\n" + " post \"/me act\" Post an action message\n" + " exit Exit successfully\n", + "TNT exec 接口\n" + "命令:\n" + " help 显示此帮助\n" + " health 输出服务健康状态\n" + " users [--json] 列出在线用户\n" + " stats [--json] 输出房间统计\n" + " tail [N] 输出最近消息\n" + " tail -n N 输出最近消息\n" + " post MESSAGE 非交互发送消息\n" + " post \"/me act\" 发送动作消息\n" + " exit 成功退出\n" + }, + [I18N_EXEC_USERS_USAGE] = { + "users: usage: users [--json]\n", + "users: 用法: users [--json]\n" + }, + [I18N_EXEC_STATS_USAGE] = { + "stats: usage: stats [--json]\n", + "stats: 用法: stats [--json]\n" + }, + [I18N_EXEC_TAIL_USAGE] = { + "tail: usage: tail [N] | tail -n N\n", + "tail: 用法: tail [N] | tail -n N\n" + }, + [I18N_EXEC_POST_USAGE] = { + "post: usage: post MESSAGE\n", + "post: 用法: post MESSAGE\n" + }, + [I18N_EXEC_POST_EMPTY] = { + "post: message cannot be empty\n", + "post: 消息不能为空\n" + }, + [I18N_EXEC_POST_INVALID_UTF8] = { + "post: invalid UTF-8 input\n", + "post: 输入不是有效 UTF-8\n" + }, + [I18N_EXEC_UNKNOWN_COMMAND_FORMAT] = { + "Unknown command: %s\n", + "未知命令: %s\n" + } +}; + +const char *i18n_text(ui_lang_t lang, i18n_text_id_t id) { + if (id < 0 || id >= I18N_TEXT_COUNT) { + return ""; + } + + const i18n_text_entry_t *entry = &text_catalog[id]; + if (lang == UI_LANG_ZH && entry->zh) { + return entry->zh; + } + if (entry->en) { + return entry->en; + } + return ""; +} diff --git a/tests/unit/Makefile b/tests/unit/Makefile index bea6a5f..c1bcbb6 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -18,6 +18,7 @@ CLI_TEXT_SRC = ../../src/cli_text.c CHAT_ROOM_SRC = ../../src/chat_room.c HISTORY_VIEW_SRC = ../../src/history_view.c I18N_SRC = ../../src/i18n.c +I18N_TEXT_SRC = ../../src/i18n_text.c SYSTEM_MESSAGE_SRC = ../../src/system_message.c HELP_TEXT_SRC = ../../src/help_text.c MANUAL_TEXT_SRC = ../../src/manual_text.c @@ -41,10 +42,10 @@ 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) +test_i18n: test_i18n.c $(I18N_SRC) $(I18N_TEXT_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_system_message: test_system_message.c $(SYSTEM_MESSAGE_SRC) $(I18N_SRC) +test_system_message: test_system_message.c $(SYSTEM_MESSAGE_SRC) $(I18N_SRC) $(I18N_TEXT_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_command_catalog: test_command_catalog.c $(COMMAND_CATALOG_SRC) $(COMMON_SRC) diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index 8b11f1e..b9deecd 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -146,6 +146,18 @@ TEST(text_lookup_matches_language) { assert(strcmp(i18n_ui_lang_code(UI_LANG_ZH), "zh") == 0); } +TEST(text_catalog_is_complete) { + for (int id = 0; id < I18N_TEXT_COUNT; id++) { + assert(i18n_text(UI_LANG_EN, (i18n_text_id_t)id)[0] != '\0'); + assert(i18n_text(UI_LANG_ZH, (i18n_text_id_t)id)[0] != '\0'); + } + + assert(strcmp(i18n_text(UI_LANG_EN, + (i18n_text_id_t)I18N_TEXT_COUNT), "") == 0); + assert(strcmp(i18n_text(UI_LANG_ZH, + (i18n_text_id_t)I18N_TEXT_COUNT), "") == 0); +} + int main(void) { printf("Running i18n unit tests...\n\n"); @@ -155,6 +167,7 @@ int main(void) { RUN_TEST(default_prefers_tnt_lang); RUN_TEST(default_uses_locale_when_no_tnt_lang); RUN_TEST(text_lookup_matches_language); + RUN_TEST(text_catalog_is_complete); printf("\n✓ All %d tests passed!\n", tests_passed); return 0;