From d1d44d0914308cdcf9f02f7c5cd52231bb1d6c33 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 24 May 2026 15:27:19 +0800 Subject: [PATCH] i18n: share localized string helper --- docs/CHANGELOG.md | 2 + include/i18n.h | 20 +++++ src/command_catalog.c | 128 +++++++++++++++--------------- src/i18n_text.c | 16 +--- tests/unit/test_command_catalog.c | 6 ++ tests/unit/test_i18n.c | 6 ++ 6 files changed, 98 insertions(+), 80 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 82b42c4..a41ec8e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -54,6 +54,8 @@ module descriptions for the split between language parsing and text lookup. - `i18n_text` now indexes localized strings by `UI_LANG_*` instead of storing English/Chinese as hard-coded struct fields. +- `command_catalog` now uses the shared localized-string helper for help, + manual, and usage text instead of per-field English/Chinese members. - Documented i18n and user-facing text rules for English-first source text, stable command syntax, concise help copy, and translation-only localization. diff --git a/include/i18n.h b/include/i18n.h index 986f99f..10c7900 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -3,6 +3,13 @@ #include "common.h" +typedef struct { + const char *text[UI_LANG_COUNT]; +} i18n_string_t; + +#define I18N_STRING(en_text, zh_text) \ + {{ [UI_LANG_EN] = (en_text), [UI_LANG_ZH] = (zh_text) }} + typedef enum { I18N_USERNAME_PROMPT, I18N_INVALID_USERNAME, @@ -62,4 +69,17 @@ ui_lang_t i18n_next_ui_lang(ui_lang_t lang); const char *i18n_ui_lang_code(ui_lang_t lang); const char *i18n_text(ui_lang_t lang, i18n_text_id_t id); +static inline const char *i18n_string(i18n_string_t value, ui_lang_t lang) { + if ((int)lang < 0 || lang >= UI_LANG_COUNT) { + lang = UI_LANG_EN; + } + if (value.text[lang]) { + return value.text[lang]; + } + if (value.text[UI_LANG_EN]) { + return value.text[UI_LANG_EN]; + } + return ""; +} + #endif /* I18N_H */ diff --git a/src/command_catalog.c b/src/command_catalog.c index 53d87e0..f04c375 100644 --- a/src/command_catalog.c +++ b/src/command_catalog.c @@ -1,17 +1,15 @@ #include "command_catalog.h" +#include "i18n.h" + #include typedef struct { tnt_command_spec_t spec; - const char *full_usage_en; - const char *full_usage_zh; - const char *summary_en; - const char *summary_zh; - const char *manual_usage_en; - const char *manual_usage_zh; - const char *error_usage_en; - const char *error_usage_zh; + i18n_string_t full_usage; + i18n_string_t summary; + i18n_string_t manual_usage; + i18n_string_t error_usage; int manual_group; bool no_args; bool requires_args; @@ -20,95 +18,97 @@ typedef struct { static const command_catalog_entry_t entries[] = { { {TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}}, - ":users, :list, :who", ":users, :list, :who", - "Show online users", "显示在线用户", - ":users", ":users", - "Usage: users\n", "用法: users\n", + I18N_STRING(":users, :list, :who", ":users, :list, :who"), + I18N_STRING("Show online users", "显示在线用户"), + I18N_STRING(":users", ":users"), + I18N_STRING("Usage: users\n", "用法: users\n"), 1, true, false }, { {TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}}, - ":msg , :w ", - ":msg , :w ", - "Send private message", "发送私信", - ":msg ", ":msg ", - "Usage: msg \n" - " w \n", - "用法: msg \n" - " w \n", + I18N_STRING(":msg , :w ", + ":msg , :w "), + I18N_STRING("Send private message", "发送私信"), + I18N_STRING(":msg ", ":msg "), + I18N_STRING("Usage: msg \n" + " w \n", + "用法: msg \n" + " w \n"), 2, false, true }, { {TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}}, - ":inbox", ":inbox", - "Show private messages", "查看私信", - ":inbox", ":inbox", - "Usage: inbox\n", "用法: inbox\n", + I18N_STRING(":inbox", ":inbox"), + I18N_STRING("Show private messages", "查看私信"), + I18N_STRING(":inbox", ":inbox"), + I18N_STRING("Usage: inbox\n", "用法: inbox\n"), 2, true, false }, { {TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}}, - ":nick , :name ", ":nick , :name ", - "Change nickname", "更改昵称", - ":nick ", ":nick ", - "Usage: nick \n", "用法: nick \n", + I18N_STRING(":nick , :name ", + ":nick , :name "), + I18N_STRING("Change nickname", "更改昵称"), + I18N_STRING(":nick ", ":nick "), + I18N_STRING("Usage: nick \n", "用法: nick \n"), 2, false, true }, { {TNT_COMMAND_LAST, "last", {"last", NULL}}, - ":last [N]", ":last [N]", - "Show last N messages (max 50)", "显示最后 N 条消息(最多50)", - ":last [N]", ":last [N]", - "Usage: last [N] (N: 1-50, default 10)\n", - "用法: last [N] (N: 1-50,默认 10)\n", + I18N_STRING(":last [N]", ":last [N]"), + I18N_STRING("Show last N messages (max 50)", + "显示最后 N 条消息(最多50)"), + I18N_STRING(":last [N]", ":last [N]"), + I18N_STRING("Usage: last [N] (N: 1-50, default 10)\n", + "用法: last [N] (N: 1-50,默认 10)\n"), 1, false, false }, { {TNT_COMMAND_SEARCH, "search", {"search", NULL}}, - ":search ", ":search ", - "Search message history", "搜索消息历史", - ":search ", ":search ", - "Usage: search \n", "用法: search \n", + I18N_STRING(":search ", ":search "), + I18N_STRING("Search message history", "搜索消息历史"), + I18N_STRING(":search ", ":search "), + I18N_STRING("Usage: search \n", "用法: search \n"), 1, false, true }, { {TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}}, - ":mute-joins, :mute", ":mute-joins, :mute", - "Toggle join/leave notices", "切换加入/离开提示", - ":mute-joins", ":mute-joins", - "Usage: mute-joins\n", "用法: mute-joins\n", + I18N_STRING(":mute-joins, :mute", ":mute-joins, :mute"), + I18N_STRING("Toggle join/leave notices", "切换加入/离开提示"), + I18N_STRING(":mute-joins", ":mute-joins"), + I18N_STRING("Usage: mute-joins\n", "用法: mute-joins\n"), 3, true, false }, { {TNT_COMMAND_HELP, "help", {"help", NULL}}, - ":help", ":help", - "Show concise manual", "显示简明手册", - NULL, NULL, - "Usage: help\n", "用法: help\n", + I18N_STRING(":help", ":help"), + I18N_STRING("Show concise manual", "显示简明手册"), + I18N_STRING(NULL, NULL), + I18N_STRING("Usage: help\n", "用法: help\n"), 0, true, false }, { {TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}}, - ":lang ", ":lang ", - "Switch UI language", "切换界面语言", - NULL, NULL, - "Usage: lang \n", "用法: lang \n", + I18N_STRING(":lang ", ":lang "), + I18N_STRING("Switch UI language", "切换界面语言"), + I18N_STRING(NULL, NULL), + I18N_STRING("Usage: lang \n", "用法: lang \n"), 0, false, false }, { {TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}}, - ":clear, :cls", ":clear, :cls", - "Clear command output", "清空命令输出", - ":clear", ":clear", - "Usage: clear\n", "用法: clear\n", + I18N_STRING(":clear, :cls", ":clear, :cls"), + I18N_STRING("Clear command output", "清空命令输出"), + I18N_STRING(":clear", ":clear"), + I18N_STRING("Usage: clear\n", "用法: clear\n"), 3, true, false }, { {TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}}, - ":q, :quit, :exit", ":q, :quit, :exit", - "Disconnect", "断开连接", - ":q", ":q", - "Usage: q\n", "用法: q\n", + I18N_STRING(":q, :quit, :exit", ":q, :quit, :exit"), + I18N_STRING("Disconnect", "断开连接"), + I18N_STRING(":q", ":q"), + I18N_STRING("Usage: q\n", "用法: q\n"), 3, true, false } }; @@ -265,10 +265,8 @@ const char *command_catalog_suggest(const char *name) { void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos, ui_lang_t lang) { for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) { - const char *usage = lang == UI_LANG_ZH ? entries[i].full_usage_zh - : entries[i].full_usage_en; - const char *summary = lang == UI_LANG_ZH ? entries[i].summary_zh - : entries[i].summary_en; + const char *usage = i18n_string(entries[i].full_usage, lang); + const char *summary = i18n_string(entries[i].summary, lang); buffer_appendf(buffer, buf_size, pos, " %-40s - %s\n", usage, summary); } @@ -285,9 +283,8 @@ void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos, if (entries[i].manual_group != group) { continue; } - usage = lang == UI_LANG_ZH ? entries[i].manual_usage_zh - : entries[i].manual_usage_en; - if (!usage) { + usage = i18n_string(entries[i].manual_usage, lang); + if (!usage || usage[0] == '\0') { continue; } if (!first) { @@ -309,7 +306,6 @@ void command_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos, return; } - usage = lang == UI_LANG_ZH ? entry->error_usage_zh - : entry->error_usage_en; + usage = i18n_string(entry->error_usage, lang); buffer_appendf(buffer, buf_size, pos, "%s", usage); } diff --git a/src/i18n_text.c b/src/i18n_text.c index 6221972..2664fa6 100644 --- a/src/i18n_text.c +++ b/src/i18n_text.c @@ -1,8 +1,6 @@ #include "i18n.h" -typedef struct { - const char *text[UI_LANG_COUNT]; -} i18n_text_entry_t; +typedef i18n_string_t i18n_text_entry_t; static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = { [I18N_USERNAME_PROMPT] = { @@ -208,16 +206,6 @@ const char *i18n_text(ui_lang_t lang, i18n_text_id_t id) { return ""; } - if ((int)lang < 0 || lang >= UI_LANG_COUNT) { - lang = UI_LANG_EN; - } - const i18n_text_entry_t *entry = &text_catalog[id]; - if (entry->text[lang]) { - return entry->text[lang]; - } - if (entry->text[UI_LANG_EN]) { - return entry->text[UI_LANG_EN]; - } - return ""; + return i18n_string(*entry, lang); } diff --git a/tests/unit/test_command_catalog.c b/tests/unit/test_command_catalog.c index 575759e..3dd67b8 100644 --- a/tests/unit/test_command_catalog.c +++ b/tests/unit/test_command_catalog.c @@ -119,6 +119,12 @@ TEST(generates_localized_usage) { assert(strcmp(en, "Usage: last [N] (N: 1-50, default 10)\n") == 0); assert(strcmp(zh, "用法: msg \n" " w \n") == 0); + + en[0] = '\0'; + en_pos = 0; + command_catalog_append_usage(en, sizeof(en), &en_pos, + TNT_COMMAND_USERS, (ui_lang_t)99); + assert(strcmp(en, "Usage: users\n") == 0); } int main(void) { diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index 4c0af8c..67113c8 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -79,6 +79,12 @@ TEST(default_uses_locale_when_no_tnt_lang) { } TEST(text_lookup_matches_language) { + i18n_string_t sample = I18N_STRING("fallback", "替代"); + + assert(strcmp(i18n_string(sample, UI_LANG_EN), "fallback") == 0); + assert(strcmp(i18n_string(sample, UI_LANG_ZH), "替代") == 0); + assert(strcmp(i18n_string(sample, (ui_lang_t)99), "fallback") == 0); + assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT), "display name") != NULL); assert(strstr(i18n_text(UI_LANG_ZH, I18N_USERNAME_PROMPT),