mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 10:04: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.
|
module descriptions for the split between language parsing and text lookup.
|
||||||
- `i18n_text` now indexes localized strings by `UI_LANG_*` instead of storing
|
- `i18n_text` now indexes localized strings by `UI_LANG_*` instead of storing
|
||||||
English/Chinese as hard-coded struct fields.
|
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,
|
- Documented i18n and user-facing text rules for English-first source text,
|
||||||
stable command syntax, concise help copy, and translation-only localization.
|
stable command syntax, concise help copy, and translation-only localization.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@
|
||||||
|
|
||||||
#include "common.h"
|
#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 {
|
typedef enum {
|
||||||
I18N_USERNAME_PROMPT,
|
I18N_USERNAME_PROMPT,
|
||||||
I18N_INVALID_USERNAME,
|
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_ui_lang_code(ui_lang_t lang);
|
||||||
const char *i18n_text(ui_lang_t lang, i18n_text_id_t id);
|
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 */
|
#endif /* I18N_H */
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
#include "command_catalog.h"
|
#include "command_catalog.h"
|
||||||
|
|
||||||
|
#include "i18n.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
tnt_command_spec_t spec;
|
tnt_command_spec_t spec;
|
||||||
const char *full_usage_en;
|
i18n_string_t full_usage;
|
||||||
const char *full_usage_zh;
|
i18n_string_t summary;
|
||||||
const char *summary_en;
|
i18n_string_t manual_usage;
|
||||||
const char *summary_zh;
|
i18n_string_t error_usage;
|
||||||
const char *manual_usage_en;
|
|
||||||
const char *manual_usage_zh;
|
|
||||||
const char *error_usage_en;
|
|
||||||
const char *error_usage_zh;
|
|
||||||
int manual_group;
|
int manual_group;
|
||||||
bool no_args;
|
bool no_args;
|
||||||
bool requires_args;
|
bool requires_args;
|
||||||
|
|
@ -20,95 +18,97 @@ typedef struct {
|
||||||
static const command_catalog_entry_t entries[] = {
|
static const command_catalog_entry_t entries[] = {
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}},
|
{TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}},
|
||||||
":users, :list, :who", ":users, :list, :who",
|
I18N_STRING(":users, :list, :who", ":users, :list, :who"),
|
||||||
"Show online users", "显示在线用户",
|
I18N_STRING("Show online users", "显示在线用户"),
|
||||||
":users", ":users",
|
I18N_STRING(":users", ":users"),
|
||||||
"Usage: users\n", "用法: users\n",
|
I18N_STRING("Usage: users\n", "用法: users\n"),
|
||||||
1, true, false
|
1, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}},
|
{TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}},
|
||||||
":msg <user> <message>, :w <user> <message>",
|
I18N_STRING(":msg <user> <message>, :w <user> <message>",
|
||||||
":msg <user> <message>, :w <user> <message>",
|
":msg <user> <message>, :w <user> <message>"),
|
||||||
"Send private message", "发送私信",
|
I18N_STRING("Send private message", "发送私信"),
|
||||||
":msg <user> <message>", ":msg <user> <message>",
|
I18N_STRING(":msg <user> <message>", ":msg <user> <message>"),
|
||||||
"Usage: msg <user> <message>\n"
|
I18N_STRING("Usage: msg <user> <message>\n"
|
||||||
" w <user> <message>\n",
|
" w <user> <message>\n",
|
||||||
"用法: msg <user> <message>\n"
|
"用法: msg <user> <message>\n"
|
||||||
" w <user> <message>\n",
|
" w <user> <message>\n"),
|
||||||
2, false, true
|
2, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}},
|
{TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}},
|
||||||
":inbox", ":inbox",
|
I18N_STRING(":inbox", ":inbox"),
|
||||||
"Show private messages", "查看私信",
|
I18N_STRING("Show private messages", "查看私信"),
|
||||||
":inbox", ":inbox",
|
I18N_STRING(":inbox", ":inbox"),
|
||||||
"Usage: inbox\n", "用法: inbox\n",
|
I18N_STRING("Usage: inbox\n", "用法: inbox\n"),
|
||||||
2, true, false
|
2, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}},
|
{TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}},
|
||||||
":nick <name>, :name <name>", ":nick <name>, :name <name>",
|
I18N_STRING(":nick <name>, :name <name>",
|
||||||
"Change nickname", "更改昵称",
|
":nick <name>, :name <name>"),
|
||||||
":nick <name>", ":nick <name>",
|
I18N_STRING("Change nickname", "更改昵称"),
|
||||||
"Usage: nick <name>\n", "用法: nick <name>\n",
|
I18N_STRING(":nick <name>", ":nick <name>"),
|
||||||
|
I18N_STRING("Usage: nick <name>\n", "用法: nick <name>\n"),
|
||||||
2, false, true
|
2, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_LAST, "last", {"last", NULL}},
|
{TNT_COMMAND_LAST, "last", {"last", NULL}},
|
||||||
":last [N]", ":last [N]",
|
I18N_STRING(":last [N]", ":last [N]"),
|
||||||
"Show last N messages (max 50)", "显示最后 N 条消息(最多50)",
|
I18N_STRING("Show last N messages (max 50)",
|
||||||
":last [N]", ":last [N]",
|
"显示最后 N 条消息(最多50)"),
|
||||||
"Usage: last [N] (N: 1-50, default 10)\n",
|
I18N_STRING(":last [N]", ":last [N]"),
|
||||||
"用法: last [N] (N: 1-50,默认 10)\n",
|
I18N_STRING("Usage: last [N] (N: 1-50, default 10)\n",
|
||||||
|
"用法: last [N] (N: 1-50,默认 10)\n"),
|
||||||
1, false, false
|
1, false, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_SEARCH, "search", {"search", NULL}},
|
{TNT_COMMAND_SEARCH, "search", {"search", NULL}},
|
||||||
":search <keyword>", ":search <keyword>",
|
I18N_STRING(":search <keyword>", ":search <keyword>"),
|
||||||
"Search message history", "搜索消息历史",
|
I18N_STRING("Search message history", "搜索消息历史"),
|
||||||
":search <keyword>", ":search <keyword>",
|
I18N_STRING(":search <keyword>", ":search <keyword>"),
|
||||||
"Usage: search <keyword>\n", "用法: search <keyword>\n",
|
I18N_STRING("Usage: search <keyword>\n", "用法: search <keyword>\n"),
|
||||||
1, false, true
|
1, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}},
|
{TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}},
|
||||||
":mute-joins, :mute", ":mute-joins, :mute",
|
I18N_STRING(":mute-joins, :mute", ":mute-joins, :mute"),
|
||||||
"Toggle join/leave notices", "切换加入/离开提示",
|
I18N_STRING("Toggle join/leave notices", "切换加入/离开提示"),
|
||||||
":mute-joins", ":mute-joins",
|
I18N_STRING(":mute-joins", ":mute-joins"),
|
||||||
"Usage: mute-joins\n", "用法: mute-joins\n",
|
I18N_STRING("Usage: mute-joins\n", "用法: mute-joins\n"),
|
||||||
3, true, false
|
3, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_HELP, "help", {"help", NULL}},
|
{TNT_COMMAND_HELP, "help", {"help", NULL}},
|
||||||
":help", ":help",
|
I18N_STRING(":help", ":help"),
|
||||||
"Show concise manual", "显示简明手册",
|
I18N_STRING("Show concise manual", "显示简明手册"),
|
||||||
NULL, NULL,
|
I18N_STRING(NULL, NULL),
|
||||||
"Usage: help\n", "用法: help\n",
|
I18N_STRING("Usage: help\n", "用法: help\n"),
|
||||||
0, true, false
|
0, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}},
|
{TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}},
|
||||||
":lang <en|zh>", ":lang <en|zh>",
|
I18N_STRING(":lang <en|zh>", ":lang <en|zh>"),
|
||||||
"Switch UI language", "切换界面语言",
|
I18N_STRING("Switch UI language", "切换界面语言"),
|
||||||
NULL, NULL,
|
I18N_STRING(NULL, NULL),
|
||||||
"Usage: lang <en|zh>\n", "用法: lang <en|zh>\n",
|
I18N_STRING("Usage: lang <en|zh>\n", "用法: lang <en|zh>\n"),
|
||||||
0, false, false
|
0, false, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}},
|
{TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}},
|
||||||
":clear, :cls", ":clear, :cls",
|
I18N_STRING(":clear, :cls", ":clear, :cls"),
|
||||||
"Clear command output", "清空命令输出",
|
I18N_STRING("Clear command output", "清空命令输出"),
|
||||||
":clear", ":clear",
|
I18N_STRING(":clear", ":clear"),
|
||||||
"Usage: clear\n", "用法: clear\n",
|
I18N_STRING("Usage: clear\n", "用法: clear\n"),
|
||||||
3, true, false
|
3, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}},
|
{TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}},
|
||||||
":q, :quit, :exit", ":q, :quit, :exit",
|
I18N_STRING(":q, :quit, :exit", ":q, :quit, :exit"),
|
||||||
"Disconnect", "断开连接",
|
I18N_STRING("Disconnect", "断开连接"),
|
||||||
":q", ":q",
|
I18N_STRING(":q", ":q"),
|
||||||
"Usage: q\n", "用法: q\n",
|
I18N_STRING("Usage: q\n", "用法: q\n"),
|
||||||
3, true, false
|
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,
|
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
ui_lang_t lang) {
|
ui_lang_t lang) {
|
||||||
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) {
|
||||||
const char *usage = lang == UI_LANG_ZH ? entries[i].full_usage_zh
|
const char *usage = i18n_string(entries[i].full_usage, lang);
|
||||||
: entries[i].full_usage_en;
|
const char *summary = i18n_string(entries[i].summary, lang);
|
||||||
const char *summary = lang == UI_LANG_ZH ? entries[i].summary_zh
|
|
||||||
: entries[i].summary_en;
|
|
||||||
buffer_appendf(buffer, buf_size, pos, " %-40s - %s\n",
|
buffer_appendf(buffer, buf_size, pos, " %-40s - %s\n",
|
||||||
usage, summary);
|
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) {
|
if (entries[i].manual_group != group) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
usage = lang == UI_LANG_ZH ? entries[i].manual_usage_zh
|
usage = i18n_string(entries[i].manual_usage, lang);
|
||||||
: entries[i].manual_usage_en;
|
if (!usage || usage[0] == '\0') {
|
||||||
if (!usage) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!first) {
|
if (!first) {
|
||||||
|
|
@ -309,7 +306,6 @@ void command_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
usage = lang == UI_LANG_ZH ? entry->error_usage_zh
|
usage = i18n_string(entry->error_usage, lang);
|
||||||
: entry->error_usage_en;
|
|
||||||
buffer_appendf(buffer, buf_size, pos, "%s", usage);
|
buffer_appendf(buffer, buf_size, pos, "%s", usage);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
#include "i18n.h"
|
#include "i18n.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef i18n_string_t i18n_text_entry_t;
|
||||||
const char *text[UI_LANG_COUNT];
|
|
||||||
} i18n_text_entry_t;
|
|
||||||
|
|
||||||
static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = {
|
static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
[I18N_USERNAME_PROMPT] = {
|
[I18N_USERNAME_PROMPT] = {
|
||||||
|
|
@ -208,16 +206,6 @@ const char *i18n_text(ui_lang_t lang, i18n_text_id_t id) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((int)lang < 0 || lang >= UI_LANG_COUNT) {
|
|
||||||
lang = UI_LANG_EN;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n_text_entry_t *entry = &text_catalog[id];
|
const i18n_text_entry_t *entry = &text_catalog[id];
|
||||||
if (entry->text[lang]) {
|
return i18n_string(*entry, lang);
|
||||||
return entry->text[lang];
|
|
||||||
}
|
|
||||||
if (entry->text[UI_LANG_EN]) {
|
|
||||||
return entry->text[UI_LANG_EN];
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,12 @@ TEST(generates_localized_usage) {
|
||||||
assert(strcmp(en, "Usage: last [N] (N: 1-50, default 10)\n") == 0);
|
assert(strcmp(en, "Usage: last [N] (N: 1-50, default 10)\n") == 0);
|
||||||
assert(strcmp(zh, "用法: msg <user> <message>\n"
|
assert(strcmp(zh, "用法: msg <user> <message>\n"
|
||||||
" w <user> <message>\n") == 0);
|
" 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) {
|
int main(void) {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,12 @@ TEST(default_uses_locale_when_no_tnt_lang) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(text_lookup_matches_language) {
|
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),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT),
|
||||||
"display name") != NULL);
|
"display name") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_USERNAME_PROMPT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_USERNAME_PROMPT),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue