220 lines
10 KiB
C
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;
|
||
}
|