mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 05:34:39 +08:00
i18n: share localized string helper
This commit is contained in:
parent
46f5780057
commit
d1d44d0914
6 changed files with 98 additions and 80 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
#include "command_catalog.h"
|
||||
|
||||
#include "i18n.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
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 <user> <message>, :w <user> <message>",
|
||||
":msg <user> <message>, :w <user> <message>",
|
||||
"Send private message", "发送私信",
|
||||
":msg <user> <message>", ":msg <user> <message>",
|
||||
"Usage: msg <user> <message>\n"
|
||||
" w <user> <message>\n",
|
||||
"用法: msg <user> <message>\n"
|
||||
" w <user> <message>\n",
|
||||
I18N_STRING(":msg <user> <message>, :w <user> <message>",
|
||||
":msg <user> <message>, :w <user> <message>"),
|
||||
I18N_STRING("Send private message", "发送私信"),
|
||||
I18N_STRING(":msg <user> <message>", ":msg <user> <message>"),
|
||||
I18N_STRING("Usage: msg <user> <message>\n"
|
||||
" w <user> <message>\n",
|
||||
"用法: msg <user> <message>\n"
|
||||
" w <user> <message>\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>, :name <name>", ":nick <name>, :name <name>",
|
||||
"Change nickname", "更改昵称",
|
||||
":nick <name>", ":nick <name>",
|
||||
"Usage: nick <name>\n", "用法: nick <name>\n",
|
||||
I18N_STRING(":nick <name>, :name <name>",
|
||||
":nick <name>, :name <name>"),
|
||||
I18N_STRING("Change nickname", "更改昵称"),
|
||||
I18N_STRING(":nick <name>", ":nick <name>"),
|
||||
I18N_STRING("Usage: nick <name>\n", "用法: nick <name>\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 <keyword>", ":search <keyword>",
|
||||
"Search message history", "搜索消息历史",
|
||||
":search <keyword>", ":search <keyword>",
|
||||
"Usage: search <keyword>\n", "用法: search <keyword>\n",
|
||||
I18N_STRING(":search <keyword>", ":search <keyword>"),
|
||||
I18N_STRING("Search message history", "搜索消息历史"),
|
||||
I18N_STRING(":search <keyword>", ":search <keyword>"),
|
||||
I18N_STRING("Usage: search <keyword>\n", "用法: search <keyword>\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 <en|zh>", ":lang <en|zh>",
|
||||
"Switch UI language", "切换界面语言",
|
||||
NULL, NULL,
|
||||
"Usage: lang <en|zh>\n", "用法: lang <en|zh>\n",
|
||||
I18N_STRING(":lang <en|zh>", ":lang <en|zh>"),
|
||||
I18N_STRING("Switch UI language", "切换界面语言"),
|
||||
I18N_STRING(NULL, NULL),
|
||||
I18N_STRING("Usage: lang <en|zh>\n", "用法: lang <en|zh>\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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <user> <message>\n"
|
||||
" w <user> <message>\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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue