i18n: split text catalog from language parsing

This commit is contained in:
m1ngsama 2026-05-24 12:18:21 +08:00
parent 06a10e2df8
commit 8fbd789dfb
10 changed files with 308 additions and 277 deletions

View file

@ -259,7 +259,8 @@ TNT/
│ ├── help_text.c # full-screen key reference content │ ├── help_text.c # full-screen key reference content
│ ├── manual.c # concise manual panel rendering │ ├── manual.c # concise manual panel rendering
│ ├── manual_text.c # concise manual content │ ├── 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 │ ├── ratelimit.c # connection limits and rate limiting
│ ├── tui.c # terminal UI rendering │ ├── tui.c # terminal UI rendering
│ ├── tui_status.c # status/input line rendering │ ├── tui_status.c # status/input line rendering

View file

@ -3,6 +3,9 @@
## Unreleased ## Unreleased
### Changed ### 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 - Renamed the internal language state from help-oriented names to
UI-language names (`ui_lang_t`, `client->ui_lang`, and UI-language names (`ui_lang_t`, `client->ui_lang`, and
`i18n_*_ui_lang`) so future i18n work has a correctly named seam. `i18n_*_ui_lang`) so future i18n work has a correctly named seam.

View file

@ -449,11 +449,11 @@ keys.
### Current Limitations ### Current Limitations
The current `src/i18n.c` implementation is a small-project translation table The current `src/i18n_text.c` implementation is a small-project translation
implemented in C, not a full gettext catalog. It is acceptable for two 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 languages because message lookup is already split from language parsing in
language parsing and move toward catalog-like storage. Do not grow the `src/i18n.c`, but adding more languages should move toward catalog-like
current approach by adding ad hoc branches for every locale. storage instead of adding ad hoc branches for every locale.
Relevant conventions: Relevant conventions:
- POSIX locale variables: `LANG`, `LC_ALL`, `LC_MESSAGES`. - POSIX locale variables: `LANG`, `LC_ALL`, `LC_MESSAGES`.

View file

@ -57,7 +57,8 @@ STRUCTURE
src/help_text.c full-screen key reference text src/help_text.c full-screen key reference text
src/manual.c concise manual panel rendering src/manual.c concise manual panel rendering
src/manual_text.c concise manual content 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/ratelimit.c connection limits and rate limiting
src/tui.c rendering src/tui.c rendering
src/tui_status.c status/input line rendering src/tui_status.c status/input line rendering

View file

@ -60,7 +60,8 @@ typedef enum {
I18N_EXEC_POST_USAGE, I18N_EXEC_POST_USAGE,
I18N_EXEC_POST_EMPTY, I18N_EXEC_POST_EMPTY,
I18N_EXEC_POST_INVALID_UTF8, I18N_EXEC_POST_INVALID_UTF8,
I18N_EXEC_UNKNOWN_COMMAND_FORMAT I18N_EXEC_UNKNOWN_COMMAND_FORMAT,
I18N_TEXT_COUNT
} i18n_text_id_t; } i18n_text_id_t;
bool i18n_try_parse_ui_lang(const char *value, ui_lang_t *lang); bool i18n_try_parse_ui_lang(const char *value, ui_lang_t *lang);

View file

@ -32,5 +32,4 @@ void tui_clear_screen(struct client *client);
* itself afterwards. */ * itself afterwards. */
void tui_render_welcome(struct client *client); void tui_render_welcome(struct client *client);
/* Get help text based on language */
#endif /* TUI_H */ #endif /* TUI_H */

View file

@ -84,269 +84,3 @@ ui_lang_t i18n_default_ui_lang(void) {
const char *i18n_ui_lang_code(ui_lang_t lang) { const char *i18n_ui_lang_code(ui_lang_t lang) {
return lang == UI_LANG_ZH ? "zh" : "en"; 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 <en|zh>\n";
case I18N_LANG_SET_FORMAT:
return "语言已切换为: %s\n";
case I18N_LANG_UNSUPPORTED_FORMAT:
return "不支持的语言: %s\n"
"用法: lang <en|zh>\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 <username> <message>\n"
" w <username> <message>\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 <new_username>\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 <keyword>\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 <en|zh>\n";
case I18N_LANG_SET_FORMAT:
return "Language set to: %s\n";
case I18N_LANG_UNSUPPORTED_FORMAT:
return "Unsupported language: %s\n"
"Usage: lang <en|zh>\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 "";
}

278
src/i18n_text.c Normal file
View file

@ -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 <username> <message>\n"
" w <username> <message>\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 <new_username>\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 <keyword>\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 <en|zh>\n",
"当前语言: %s\n"
"用法: lang <en|zh>\n"
},
[I18N_LANG_SET_FORMAT] = {
"Language set to: %s\n",
"语言已切换为: %s\n"
},
[I18N_LANG_UNSUPPORTED_FORMAT] = {
"Unsupported language: %s\n"
"Usage: lang <en|zh>\n",
"不支持的语言: %s\n"
"用法: lang <en|zh>\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 "";
}

View file

@ -18,6 +18,7 @@ CLI_TEXT_SRC = ../../src/cli_text.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 I18N_SRC = ../../src/i18n.c
I18N_TEXT_SRC = ../../src/i18n_text.c
SYSTEM_MESSAGE_SRC = ../../src/system_message.c SYSTEM_MESSAGE_SRC = ../../src/system_message.c
HELP_TEXT_SRC = ../../src/help_text.c HELP_TEXT_SRC = ../../src/help_text.c
MANUAL_TEXT_SRC = ../../src/manual_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) 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) test_i18n: test_i18n.c $(I18N_SRC) $(I18N_TEXT_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(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) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_command_catalog: test_command_catalog.c $(COMMAND_CATALOG_SRC) $(COMMON_SRC) test_command_catalog: test_command_catalog.c $(COMMAND_CATALOG_SRC) $(COMMON_SRC)

View file

@ -146,6 +146,18 @@ TEST(text_lookup_matches_language) {
assert(strcmp(i18n_ui_lang_code(UI_LANG_ZH), "zh") == 0); 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) { int main(void) {
printf("Running i18n unit tests...\n\n"); printf("Running i18n unit tests...\n\n");
@ -155,6 +167,7 @@ int main(void) {
RUN_TEST(default_prefers_tnt_lang); RUN_TEST(default_prefers_tnt_lang);
RUN_TEST(default_uses_locale_when_no_tnt_lang); RUN_TEST(default_uses_locale_when_no_tnt_lang);
RUN_TEST(text_lookup_matches_language); RUN_TEST(text_lookup_matches_language);
RUN_TEST(text_catalog_is_complete);
printf("\n✓ All %d tests passed!\n", tests_passed); printf("\n✓ All %d tests passed!\n", tests_passed);
return 0; return 0;