/* test_i18n.c - 界面文案表 * * 这张表是"每种语言一条". 它最容易出的错不是逻辑, 而是漏: 加了键忘了翻, * 或者译文里少了个占位符 ({path} 写成 {paths})—— 用户看到的就是一句缺字的话. * 所以这里的检查全部是机械的: 遍历表, 逐条比对. * * 文案本身好不好不在这里管; "英文界面是不是每句都有着落"才是. */ #include "i18n.h" #include #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) /* 收一份占位符名单 ({name} 里的 name), 按出现顺序放进 out */ static int placeholders(const char *s, char (*out)[32], int max) { int n = 0; for (const char *p = s; *p; p++) { if (*p != '{') continue; const char *e = strchr(p + 1, '}'); if (!e) break; size_t len = (size_t)(e - p - 1); if (len == 0 || len >= 32 || n >= max) break; memcpy(out[n], p + 1, len); out[n][len] = '\0'; n++; p = e; } return n; } /* 收一份 printf 转换符名单 (%zu、%c、%.20s …), 原样带走标志/宽度/长度. * json.* 那几条是 printf 模板而不是 {name}, 两种语言只要转换符顺序或类型 * 对不上, snprintf 就会读错参数 —— 比少字更糟, 得由机器盯住. */ static int printf_specs(const char *s, char (*out)[16], int max) { int n = 0; for (const char *p = s; p && *p; p++) { if (*p != '%' || n >= max) continue; const char *q = p + 1; while (*q && strchr("-+ #0", *q)) q++; /* 标志 */ while (*q >= '0' && *q <= '9') q++; /* 宽度 */ if (*q == '.') { q++; while (*q >= '0' && *q <= '9') q++; } while (*q == 'h' || *q == 'l' || *q == 'L' || *q == 'z' || *q == 'j' || *q == 't') q++; /* 长度修饰 */ if (strchr("diouxXcspfgeE", *q)) { size_t len = (size_t)(q + 1 - p); if (len < 16) { memcpy(out[n], p, len); out[n][len] = '\0'; n++; } } if (*q) p = q; } return n; } /* 有没有中日韩字符 (含全角标点: 〔〕 ()). 英文那一列里出现它, 基本就是 * "照抄了中文"或者"美化用的框线忘了换" —— 用户看到的是英文句子中间卡着一对 * 中文括号. 顺带把"英文栏里混进日文假名"这种复制粘贴也一并挡住. */ static int has_cjk(const char *s) { for (const unsigned char *p = (const unsigned char *)s; p && *p; p++) { if (p[0] >= 0xE3 && p[0] <= 0xEF && p[1] >= 0x80 && p[2] >= 0x80) { unsigned cp = (unsigned)((p[0] & 0x0Fu) << 12) | (unsigned)((p[1] & 0x3Fu) << 6) | (unsigned)(p[2] & 0x3Fu); if ((cp >= 0x3000 && cp <= 0x30FF) || /* 标点 / 假名 */ (cp >= 0x4E00 && cp <= 0x9FFF) || /* 汉字 */ (cp >= 0xFF00 && cp <= 0xFFEF)) /* 全角 */ return 1; } } return 0; } int main(void) { size_t n = 0; const LigentMsg *tab = i18n_table(&n); CHECK(tab != NULL && n > 0, "文案表不能是空的"); puts("每条两种语言都齐、key 不重复、占位符对得上"); for (size_t i = 0; i < n; i++) { const LigentMsg *m = &tab[i]; CHECK(m->key && *m->key, "第 %zu 条没有 key", i); if (!m->key || !*m->key) continue; CHECK(m->en && *m->en, "%s 缺英文", m->key); CHECK(m->zh && *m->zh, "%s 缺中文", m->key); CHECK(!has_cjk(m->en ? m->en : ""), "%s 的英文里混进了中日韩字符", m->key); /* key 唯一性: 比较是 O(n²), 但每个 key 只记一条检查 —— 写成循环里 * CHECK 的话, check-docs 数出来的"单元项数"就没意义了. */ int dup = 0; for (size_t j = i + 1; j < n; j++) if (strcmp(tab[j].key, m->key) == 0) dup = 1; CHECK(!dup, "key 重复: %s", m->key); char pe[16][32], pz[16][32]; int ne = placeholders(m->en ? m->en : "", pe, 16); int nz = placeholders(m->zh ? m->zh : "", pz, 16); CHECK(ne == nz, "%s 两种语言的占位符数量不同 (en %d / zh %d)", m->key, ne, nz); for (int a = 0; a < ne && a < nz; a++) { int found = 0; for (int b = 0; b < nz; b++) if (strcmp(pe[a], pz[b]) == 0) found = 1; CHECK(found, "%s: 英文的 {%s} 在中文里没有对应", m->key, pe[a]); } /* 译文里多出一个原文没有的占位符, 同样会在运行时漏字 */ for (int a = 0; a < nz && a < ne; a++) { int found = 0; for (int b = 0; b < ne; b++) if (strcmp(pz[a], pe[b]) == 0) found = 1; CHECK(found, "%s: 中文的 {%s} 在英文里没有对应", m->key, pz[a]); } } /* 表里有一部分键是**当 printf 模板用的** (repl.usage.*、repl.model.row、 * json.* …): 调用方直接把 t(key) 交给 printf/snprintf. 这些条目的 * {name} 检查管不到, 得单独比转换符 —— 少一个 %s 会读到别的参数, * 类型写错 (%d 对 %zu)更是直接踩内存. */ puts("printf 模板: 两种语言的转换符必须一模一样"); for (size_t i = 0; i < n; i++) { const LigentMsg *m = &tab[i]; char se[16][16], sz[16][16]; int ce = printf_specs(m->en ? m->en : "", se, 16); int cz = printf_specs(m->zh ? m->zh : "", sz, 16); if (ce == 0 && cz == 0) continue; /* 不是模板, 跳过 */ CHECK(ce == cz, "%s: 转换符个数不同 (en %d / zh %d)", m->key, ce, cz); for (int a = 0; a < ce && a < cz; a++) CHECK(strcmp(se[a], sz[a]) == 0, "%s: 第 %d 个转换符不同 (en %s / zh %s)", m->key, a + 1, se[a], sz[a]); } puts("查表、切换语言、漏键可见"); i18n_set_lang(LIGENT_LANG_ZH); CHECK(strcmp(t("cli.err.unknown_option"), i18n_lookup(LIGENT_LANG_ZH, "cli.err.unknown_option")) == 0, "t() 应取当前语言的那条"); CHECK(i18n_lang() == LIGENT_LANG_ZH, "语言状态"); CHECK(strstr(t("cli.err.unknown_option"), "未知") != NULL, "中文那条"); i18n_set_lang(LIGENT_LANG_EN); CHECK(i18n_lang_name()[0] == 'e', "i18n_lang_name"); CHECK(strstr(t("cli.err.unknown_option"), "unknown") != NULL, "英文那条"); CHECK(strncmp(t("no.such.key"), "[?", 2) == 0, "漏掉的键要显眼地露出来 (实际 %s), 不能变成空白", t("no.such.key")); puts("占位符替换按名字走"); i18n_set_lang(LIGENT_LANG_EN); const char *r = tf("cli.err.unknown_option", "option", "--nope", NULL); CHECK(strstr(r, "--nope") != NULL, "值要填进去"); CHECK(strstr(r, "{option}") == NULL, "填过的占位符不该留下"); CHECK(strstr(tf("cli.err.unknown_option", "wrong", "x", NULL), "{option}") != NULL, "名字对不上时原样保留, 别静默吞掉"); i18n_set_lang(LIGENT_LANG_ZH); CHECK(strstr(tf("cli.err.unknown_option", "option", "选项X", NULL), "选项X") != NULL, "中文模板同样能填"); /* 同一句里多个占位符, 且顺序与调用顺序相反 */ i18n_set_lang(LIGENT_LANG_EN); const char *two = tf("doctor.val.match_bad", "guess", "OpenAI", "owner", "DeepSeek", NULL); CHECK(strstr(two, "DeepSeek") && strstr(two, "OpenAI"), "两个值都要在"); CHECK(strstr(two, "{") == NULL, "不该有没填上的占位符: %s", two); puts("语言名解析"); CHECK(i18n_parse_lang("zh") == LIGENT_LANG_ZH, "zh"); CHECK(i18n_parse_lang("zh_CN.UTF-8") == LIGENT_LANG_ZH, "zh_CN.UTF-8"); CHECK(i18n_parse_lang("zh-Hans") == LIGENT_LANG_ZH, "zh-Hans"); CHECK(i18n_parse_lang("en_US") == LIGENT_LANG_EN, "en_US"); CHECK(i18n_parse_lang("C") == LIGENT_LANG_EN, "C 就是英文"); CHECK(i18n_parse_lang("POSIX") == LIGENT_LANG_EN, "POSIX 也是英文"); CHECK(i18n_parse_lang("fr_FR") < 0, "认不出来的返回 -1"); CHECK(i18n_parse_lang("") < 0, "空串返回 -1"); CHECK(i18n_parse_lang(NULL) < 0, "NULL 返回 -1"); puts("环境探测: LIGENT_LANG > LC_ALL > LC_MESSAGES > LANG, 认不出/没设 -> " "英文"); unsetenv("LIGENT_LANG"); unsetenv("LC_ALL"); unsetenv("LC_MESSAGES"); unsetenv("LANG"); CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "什么都没设 -> 英文"); setenv("LANG", "zh_CN.UTF-8", 1); CHECK(i18n_lang_from_env() == LIGENT_LANG_ZH, "LANG=zh_CN -> 中文"); setenv("LC_MESSAGES", "en_US.UTF-8", 1); CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "LC_MESSAGES 优先于 LANG"); setenv("LC_ALL", "zh_TW", 1); CHECK(i18n_lang_from_env() == LIGENT_LANG_ZH, "LC_ALL 优先于 LC_MESSAGES"); setenv("LIGENT_LANG", "en", 1); CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "LIGENT_LANG 优先级最高"); setenv("LIGENT_LANG", "fr", 1); CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "LIGENT_LANG 认不出时回退英文"); unsetenv("LIGENT_LANG"); unsetenv("LC_ALL"); unsetenv("LC_MESSAGES"); unsetenv("LANG"); CHECK(i18n_lang_from_env() == LIGENT_LANG_EN, "清干净之后仍是英文"); printf("\n%d 项检查, %d 项失败\n", checks, failures); return failures ? 1 : 0; }