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

220 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}