Add language-keyed i18n string initializers

This commit is contained in:
m1ngsama 2026-05-28 09:14:36 +08:00
parent 57d0f931b5
commit d893351c5a
4 changed files with 25 additions and 2 deletions

View file

@ -113,6 +113,9 @@
the packaged systemd unit, and release preflight checks that metadata. the packaged systemd unit, and release preflight checks that metadata.
- The Homebrew formula draft now defines a `brew services` entry that runs the - The Homebrew formula draft now defines a `brew services` entry that runs the
installed `tnt` binary with state under `var/tnt`. 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 - 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 locale/code parsing, while `src/i18n_text.c` owns the table-driven text
catalog with coverage checks for every message ID. catalog with coverage checks for every message ID.

View file

@ -480,6 +480,10 @@ keys.
fragments. fragments.
- Keep placeholders visible and stable, for example `%s`, `%d`, - Keep placeholders visible and stable, for example `%s`, `%d`,
`<user>`, and `<message>`. `<user>`, and `<message>`.
- 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 - Every new user-facing string needs tests for at least English fallback
and Chinese output while this project has two UI languages. 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 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 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 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. storage instead of adding ad hoc branches for every locale.
Relevant conventions: Relevant conventions:

View file

@ -7,8 +7,12 @@ typedef struct {
const char *text[UI_LANG_COUNT]; const char *text[UI_LANG_COUNT];
} i18n_string_t; } 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) \ #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 { typedef enum {
I18N_USERNAME_PROMPT, I18N_USERNAME_PROMPT,

View file

@ -80,10 +80,21 @@ 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", "替代"); 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_EN), "fallback") == 0);
assert(strcmp(i18n_string(sample, UI_LANG_ZH), "替代") == 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(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), assert(strstr(i18n_text(UI_LANG_EN, I18N_USERNAME_PROMPT),
"display name") != NULL); "display name") != NULL);