公开 SDK 前把 SDK/App 硬编码文案外置:新增 meow_str/meow_strf/meow_str_set_locale/ meow_str_locale,按当前 locale 取串(永不 NULL),第三方开发者可做多语言。 - 新增 include/meow_ui_str.h + meow_ui_str.c:取串优先级 = TF 覆盖表 > 内嵌当前语言 (en_us→en 其余→zh) > 内嵌内置默认(zh_cn) > 原样返回 id。内嵌双语表(common.* 21 条 + ime.* 9 条);热路径零 malloc(编译期静态表 + ≤256 静态覆盖数组);纯 libc 宿主可单测。 - locale 存系统设置 settings.conf 的 ui.locale(默认 zh_cn,首次调用惰性读取); meow_str_set_locale 即时切换 + 重载覆盖表 + best-effort 持久化(流式改写保留其它行)。 - TF 覆盖表 MeowData/<pkg>/strings/<locale>.txt(id=text 行,UTF-8,'=' 两侧去空白); 无卡/无文件/无该 id → 回落内嵌。meow_strf 用调用方缓冲(snprintf 语义,重入安全)。 - SDK 控件接入:dialog 默认按钮 OK/CANCEL + INPUT 占位、IME 键面/徽标(空格/确定/拼英/ ?123/清/拼音/(无候选)/中)改走 meow_str;空态/加载态给 App 提供 common.no_data/loading。 - 测试:新增 test_meow_ui_str.c(内嵌回退/TF 覆盖优先级/未知 id 回退/切 locale 即时生效/ 查找正确性/meow_strf,tmp+env 两模式);全量宿主回归(widgets/dispatch/box/anim/scroll/ listvirt/theme/ime_fix/pinyin/overlay/notif/sdk_input_keys/p1p2p3/config/vol/sb_collapse/ notify_*) 0 回归;交叉 zig-mipsel -Wall -Wextra 0 warning。 - 文档:MEOW_APP_SDK.md 新增 §13(字符串资源/i18n,遗留顺延为 §14);docs/APP_DEV_GUIDE.md 新增 §7.11。各宿主 test_*.c 头注释补 meow_ui_str.c 编译依赖。 边界(§13.7):仅 zh_cn/en_us 整串取用;不做复数规则 / BiDi / RTL / 动态字体回退(雏形)。
175 lines
9.1 KiB
C
175 lines
9.1 KiB
C
/*
|
||
* test_meow_ui_str.c — meow_str_* 宿主单测(2026-09-02,§13.i18n 雏形)。
|
||
*
|
||
* 编译(宿主,meow_ui_str 为纯 libc,无需 SDK 其它对象):
|
||
* gcc -Wall -Wextra -o test_meow_ui_str test_meow_ui_str.c meow_ui_str.c -I.
|
||
*
|
||
* 两种模式(覆盖表按 locale 懒加载,分两次跑):
|
||
* ./test_meow_ui_str tmp → unsetenv MEOW_DATA_DIR,仅验证内嵌双语表(无覆盖表)
|
||
* ./test_meow_ui_str env → 设 MEOW_DATA_DIR=<mkdtemp>/data,写 <mkdtemp>/strings/en_us.txt,
|
||
* 验证 TF 覆盖表优先级(覆盖>内嵌)+ locale 即时生效
|
||
*
|
||
* 覆盖矩阵:
|
||
* T1 内嵌回退(默认 zh_cn):common.ok/loading/no_data 等取 zh
|
||
* T2 切 locale 即时生效:set_locale(en_us) → 内嵌 en;set_locale(zh_cn) → 内嵌 zh(无需重初始化)
|
||
* T3 未知 id 回退:meow_str("nope.missing") 原样返回 id;meow_str(NULL) 返回 ""
|
||
* T4 TF 覆盖优先级:override > 内嵌当前语言;未覆盖 id 回落内嵌;自定义 id 命中
|
||
* T5 查找正确性:一批 id 的 zh/en 值逐一断言
|
||
* T6 meow_strf:取串并按 snprintf 语义写调用方缓冲(与 meow_str 一致)
|
||
*
|
||
* 注:set_locale 的 ui.locale 持久化目标是板载 /usr/data/meow/settings.conf,宿主无该目录
|
||
* → 静默失败(运行时仍切换,仅不跨启动);不影响本测试断言。
|
||
*/
|
||
#include "meow_ui.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/stat.h>
|
||
|
||
static int g_fail = 0;
|
||
#define CHECK(cond, msg) do { \
|
||
if (cond) { printf(" PASS: %s\n", msg); } \
|
||
else { printf(" FAIL: %s\n", msg); g_fail = 1; } \
|
||
} while (0)
|
||
|
||
/* 找内嵌某 id 的 zh/en 期望值(供 T5 逐项对照)。不在表内返回 NULL。 */
|
||
static const char *expect(const char *id, int en)
|
||
{
|
||
/* 与 meow_ui_str.c 的 g_embed[] 对齐(此处只列 T5 抽查的子集) */
|
||
static const struct { const char *id; const char *zh; const char *en; } t[] = {
|
||
{ "common.ok", "确定", "OK" },
|
||
{ "common.cancel", "取消", "Cancel" },
|
||
{ "common.back", "返回", "Back" },
|
||
{ "common.confirm", "确认", "Confirm" },
|
||
{ "common.loading", "加载中...", "Loading..." },
|
||
{ "common.no_data", "暂无数据", "No data" },
|
||
{ "common.retry_fail", "重试失败", "Retry failed" },
|
||
{ "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." },
|
||
{ "ime.space", "空格", "Space" },
|
||
{ "ime.enter", "确定", "OK" },
|
||
{ "ime.esc", "取消", "Cancel" },
|
||
{ "ime.clear", "清", "Clear" },
|
||
{ "ime.pinyin_mode", "拼音", "Pinyin" },
|
||
{ "ime.no_candidate", "(无候选)", "(none)" },
|
||
};
|
||
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++)
|
||
if (strcmp(t[i].id, id) == 0) return en ? t[i].en : t[i].zh;
|
||
return NULL;
|
||
}
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
const char *mode = (argc > 1) ? argv[1] : "tmp";
|
||
char *tmpdir = NULL;
|
||
|
||
if (strcmp(mode, "env") == 0) {
|
||
/* 建 <mkdtemp>/data + <mkdtemp>/strings/en_us.txt(meow_str 由 MEOW_DATA_DIR 去尾 /data
|
||
* 得 <mkdtemp>/,再拼 strings/<locale>.txt)。 */
|
||
tmpdir = strdup("/tmp/meowstrtestXXXXXX");
|
||
if (!mkdtemp(tmpdir)) { printf("FATAL: mkdtemp failed\n"); return 2; }
|
||
char datadir[512], strdir[512];
|
||
snprintf(datadir, sizeof datadir, "%s/data", tmpdir);
|
||
snprintf(strdir, sizeof strdir, "%s/strings", tmpdir);
|
||
mkdir(datadir, 0755);
|
||
mkdir(strdir, 0755);
|
||
char file[512];
|
||
snprintf(file, sizeof file, "%s/strings/en_us.txt", tmpdir);
|
||
FILE *f = fopen(file, "w");
|
||
if (!f) { printf("FATAL: cannot write override file\n"); free(tmpdir); return 2; }
|
||
/* 混合:正常行 / '=' 两侧带空格 / 空行 / 无 '=' 行(应被跳过) */
|
||
fprintf(f, "# comment line (no equals)\n");
|
||
fprintf(f, "common.ok=确认OK\n");
|
||
fprintf(f, "common.custom=自定义覆盖\n");
|
||
fprintf(f, "common.loading = 加载中覆盖\n");
|
||
fprintf(f, "badline-without-equals\n");
|
||
fprintf(f, "\n");
|
||
fclose(f);
|
||
setenv("MEOW_DATA_DIR", datadir, 1);
|
||
} else {
|
||
unsetenv("MEOW_DATA_DIR");
|
||
}
|
||
|
||
printf("== meow_str test [%s] ==\n", mode);
|
||
printf(" locale = %s\n", meow_str_locale());
|
||
|
||
/* --- T1 内嵌回退(默认 zh_cn,无覆盖表/无 zh_cn.txt)--- */
|
||
CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T1 zh common.ok = 确定");
|
||
CHECK(strcmp(meow_str("common.cancel"), "取消") == 0, "T1 zh common.cancel = 取消");
|
||
CHECK(strcmp(meow_str("common.loading"), "加载中...") == 0, "T1 zh common.loading = 加载中...");
|
||
CHECK(strcmp(meow_str("common.no_data"), "暂无数据") == 0, "T1 zh common.no_data = 暂无数据");
|
||
CHECK(strcmp(meow_str("ime.space"), "空格") == 0, "T1 zh ime.space = 空格");
|
||
|
||
/* --- T3 未知 id 回退(永不 NULL)--- */
|
||
CHECK(strcmp(meow_str("nope.missing"), "nope.missing") == 0, "T3 未知 id 原样返回 id");
|
||
CHECK(strcmp(meow_str(NULL), "") == 0, "T3 meow_str(NULL) = \"\"");
|
||
|
||
/* --- T6 meow_strf:与 meow_str 一致,snprintf 语义 --- */
|
||
{
|
||
char buf[128];
|
||
int r = meow_strf(buf, sizeof buf, "common.ok");
|
||
CHECK(r == (int)strlen("确定") && strcmp(buf, "确定") == 0, "T6 meow_strf zh common.ok = 确定");
|
||
meow_strf(buf, sizeof buf, "common.no_data");
|
||
CHECK(strcmp(buf, meow_str("common.no_data")) == 0, "T6 meow_strf == meow_str");
|
||
CHECK(meow_strf(NULL, 10, "common.ok") == -1, "T6 meow_strf(out=NULL) = -1");
|
||
CHECK(meow_strf(buf, 0, "common.ok") == -1, "T6 meow_strf(outsz=0) = -1");
|
||
}
|
||
|
||
/* --- T2 切 locale 即时生效(无需重初始化)---
|
||
* 用「未在覆盖表」的 id 证明 locale 即时切换(common.ok 等在 env 会被覆盖,留给 T4 验覆盖路径)。 */
|
||
meow_str_set_locale("en_us");
|
||
CHECK(strcmp(meow_str_locale(), "en_us") == 0, "T2 locale = en_us");
|
||
CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T2 en common.cancel = Cancel");
|
||
CHECK(strcmp(meow_str("common.no_data"), "No data") == 0, "T2 en common.no_data = No data");
|
||
CHECK(strcmp(meow_str("ime.space"), "Space") == 0, "T2 en ime.space = Space");
|
||
|
||
/* --- T4 TF 覆盖优先级(仅 env 模式)--- */
|
||
if (strcmp(mode, "env") == 0) {
|
||
/* ① 覆盖内嵌当前语言(en_us*下 common.ok 应是覆盖值"确认OK"而非内嵌"OK") */
|
||
CHECK(strcmp(meow_str("common.ok"), "确认OK") == 0, "T4 @en override 覆盖 common.ok");
|
||
/* ② 覆盖自定义 id(内嵌无 → 覆盖命中) */
|
||
CHECK(strcmp(meow_str("common.custom"), "自定义覆盖") == 0, "T4 @en override 自定义 id");
|
||
/* ③ '=' 两侧带空格也可解析 */
|
||
CHECK(strcmp(meow_str("common.loading"), "加载中覆盖") == 0, "T4 @en override 带空格行");
|
||
/* ④ 未覆盖 id 回落内嵌当前语言 */
|
||
CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T4 @en 未覆盖 → 内嵌 en");
|
||
CHECK(strcmp(meow_str("ime.clear"), "Clear") == 0, "T4 @en ime.clear 未覆盖 → 内嵌 en");
|
||
/* ⑤ 自定义 id 在 zh(无 zh_cn.txt,覆盖表清空)→ 内嵌无 → 原样返回 id */
|
||
meow_str_set_locale("zh_cn");
|
||
CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T4 @zh 覆盖清除 → 内嵌 zh");
|
||
CHECK(strcmp(meow_str("common.custom"), "common.custom") == 0, "T4 @zh 自定义 id 原样回退");
|
||
meow_str_set_locale("en_us"); /* 回到 en 供 T5 */
|
||
}
|
||
|
||
/* --- T5 查找正确性:一批 id 的 zh/en 逐一断言(仅 tmp 模式——无覆盖干扰,
|
||
* 才能验证内嵌表本身;env 模式的内嵌 vs 覆盖已由 T2/T4 覆盖,此处跳过多余断言)--- */
|
||
if (strcmp(mode, "tmp") == 0) {
|
||
const char *ids[] = { "common.ok", "common.cancel", "common.back", "common.confirm",
|
||
"common.loading", "common.no_data", "common.retry_fail",
|
||
"common.tap_ok_edit", "ime.space", "ime.enter", "ime.esc",
|
||
"ime.clear", "ime.pinyin_mode", "ime.no_candidate" };
|
||
meow_str_set_locale("zh_cn");
|
||
for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) {
|
||
if (expect(ids[i], 0)) {
|
||
char msg[128];
|
||
snprintf(msg, sizeof msg, "T5 zh %s == 期望", ids[i]);
|
||
CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 0)) == 0, msg);
|
||
}
|
||
}
|
||
meow_str_set_locale("en_us");
|
||
for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) {
|
||
if (expect(ids[i], 1)) {
|
||
char msg[128];
|
||
snprintf(msg, sizeof msg, "T5 en %s == 期望", ids[i]);
|
||
CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 1)) == 0, msg);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tmpdir) free(tmpdir);
|
||
if (g_fail) { printf("\nRESULT: FAIL (%d)\n", g_fail); return 1; }
|
||
printf("\nRESULT: ALL PASS\n");
|
||
return 0;
|
||
}
|