From 8eb311e54b0bdb3d7d11e451a70f89fff99d54a9 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 24 May 2026 11:06:29 +0800 Subject: [PATCH] i18n: restore code-based language syntax --- .gitignore | 1 + docs/CHANGELOG.md | 7 ++++ docs/Development-Guide.md | 68 +++++++++++++++++++++++++++++++++-- src/i18n.c | 5 +-- src/manual_text.c | 44 ++++++++--------------- tests/unit/test_i18n.c | 14 +++++--- tests/unit/test_manual_text.c | 21 +++++++++-- 7 files changed, 117 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index fcd0eeb..96b5d27 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ tests/unit/test_history_view tests/unit/test_i18n tests/unit/test_system_message tests/unit/test_help_text +tests/unit/test_manual_text tests/unit/test_support_text tests/unit/test_cli_text tests/unit/test_ratelimit diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6bc160e..4e6f95e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,13 @@ instead of the removed support entry. - The concise manual module is now named `manual_text`, and the redundant interactive `:commands` entrypoint was removed. +- The concise `:help` manual now stays within one command-output screen so it + does not truncate on normal terminal sizes. +- Language selection is limited to stable codes (`en`, `zh`) and + locale-shaped environment values; natural-language labels are not accepted + as command arguments. +- Documented i18n and user-facing text rules for English-first source text, + stable command syntax, concise help copy, and translation-only localization. ## 1.0.1 - 2026-05-24 - Release candidate hardening diff --git a/docs/Development-Guide.md b/docs/Development-Guide.md index 779cfd8..0bc47ce 100644 --- a/docs/Development-Guide.md +++ b/docs/Development-Guide.md @@ -9,9 +9,10 @@ Complete guide for TNT developers and contributors. 3. [Building and Testing](#building-and-testing) 4. [Core Components](#core-components) 5. [Adding Features](#adding-features) -6. [Debugging](#debugging) -7. [Performance Optimization](#performance-optimization) -8. [Contributing Guidelines](#contributing-guidelines) +6. [User-Facing Text and i18n](#user-facing-text-and-i18n) +7. [Debugging](#debugging) +8. [Performance Optimization](#performance-optimization) +9. [Contributing Guidelines](#contributing-guidelines) --- @@ -395,6 +396,67 @@ case MODE_INSERT: --- +## User-Facing Text and i18n + +TNT should follow Unix/open-source conventions for user-facing text: +English is the source language, command syntax is stable ASCII, and +translations are presentation only. A localized interface must never create +localized command names, localized option names, or localized configuration +keys. + +### Principles + +1. **English-first source text** + - Keep code identifiers, comments, command names, option names, and + documentation source in English. + - Treat English text as the canonical source text for future gettext-style + catalogs. + - Do not use translated text as a programmatic key. + +2. **Stable language identifiers** + - Interactive `:lang` accepts only stable language codes: `en` and `zh`. + - Locale detection may accept locale-shaped values such as + `en_US.UTF-8`, `zh_CN.UTF-8`, `C`, and `POSIX`. + - Do not accept natural-language labels such as `english`, `chinese`, + `中文`, or `英文` as command arguments. + - If regional variants are added later, add explicit locale identifiers + such as `zh_TW` instead of overloading `zh`. + +3. **Concise writing** + - Prefer imperative verbs: "Show", "Switch", "Disconnect". + - Keep command descriptions noun-like or verb-like, not explanatory prose. + - Avoid tutorial language in `:help`; put detailed behavior in `tnt(1)`. + - Keep `:help` within one command-output screen. `?` is the full key + reference. + +4. **One behavior, one name** + - Do not create parallel help commands for the same task. + - Keep `:help` for the concise manual and `?` for the full key reference. + - Keep SSH exec commands small, scriptable, and stable. + +5. **Translation safety** + - Use whole sentences or whole phrases; do not concatenate translated + fragments. + - Keep placeholders visible and stable, for example `%s`, `%d`, + ``, and ``. + - Every new user-facing string needs tests for at least English fallback + and Chinese output while this project has two UI languages. + +### 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. + +Relevant conventions: +- POSIX locale variables: `LANG`, `LC_ALL`, `LC_MESSAGES`. +- GNU gettext source preparation: decent English, whole sentences, and + format placeholders rather than string concatenation. + +--- + ## Debugging ### Enable Verbose SSH Logging diff --git a/src/i18n.c b/src/i18n.c index 70977d0..007f93d 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -41,15 +41,12 @@ bool i18n_try_parse_lang(const char *value, help_lang_t *lang) { return false; } - if (starts_with_lang(value, "zh") || - starts_with_lang(value, "cn") || - starts_with_lang(value, "chinese")) { + if (starts_with_lang(value, "zh")) { if (lang) *lang = LANG_ZH; return true; } if (starts_with_lang(value, "en") || - starts_with_lang(value, "english") || starts_with_lang(value, "c") || starts_with_lang(value, "posix")) { if (lang) *lang = LANG_EN; diff --git a/src/manual_text.c b/src/manual_text.c index cb4f0ab..f62a6e4 100644 --- a/src/manual_text.c +++ b/src/manual_text.c @@ -7,26 +7,18 @@ const char *manual_text_interactive(help_lang_t lang) { "\033[1;37m名称\033[0m\n" " TNT - SSH 终端聊天室\n" "\n" - "\033[1;37m快速开始\033[0m\n" - " 直接输入消息,Enter 发送\n" - " Esc 进入 NORMAL 浏览历史;G 回到最新;i 继续输入\n" - " : 输入命令;q 或 Esc 关闭输出面板\n" + "\033[1;37m使用\033[0m\n" + " 输入消息并 Enter 发送;Esc 浏览历史;G 最新;i 输入\n" + " : 运行命令;? 打开完整按键参考\n" "\n" "\033[1;37m命令\033[0m\n" - " :users 在线用户\n" - " :last [N] 最近消息,默认 10,最多 50\n" - " :search <关键词> 搜索历史\n" - " :msg <用户> <文本> 私聊\n" - " :inbox 私聊收件箱\n" - " :nick <名字> 修改昵称\n" - " :mute-joins 静音/开启进出提示\n" - " :clear 清空命令输出\n" - " :q 断开连接\n" + " :users, :last [N], :search <词>\n" + " :msg <用户> <文本>, :inbox, :nick <名字>\n" + " :mute-joins, :clear, :q\n" "\n" "\033[1;37m语言\033[0m\n" " :lang 显示当前语言\n" - " :lang zh 切换中文\n" - " :lang en 切换英文\n" + " :lang en|zh 切换语言\n" "\n" "\033[1;37m参见\033[0m\n" " ? 完整按键参考\n"; @@ -37,26 +29,18 @@ const char *manual_text_interactive(help_lang_t lang) { "\033[1;37mName\033[0m\n" " TNT - SSH terminal chat room\n" "\n" - "\033[1;37mQuick start\033[0m\n" - " Type a message and press Enter to send\n" - " Esc enters NORMAL history browsing; G jumps latest; i types again\n" - " : enters COMMAND mode; q or Esc closes output panels\n" + "\033[1;37mUse\033[0m\n" + " Type a message and press Enter; Esc browses; G latest; i types\n" + " : runs commands; ? opens the full key reference\n" "\n" "\033[1;37mCommands\033[0m\n" - " :users show online users\n" - " :last [N] show recent messages, default 10, max 50\n" - " :search search history\n" - " :msg whisper privately\n" - " :inbox show whispers\n" - " :nick change nickname\n" - " :mute-joins toggle join/leave notices\n" - " :clear clear command output\n" - " :q disconnect\n" + " :users, :last [N], :search \n" + " :msg , :inbox, :nick \n" + " :mute-joins, :clear, :q\n" "\n" "\033[1;37mLanguage\033[0m\n" " :lang show current language\n" - " :lang en switch to English\n" - " :lang zh switch to Chinese\n" + " :lang en|zh switch language\n" "\n" "\033[1;37mSee also\033[0m\n" " ? full key reference\n"; diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index a7c061e..24deaf6 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -21,14 +21,20 @@ TEST(parse_explicit_languages) { assert(i18n_parse_lang("zh", LANG_EN) == LANG_ZH); assert(i18n_parse_lang("zh_CN.UTF-8", LANG_EN) == LANG_ZH); - assert(i18n_parse_lang("cn", LANG_EN) == LANG_ZH); assert(i18n_parse_lang("en", LANG_ZH) == LANG_EN); assert(i18n_parse_lang("en_US.UTF-8", LANG_ZH) == LANG_EN); + assert(i18n_parse_lang("C", LANG_ZH) == LANG_EN); + assert(i18n_parse_lang("POSIX", LANG_ZH) == LANG_EN); assert(i18n_try_parse_lang("zh", &lang) == true); assert(lang == LANG_ZH); assert(i18n_try_parse_lang("en", &lang) == true); assert(lang == LANG_EN); + assert(i18n_try_parse_lang("cn", &lang) == false); + assert(i18n_try_parse_lang("english", &lang) == false); + assert(i18n_try_parse_lang("chinese", &lang) == false); + assert(i18n_try_parse_lang("中文", &lang) == false); + assert(i18n_try_parse_lang("英文", &lang) == false); assert(i18n_try_parse_lang("fr", &lang) == false); } @@ -44,7 +50,7 @@ TEST(parse_ignores_surrounding_whitespace) { assert(i18n_try_parse_lang(" zh ", &lang) == true); assert(lang == LANG_ZH); assert(i18n_parse_lang("\ten_US.UTF-8\n", LANG_ZH) == LANG_EN); - assert(i18n_parse_lang(" english ", LANG_ZH) == LANG_EN); + assert(i18n_try_parse_lang(" english ", &lang) == false); assert(i18n_try_parse_lang("zh CN", &lang) == false); setenv("TNT_LANG", " zh ", 1); @@ -109,9 +115,9 @@ TEST(text_lookup_matches_language) { assert(strstr(i18n_text(LANG_ZH, I18N_SEARCH_HEADER_FORMAT), "搜索") != NULL); assert(strstr(i18n_text(LANG_EN, I18N_LANG_CURRENT_FORMAT), - "Current language") != NULL); + "lang ") != NULL); assert(strstr(i18n_text(LANG_ZH, I18N_LANG_CURRENT_FORMAT), - "当前语言") != NULL); + "lang ") != NULL); assert(strstr(i18n_text(LANG_EN, I18N_UNKNOWN_COMMAND_FORMAT), "Unknown command") != NULL); assert(strstr(i18n_text(LANG_ZH, I18N_UNKNOWN_COMMAND_FORMAT), diff --git a/tests/unit/test_manual_text.c b/tests/unit/test_manual_text.c index 9309a51..2eff4fc 100644 --- a/tests/unit/test_manual_text.c +++ b/tests/unit/test_manual_text.c @@ -15,23 +15,40 @@ static int tests_passed = 0; +static int count_lines(const char *text) { + int lines = 0; + + while (text && *text) { + if (*text == '\n') { + lines++; + } + text++; + } + + return lines; +} + TEST(interactive_manual_matches_language) { const char *en = manual_text_interactive(LANG_EN); const char *zh = manual_text_interactive(LANG_ZH); assert(strstr(en, "TNT(1) help") != NULL); - assert(strstr(en, "Quick start") != NULL); + assert(strstr(en, "Use") != NULL); assert(strstr(en, "Commands") != NULL); + assert(strstr(en, ":lang en|zh") != NULL); assert(strstr(en, ":mute-joins") != NULL); assert(strstr(en, ":support") == NULL); assert(strstr(en, ":commands") == NULL); + assert(count_lines(en) <= 20); assert(strstr(zh, "TNT(1) 帮助") != NULL); - assert(strstr(zh, "快速开始") != NULL); + assert(strstr(zh, "使用") != NULL); assert(strstr(zh, "命令") != NULL); + assert(strstr(zh, ":lang en|zh") != NULL); assert(strstr(zh, ":mute-joins") != NULL); assert(strstr(zh, ":support") == NULL); assert(strstr(zh, ":commands") == NULL); + assert(count_lines(zh) <= 20); } int main(void) {