Files
ligent/tests/test_i18n.c
T
2026-10-03 14:56:56 +08:00

220 lines
10 KiB
C

/* test_i18n.c - 界面文案表
*
* 这张表是"每种语言一条". 它最容易出的错不是逻辑, 而是漏: 加了键忘了翻,
* 或者译文里少了个占位符 ({path} 写成 {paths})—— 用户看到的就是一句缺字的话.
* 所以这里的检查全部是机械的: 遍历表, 逐条比对.
*
* 文案本身好不好不在这里管; "英文界面是不是每句都有着落"才是.
*/
#include "i18n.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}