diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3ac38ea..25ead24 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -113,6 +113,9 @@ the packaged systemd unit, and release preflight checks that metadata. - The Homebrew formula draft now defines a `brew services` entry that runs the installed `tnt` binary with state under `var/tnt`. +- The i18n helper now supports language-keyed string initializers through + `I18N_STRING_MAP`, so future languages can be added incrementally without + changing every existing two-language string initializer. - 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. diff --git a/docs/Development-Guide.md b/docs/Development-Guide.md index 251f9aa..cd11dbf 100644 --- a/docs/Development-Guide.md +++ b/docs/Development-Guide.md @@ -480,6 +480,10 @@ keys. fragments. - Keep placeholders visible and stable, for example `%s`, `%d`, ``, and ``. + - Use `I18N_STRING(en, zh)` for ordinary two-language entries. Use + `I18N_STRING_MAP(I18N_EN(...), I18N_ZH(...))` when an entry needs + language-keyed initialization so future languages can be added without + changing every existing initializer. - Every new user-facing string needs tests for at least English fallback and Chinese output while this project has two UI languages. @@ -488,7 +492,8 @@ keys. The current `src/i18n_text.c` implementation is a small-project translation table implemented in C, not a full gettext catalog. It is acceptable for two languages because message lookup is already split from language parsing in -`src/i18n.c`, but adding more languages should move toward catalog-like +`src/i18n.c`, and localized strings can now be initialized by language key. +Adding many more languages should still move toward external catalog-like storage instead of adding ad hoc branches for every locale. Relevant conventions: diff --git a/include/i18n.h b/include/i18n.h index 219965b..fc0bd10 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -7,8 +7,12 @@ typedef struct { const char *text[UI_LANG_COUNT]; } i18n_string_t; +#define I18N_LANG_TEXT(lang, value) [lang] = (value) +#define I18N_EN(value) I18N_LANG_TEXT(UI_LANG_EN, value) +#define I18N_ZH(value) I18N_LANG_TEXT(UI_LANG_ZH, value) +#define I18N_STRING_MAP(...) {{ __VA_ARGS__ }} #define I18N_STRING(en_text, zh_text) \ - {{ [UI_LANG_EN] = (en_text), [UI_LANG_ZH] = (zh_text) }} + I18N_STRING_MAP(I18N_EN(en_text), I18N_ZH(zh_text)) typedef enum { I18N_USERNAME_PROMPT, diff --git a/tests/unit/test_i18n.c b/tests/unit/test_i18n.c index 73675ed..838dbff 100644 --- a/tests/unit/test_i18n.c +++ b/tests/unit/test_i18n.c @@ -80,10 +80,21 @@ TEST(default_uses_locale_when_no_tnt_lang) { TEST(text_lookup_matches_language) { i18n_string_t sample = I18N_STRING("fallback", "替代"); + i18n_string_t mapped = I18N_STRING_MAP( + I18N_EN("mapped fallback"), + I18N_ZH("映射替代") + ); + i18n_string_t english_only = I18N_STRING_MAP( + I18N_EN("english only") + ); 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(strcmp(i18n_string(mapped, UI_LANG_EN), "mapped fallback") == 0); + assert(strcmp(i18n_string(mapped, UI_LANG_ZH), "映射替代") == 0); + assert(strcmp(i18n_string(english_only, UI_LANG_ZH), + "english only") == 0); assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT), "display name") != NULL);