From 54857bc0fd5c3a77116db177473fa285672f6f5b Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 2 Sep 2026 02:37:38 +0800 Subject: [PATCH] =?UTF-8?q?sdk(ui):=20=E5=AD=97=E7=AC=A6=E4=B8=B2=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=A4=96=E7=BD=AE=EF=BC=88i18n=20=E9=9B=8F=E5=BD=A2?= =?UTF-8?q?=20=C2=A713=EF=BC=89=E2=80=94=E2=80=94=20meow=5Fstr=5F*=20+=20?= =?UTF-8?q?=E5=86=85=E5=B5=8C=E5=8F=8C=E8=AF=AD=E8=A1=A8=20+=20TF=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E8=A1=A8=20+=20=E6=8E=A7=E4=BB=B6=E6=8E=A5?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公开 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//strings/.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 / 动态字体回退(雏形)。 --- MEOW_APP_SDK.md | 58 +++++- docs/APP_DEV_GUIDE.md | 9 + patch_v2/sdk/build_sdk.sh | 2 +- patch_v2/sdk/include/meow_ui_str.h | 55 +++++ patch_v2/sdk/meow_ui.h | 2 + patch_v2/sdk/meow_ui_dialog.c | 6 +- patch_v2/sdk/meow_ui_ime.c | 22 +- patch_v2/sdk/meow_ui_str.c | 288 +++++++++++++++++++++++++++ patch_v2/sdk/test_ime_fix.c | 2 +- patch_v2/sdk/test_meow_ui_anim.c | 2 +- patch_v2/sdk/test_meow_ui_box.c | 2 +- patch_v2/sdk/test_meow_ui_dispatch.c | 2 +- patch_v2/sdk/test_meow_ui_listvirt.c | 2 +- patch_v2/sdk/test_meow_ui_scroll.c | 2 +- patch_v2/sdk/test_meow_ui_str.c | 174 ++++++++++++++++ patch_v2/sdk/test_meow_ui_theme.c | 2 +- patch_v2/sdk/test_meow_ui_widgets.c | 2 +- patch_v2/sdk/test_notif_gesture.c | 2 +- patch_v2/sdk/test_overlay_suppress.c | 2 +- patch_v2/sdk/test_p1p2p3_repro.c | 2 +- patch_v2/sdk/test_pinyin.c | 2 +- patch_v2/sdk/test_sdk_fixes.c | 2 +- patch_v2/sdk/test_umbrella.c | 2 + 23 files changed, 615 insertions(+), 29 deletions(-) create mode 100644 patch_v2/sdk/include/meow_ui_str.h create mode 100644 patch_v2/sdk/meow_ui_str.c create mode 100644 patch_v2/sdk/test_meow_ui_str.c diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index d975991..6a225dc 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -29,6 +29,7 @@ | `meow_ui_config.h` | 应用配置读写 | §9.2 | | `meow_ui_font.h` | 字体 | §5.7 卡片字体列表 + §10 字体分层 | | `meow_ui_audio.h` | 独占音频客户端 | §11 | +| `meow_ui_str.h` | 字符串资源/i18n | §13 `meow_str`/`meow_strf`/`meow_str_set_locale` | 其余按需显式 include、**不**随伞头发布的模块头(原样保留、本次未改):`meow_ui_view.h` / `meow_ui_nav.h` / `meow_ui_media.h` / `meow_ui_sys.h` / `meow_ui_vol.h`。`meow_ui_internal.h` 为 SDK 内部件,不随伞头发布。 @@ -1334,7 +1335,62 @@ void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 默认 180ms --- -## 13. 遗留 / 待实现 +## 13. 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加) + +> **性质**:字符串资源外置(i18n 雏形)——公开 SDK 前,SDK/App 的 UI 文案不再硬编码在 C 源码:第三方开发者按系统 locale 取对应语言串,且可不改 SDK 二进制用 TF 卡覆盖/追加。**追加式**:新头 `include/meow_ui_str.h` + 新 `.o` `meow_ui_str.c`;既有 `meow_ui.h` 伞头自动聚合(仅新增 #include),既有 API/控件签名一行未改。 + +### 13.1 API(追加式,纯 libc,宿主 gcc 可直接单测) + +```c +const char *meow_str(const char *id); /* 按当前 locale 取 id 文案;静态串,永不 NULL;未知 id 原样返回 id */ +int meow_strf(char *out, int outsz, const char *id, ...); /* 取串后按 snprintf 语义格式化进 out(自带 NUL);out==NULL/outsz<=0→-1 */ +void meow_str_set_locale(const char *locale); /* 切 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表 + best-effort 持久化 ui.locale */ +const char *meow_str_locale(void); /* 当前生效 locale(缺省 "zh_cn") */ +``` + +> `meow_strf` 用**调用方自带缓冲**(而非静态/线程局部),避免 IME/模态等**重入路径**的缓冲竞态,且与 SDK 既有 `snprintf` 习惯一致(无格式参数时等价深拷贝 `meow_str`)。这是把任务书 `meow_strf(id, ...)` 落地为 `(out, outsz, id, ...)` 的定案(可重入、线程安全优先于少一个参数)。 + +### 13.2 取值优先级(四层,逐层回落,永不返回 NULL) + +1. 当前 locale 的 **TF 覆盖表**:`MeowData//strings/.txt`,`id=text` 行格式 UTF-8,`meow_str_set_locale` 加载进静态数组(≤256 条); +2. 内嵌表**当前语言**(`en_us→en`,其余→`zh`); +3. 内嵌表**内置默认**(`zh_cn`,locale 缺省/未知语言回落); +4. 仍不可得 → **原样返回 `id`**(调用方可展示 id/打点,绝不见 NULL)。 + +> 无卡/无覆盖文件/无该 id → 自动回落**内嵌双语默认表**(编译进二进制,必可用)。内嵌表含 `common.*`(确定/取消/返回/确认/完成/保存/删除/编辑/搜索/输入/请输入/请选择/加载中/暂无数据/重试/失败/重试失败/关闭/未知/设置/点击确定编辑,共 21 条)+ `ime.*`(空格/确定/取消/拼英/?123/清/拼音/(无候选)/中,共 9 条),`zh_cn`+`en_us` 双语。 + +### 13.3 ID 规范 + +`域.名`(`common.ok` / `common.cancel` / `player.pause`)。SDK 自带 `common.*` 通用组 + `ime.*` IME 键面组;第三方可自建 `域`(如 `player.*`),经 TF 覆盖表追加,不改 SDK 二进制。 + +### 13.4 locale 存系统设置 + +locale 权威值 = 板载 `settings.conf` 的 `ui.locale`(默认 `zh_cn`)。SDK 首次调用**惰性读取**;`meow_str_set_locale` 即时切换(运行时)并 **best-effort 持久化**回 `ui.locale`(流式改写、保留其它行——绝不 O_TRUNC 整文件;失败静默,切换仍生效)。SDK 与 launcher 系统组件读同一份,后续系统 UI 复用同键即当前语言。 + +### 13.5 性能 + +**启动时一次加载**到静态线性表:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组(≤256 条)。`meow_str` 只做 O(n) 线性扫描(表小可接受),**热路径零 malloc**(无堆申请、无引用计数)。 + +### 13.6 SDK 控件接入 + +- **dialog**(§5.8.6):默认按钮 `"CANCEL"/"OK"` → `meow_str("common.cancel"/"common.ok")`;INPUT 预览占位 `"(tap OK to edit...)"` → `meow_str("common.tap_ok_edit")`。 +- **IME**(§5.1):拼音键面/徽标(空格/确定/拼英/?123/清/拼音/(无候选))与 ASCII/NUM 顶条「中」→ `meow_str("ime.*")`。 +- **空态/加载态**:SDK 不硬编码空态文案;`common.loading`/`common.no_data` 供 App 空态/加载中直接用(如 `meow_listview_set_empty_text(w, meow_str("common.no_data"))`)。 +- **launcher 等系统组件**:后续经同框架接入(本任务只管 SDK 侧;launcher 复用同 `settings.conf ui.locale`)。 + +### 13.7 边界(明示不做,写清理由) + +- **不做复数规则**:需占位符格式化协议 + 每语言复数表,超出「按 locale 取整串」的最小必要范围; +- **不做 BiDi / RTL / 动态字体回退**:需文本整形 + 布局镜像 + 字体回退策略,面向 480×720 单行/短文案场景暂不需要; +- 当前形态为 i18n **雏形**,复杂变体(复数/语言方向/字体重载)留后续批次;locale 仅 `zh_cn`/`en_us` 双语(任意串作覆盖表文件名,内嵌按非 `en` 视为内置默认)。 + +### 13.8 同步重编注意 + +`build_sdk.sh` OBJS 新增 `meow_ui_str`;`meow_ui_str.o`(含内嵌表)+ `meow_ui_dialog.o`/`meow_ui_ime.o` 重编 → **链 `meow_ui.a` 的 App 需重链**(一键 `build_sdk.sh`)。既有 App **不改源码**即零回归(仅按钮/键面文案随 locale 变化)。宿主回归测试需在编译命令补 `meow_ui_str.c`(各 test_*.c 头注释已同步)。 + +--- + +## 14. 遗留 / 待实现 1. `meow_ui` 渲染后端需从 launcher 里**抽公共绘制实现**(`back_buf`/`meow_dirty_*`/`meow_present`/`meow_font_bind` 目前是 launcher 静态函数——需提取为可链接的 `meow_ui_gfx` 或改为共享 obj,注意 launcher 与 app 是**不同进程**,不能共享同一 back_buf;`meow_ui.a` 是**独立库**,内部自建 fb 上下文,不复用 launcher 的全局 `g_dirty`)。 2. 输入线程优先级:vs 主循环 10ms 热键轮询的竞态,需在 B1 实现时定义清楚(应用进程内独立事件循环,不与 launcher 抢)。 diff --git a/docs/APP_DEV_GUIDE.md b/docs/APP_DEV_GUIDE.md index 575a41c..953ce00 100644 --- a/docs/APP_DEV_GUIDE.md +++ b/docs/APP_DEV_GUIDE.md @@ -424,6 +424,15 @@ am resolve # 路由决策(返回 CHAIN) - App 一般不需要联网;确需联网时**克制请求**(见 §8 风控式节制)。 - 系统层面(`netmgrd` 期 0/1):无 iptables 用户态 / 无 cgroup / 无 conntrack → **内核级每应用管控不可行**;主杠杆 = DNS 代理(域名级)+ 连接普查(inet_diag)+ **SDK 自愿通道**。App 走 SDK 自愿通道配合管控是后续方向;当前 App 如联网,请自律、尊重断网降级。 +### 7.11 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加) + +- **任何 UI 文案都不要硬编码**,用 `meow_str("域.名")` 按当前语言取串(示例:`meow_str("common.ok")`)。`meow_str` 永不返回 `NULL`:未知 id 原样返回 `id`,方便你打点/兜底展示。 +- **`meow_strf(out, outsz, id, ...)`**:取串后按 `snprintf` 语义格式化进调用方缓冲(自带 NUL;`out==NULL`/`outsz<=0` 返回 `-1`)。 +- **切换语言**:`meow_str_set_locale("zh_cn"|"en_us")` 即时生效;locale 存系统设置 `settings.conf` 的 `ui.locale`(默认 `zh_cn`,SDK 启动惰性读取)。App 里做语言切换页时调用它即可。 +- **覆盖/追加**:可不改二进制在 TF 卡放 `MeowData//strings/.txt`(每行 `id=text`,UTF-8)覆盖内嵌或新增自己的串(如 `player.pause=暂停`);无卡/无文件/无该 id → 回落内嵌双语默认表(`common.*` + `ime.*` 内置,见 `include/meow_ui_str.h`)。 +- **id 规范**:`域.名`;SDK 自带 `common.*`/`ime.*`,你自建 `域`(如 `player.*`)避免撞名。 +- **边界**:当前为 i18n 雏形,仅 `zh_cn`/`en_us` 双语**整串**取用;**不做复数规则 / BiDi / RTL / 动态字体回退**(详见 `MEOW_APP_SDK.md §13.7`)。 + --- ## 8. 规范与红线 diff --git a/patch_v2/sdk/build_sdk.sh b/patch_v2/sdk/build_sdk.sh index 7253c20..b1ded10 100755 --- a/patch_v2/sdk/build_sdk.sh +++ b/patch_v2/sdk/build_sdk.sh @@ -10,7 +10,7 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架 rm -f meow_ui.a *.o # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) -OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_ui_box meow_ui_anim meow_ui_scroll" +OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str" CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" diff --git a/patch_v2/sdk/include/meow_ui_str.h b/patch_v2/sdk/include/meow_ui_str.h new file mode 100644 index 0000000..2ce33e3 --- /dev/null +++ b/patch_v2/sdk/include/meow_ui_str.h @@ -0,0 +1,55 @@ +/* + * meow_ui_str.h — SDK 字符串资源(i18n 雏形,§13.meow_str_*)。 + * + * 定位:公开 SDK 前,系统/App UI 文案不得再硬编码在 C 源码里——第三方开发者必须能按 + * 系统 locale 拿到对应语言文案,且能在不改 SDK 二进制的情况下用 TF 卡覆盖/追加。 + * 本头声明字符串资源框架的**对外 API**(追加式,既有 App 零改动)。 + * + * 单表双语内置(zh_cn = 内置默认 / en_us),四层取值优先级(见 meow_ui_str.c): + * meow_str(id):按当前 locale 取串;override 覆盖表 > 内嵌当前语言 > 内嵌内置默认(zh_cn); + * 全程不可得则原样返回 id(调用方永不见 NULL)。 + * meow_strf(out,outsz,id,...):取串后按 snprintf 语义格式化进调用方缓冲(自带 NUL 终止)。 + * meow_str_set_locale(locale):切当前 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表 + * + best-effort 持久化到板载 settings.conf 的 ui.locale。 + * + * 依赖:纯 libc(stdio/stdlib/string/stdarg),宿主 gcc 可直接编译单测;不依赖其它 SDK 对象。 + * - locale 初值:启动首次调用惰性读系统设置 settings.conf 的 `ui.locale`,缺省 "zh_cn"。 + * - TF 覆盖表:`/../strings/.txt`(即 MeowData//strings/,见 + * §9.2 数据目录约定),`id=text` 行格式 UTF-8;无卡/无文件/不含该 id → 内嵌表兜底。 + * - 热路径(meow_str)零 malloc:内嵌为编译期静态表,覆盖表为启动加载的静态数组。 + * + * 边界(§13 明示不做,写清理由): + * 不做复数规则 / BiDi / RTL / 动态字体回退——当前形态是「按 locale 取整串」的 i18n 雏形, + * 面向 480×720 单行/短文案 UI;复数变体需占位符格式化协议,BiDi/RTL 需文本整形 + 布局镜像, + * 两者都超出「字符串资源外置」的最小必要范围,留待后续批次。 + */ +#ifndef MEOW_UI_STR_H +#define MEOW_UI_STR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* 按当前 locale 取 id 对应的文案。id 形如 `域.名`(common.ok / player.pause)。 + * 返回静态字符串(内嵌表或覆盖表;永不返回 NULL)。未知 id 原样返回 id。 */ +const char *meow_str(const char *id); + +/* 取 id 文案后按 snprintf 语义格式化进 out(outsz 为缓冲字节数,含 NUL)。 + * 返回 snprintf 样式的「应写入字符数」(不截断);out==NULL 或 outsz<=0 返回 -1。 + * 设计理由:调用方自带缓冲,避免静态/线程局部缓冲在 IME/模态等重入路径的竞态, + * 且与 SDK 既有 snprintf 习惯一致(无格式参数时等价深拷贝 meow_str 结果)。 */ +int meow_strf(char *out, int outsz, const char *id, ...); + +/* 设当前 locale(识别 "zh_cn"|"en_us";任意串作为覆盖表文件名 + 内嵌按非 en 处理为内置默认)。 + * 即时生效(下次 meow_str 即按新 locale);重载该 locale 的 TF 覆盖表; + * best-effort 持久化到 settings.conf 的 ui.locale(失败静默,运行时仍切换)。 + * 须在 App 主线程(UI 线程)调用,避免与 meow_str 并发写覆盖表。 */ +void meow_str_set_locale(const char *locale); + +/* 当前生效 locale(内部缓冲;未初始化时读 settings 后返回,默认 "zh_cn")。 */ +const char *meow_str_locale(void); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_UI_STR_H */ diff --git a/patch_v2/sdk/meow_ui.h b/patch_v2/sdk/meow_ui.h index f792d70..0e9cd19 100644 --- a/patch_v2/sdk/meow_ui.h +++ b/patch_v2/sdk/meow_ui.h @@ -37,6 +37,7 @@ * include/meow_ui_box.h 线性布局引擎(§12.1:meow_box,固定/weight/wrap/对齐/嵌套) * include/meow_ui_anim.h 动画基座+UI 节拍器(§12.2:tween/easing/池+脏区联动) * include/meow_ui_scroll.h 滚动物理(§12.3:速度跟踪/fling/边界回弹/snap) + * include/meow_ui_str.h 字符串资源/i18n(§13:meow_str/meow_strf/meow_str_set_locale) * * 其余模块头(非伞头、App 按需显式 include,均保持不动): * meow_ui_view.h / meow_ui_nav.h / meow_ui_media.h / meow_ui_sys.h / meow_ui_vol.h @@ -69,5 +70,6 @@ #include "include/meow_ui_box.h" #include "include/meow_ui_anim.h" #include "include/meow_ui_scroll.h" +#include "include/meow_ui_str.h" #endif /* MEOW_UI_H */ diff --git a/patch_v2/sdk/meow_ui_dialog.c b/patch_v2/sdk/meow_ui_dialog.c index 4fa60ff..5987bf9 100644 --- a/patch_v2/sdk/meow_ui_dialog.c +++ b/patch_v2/sdk/meow_ui_dialog.c @@ -125,7 +125,7 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g) int y = g->row_y; if (d->type == MEOW_DLG_INPUT) { char buf[300]; - snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : "(tap OK to edit...)"); + snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : meow_str("common.tap_ok_edit")); meow_ui_text(g->dx + pad, y, buf, th->font_body, th->onSurfaceVariant); } else { for (int i = 0; i < nmsg; i++) { @@ -138,10 +138,10 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g) int bx = g->dx + pad, cx = bx + g->btn_w + 12, by = g->btn_y; meow_ui_draw_rect_round(bx, by, g->btn_w, g->btn_h, 10, th->primary); meow_ui_text_center(bx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2, - "CANCEL", th->font_body, th->primary); + meow_str("common.cancel"), th->font_body, th->primary); meow_gfx_fill_round(cx, by, g->btn_w, g->btn_h, 10, th->primary); meow_ui_text_center(cx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2, - "OK", th->font_body, th->onPrimary); + meow_str("common.ok"), th->font_body, th->onPrimary); } } diff --git a/patch_v2/sdk/meow_ui_ime.c b/patch_v2/sdk/meow_ui_ime.c index b28ad6e..2de7979 100644 --- a/patch_v2/sdk/meow_ui_ime.c +++ b/patch_v2/sdk/meow_ui_ime.c @@ -362,10 +362,10 @@ static const char *py_key_label(const struct kbkey *k, char *tmp) switch (k->kind) { case K_CH: { char c = (char)tolower((unsigned char)k->ch); tmp[0] = c; tmp[1] = 0; return tmp; } case K_BACK: return "⌫"; - case K_SPACE: return "空格"; - case K_ENTER: return "确定"; - case K_SWITCH: return "拼/英"; - case K_SYMBOL: return "?123"; + case K_SPACE: return meow_str("ime.space"); + case K_ENTER: return meow_str("ime.enter"); + case K_SWITCH: return meow_str("ime.switch"); + case K_SYMBOL: return meow_str("ime.symbol"); default: return ""; } } @@ -398,9 +398,9 @@ static void meow_ime_draw_py(const meow_theme *th) snprintf(pybuf, sizeof pybuf, "%s_", g_py_buf); meow_ui_text(14, PY_COMBO_Y + 8, pybuf, th->font_body, th->onSurface); meow_gfx_fill_round(PY_CLEAR_X, PY_COMBO_Y + 4, PY_CLEAR_W, PY_BADGE_H, 8, th->secondaryContainer); - meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, "清", 20, th->onSecondaryContainer); + meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, meow_str("ime.clear"), 20, th->onSecondaryContainer); meow_gfx_fill_round(PY_BADGE_X, PY_BADGE_Y, PY_BADGE_W, PY_BADGE_H, 8, th->primaryContainer); - meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, "拼音", 20, th->onPrimaryContainer); + meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, meow_str("ime.pinyin_mode"), 20, th->onPrimaryContainer); /* 候选栏:◀ ▶ 翻页 + 横排候选(高亮第 0 项) */ meow_ui_fill_rect(0, PY_CAND_Y, 480, PY_CAND_H, th->surfaceContainerHighest); @@ -409,7 +409,7 @@ static void meow_ime_draw_py(const meow_theme *th) meow_gfx_fill_round(PY_NEXT_X, PY_CAND_Y, PY_NEXT_W, PY_CAND_H, 8, th->surfaceContainerLow); meow_ui_text_center(PY_NEXT_X + PY_NEXT_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "▶", 20, th->onSurface); if (g_py_nc == 0) { - meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, "(无候选)", th->font_body, th->onSurfaceVariant); + meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, meow_str("ime.no_candidate"), th->font_body, th->onSurfaceVariant); } else { int x = PY_CAND_X0; for (int i = 0; i < PY_PAGE; i++) { @@ -458,7 +458,7 @@ void meow_ime_draw(void) { int th20 = meow_ui_text_height(20); meow_gfx_fill_round(ASCII_MID_X, KB_TOP + 7, ASCII_MID_W, 28, 8, th->primaryContainer); - meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, "中", 20, th->onPrimaryContainer); + meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, meow_str("ime.to_pinyin"), 20, th->onPrimaryContainer); } /* 键 */ for (int i = 0; i < g_nkeys; i++) { @@ -471,9 +471,9 @@ void meow_ime_draw(void) char label[2] = { k->ch, 0 }; if (is_letter(k->ch)) label[0] = g_shift ? (char)toupper((unsigned char)k->ch) : (char)tolower((unsigned char)k->ch); - if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "SPACE", th->font_body, fg); } - else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "OK", 20, fg); } - else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "X", 20, fg); } + if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.space"), th->font_body, fg); } + else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.enter"), 20, fg); } + else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.esc"), 20, fg); } else { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); } } } diff --git a/patch_v2/sdk/meow_ui_str.c b/patch_v2/sdk/meow_ui_str.c new file mode 100644 index 0000000..0f606ac --- /dev/null +++ b/patch_v2/sdk/meow_ui_str.c @@ -0,0 +1,288 @@ +/* + * meow_ui_str.c — SDK 字符串资源框架(i18n 雏形,§13.meow_str_*)。 + * + * 背景:SDK/系统 UI 文案此前全部硬编码在 C 源码(dialog 的 "OK/CANCEL"、IME 的 "空格/确定/ + * 拼音/(无候选)" 等),第三方开发者无法按语言取串,公开 SDK 前必须外置。本 TU 提供 + * 「按 locale 取整串」的最小框架:内嵌双语默认表(编译进 meow_ui.a,必可用)+ TF 卡覆盖表 + * (MeowData//strings/.txt,开发者可加壳不改二进制)+ locale 存系统设置 + * settings.conf 的 `ui.locale`(默认 zh_cn)。 + * + * 取值优先级(meow_str,四层,逐层回落,永不返回 NULL): + * ① 当前 locale 的 TF 覆盖表(meow_ui_str_set_locale 加载,静态数组,≤256 条) + * ② 内嵌表中当前语言(en_us→en,其余→zh) + * ③ 内嵌表中内置默认(zh_cn,即 locale 缺省/未知语言时回退) + * ④ 仍不可得 → 原样返回 id(调用方可展示 id / 打点,绝不见 NULL) + * + * 设计要点: + * - 零 malloc 热路径:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组, + * meow_str 只做线性扫描(表 ≤256,O(n) 可接受),不申请任何堆。 + * - 纯 libc:不依赖其它 SDK TU,宿主 gcc 直接可单测(同 meow_ui_config.c / meow_ui_vol.c)。 + * - locale 初值惰性读 settings.conf ui.locale(默认 zh_cn);meow_str_set_locale 即时生效。 + * - 覆盖表解析兼容 "key = value"('=' 两侧去空白),忽略空行/无 '=' 行;UTF-8 原样透传。 + * - 幂等:settings 只读一次;覆盖表按需加载。locale 修改须在 App 主线程(防并发写覆盖表)。 + * + * 边界(§13 明示不做):无复数规则/BiDi/RTL/动态字体回退,理由见 meow_ui_str.h。 + */ +#include "meow_ui.h" + +#include +#include +#include +#include + +#define MEOW_STR_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 板载系统设置(与 theme/vol 同源) */ +#define MEOW_STR_LOCALE_MAX 16 +#define MEOW_STR_OVERRIDE_MAX 256 /* 覆盖表上限(§13 性能约束 ≤256) */ +#define MEOW_STR_OVR_ID_MAX 64 +#define MEOW_STR_OVR_TEXT_MAX 128 +#define MEOW_STR_PATH_MAX 384 + +/* ---- 内嵌双语默认表(编译进二进制,必可用;zh = 内置默认)---- + * id 规范:`域.名`(common.* 为 SDK 自带通用组;ime.* 为 SDK 自带 IME 键面组; + * 第三方 App 可自建 域(如 player.pause)经 TF 覆盖表追加,不改 SDK 二进制)。 */ +typedef struct meow_str_embed { + const char *id; + const char *zh; + const char *en; +} meow_str_embed; + +static const meow_str_embed g_embed[] = { + /* common.*:通用 UI(dialog 默认按钮 / 空态占位可复用) */ + { "common.ok", "确定", "OK" }, + { "common.cancel", "取消", "Cancel" }, + { "common.back", "返回", "Back" }, + { "common.confirm", "确认", "Confirm" }, + { "common.done", "完成", "Done" }, + { "common.save", "保存", "Save" }, + { "common.delete", "删除", "Delete" }, + { "common.edit", "编辑", "Edit" }, + { "common.search", "搜索", "Search" }, + { "common.input", "输入", "Input" }, + { "common.please_input", "请输入", "Please input" }, + { "common.select", "请选择", "Select" }, + { "common.loading", "加载中...", "Loading..." }, + { "common.no_data", "暂无数据", "No data" }, + { "common.retry", "重试", "Retry" }, + { "common.failed", "失败", "Failed" }, + { "common.retry_fail", "重试失败", "Retry failed" }, + { "common.close", "关闭", "Close" }, + { "common.unknown", "未知", "Unknown" }, + { "common.settings", "设置", "Settings" }, + { "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." }, + /* ime.*:SDK 内置 IME 键面/徽标(meow_ui_ime.c 接入) */ + { "ime.space", "空格", "Space" }, + { "ime.enter", "确定", "OK" }, + { "ime.esc", "取消", "Cancel" }, + { "ime.switch", "拼/英", "PY/EN" }, + { "ime.symbol", "?123", "?123" }, + { "ime.clear", "清", "Clear" }, + { "ime.pinyin_mode", "拼音", "Pinyin" }, + { "ime.no_candidate", "(无候选)", "(none)" }, + { "ime.to_pinyin", "中", "中" }, +}; +#define MEOW_STR_EMBED_N (sizeof(g_embed) / sizeof(g_embed[0])) + +/* ---- 运行时状态(静态,零 malloc)---- */ +static char g_locale[MEOW_STR_LOCALE_MAX] = "zh_cn"; +static int g_locale_loaded = 0; /* 1 = 已从 settings 读过一次 */ +static int g_override_loaded = 0; /* 1 = 覆盖表已按当前 locale 加载 */ + +/* 覆盖表(当前 locale):线性静态数组(id/text 各自定长),≤ MEOW_STR_OVERRIDE_MAX。 */ +static char g_ov_id[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_ID_MAX]; +static char g_ov_text[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_TEXT_MAX]; +static int g_ov_n = 0; + +/* ---- settings.conf 读(同 meow_ui_theme.c get_setting_str 口径:'=' 切分,精确匹配 key)---- + * 返回 0=命中并把值拷入 buf;-1=未命中/文件不可读/超长。 */ +static int str_setting_get(const char *key, char *buf, int buflen) +{ + FILE *f = fopen(MEOW_STR_SETTINGS_FILE, "r"); + if (!f || buflen <= 0) return -1; + char line[160]; + int kl = (int)strlen(key); + int found = 0; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = (int)(eq - k); + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; + int m = 0; + while (*v && *v != '\n' && *v != '\r' && m < buflen - 1) buf[m++] = *v++; + while (m > 0 && (buf[m-1] == ' ' || buf[m-1] == '\t')) m--; + buf[m] = 0; + found = 1; + break; + } + fclose(f); + return found ? 0 : -1; +} + +/* ---- settings.conf 写一个字符串键(best-effort):流式拷贝到 .tmp,命中键行改写为 + * "key = value"(launcher 落盘格式),键缺失则末尾追加;rename 原子替换。保留其余注释/设置, + * 绝不 O_TRUNC 整文件(同 meow_ui_vol_set 思路)。失败静默返回 -1(读不改状态、不崩)。 */ +static int str_setting_set(const char *key, const char *val) +{ + if (!key || !key[0] || !val) return -1; + FILE *in = fopen(MEOW_STR_SETTINGS_FILE, "r"); + char tmp[MEOW_STR_PATH_MAX]; + int kl = (int)strlen(key); + snprintf(tmp, sizeof tmp, "%s.tmp", MEOW_STR_SETTINGS_FILE); + FILE *out = fopen(tmp, "w"); + if (!out) { if (in) fclose(in); return -1; } + + int found = 0; + char line[256]; + while (in && fgets(line, sizeof line, in)) { + char *eq = strchr(line, '='); + char *k = line; + while (*k == ' ' || *k == '\t') k++; + int n = eq ? (int)(eq - k) : 0; + while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; + if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) { + fprintf(out, "%s = %s\n", key, val); + found = 1; + } else { + fputs(line, out); + } + } + if (in) fclose(in); + if (!found) fprintf(out, "%s = %s\n", key, val); + fclose(out); + + if (rename(tmp, MEOW_STR_SETTINGS_FILE) != 0) { remove(tmp); return -1; } + return 0; +} + +/* ---- TF 覆盖表路径:/strings/.txt(= MeowData//strings/)。 + * 无 MEOW_DATA_DIR(手动跑/宿主/无卡)→ path 置空(覆盖表不加载,内嵌兜底)。返回 path 是否有效。 */ +static int str_override_path(char *path, int n) +{ + const char *env = getenv("MEOW_DATA_DIR"); + if (!env || !env[0] || n <= 0) { if (n > 0) path[0] = 0; return 0; } + char base[MEOW_STR_PATH_MAX]; + snprintf(base, sizeof base, "%s", env); + size_t bn = strlen(base); + /* launcher 注入的 MEOW_DATA_DIR = /data;去尾 "/data" 回到 ,再拼 strings/。 */ + if (bn >= 5 && strcmp(base + bn - 5, "/data") == 0) base[bn - 5] = 0; + if (snprintf(path, (size_t)n, "%s/strings/%s.txt", base, g_locale) >= n) { path[0] = 0; return 0; } + return path[0] != 0; +} + +/* 解析覆盖表(`id=text` 行,'=' 两侧去空白;空行/无 '=' 行跳过;UTF-8 原样透传)。 + * 覆盖优先级:同名覆盖内嵌,未覆盖该 id 则回退内嵌。 */ +static void str_load_override(void) +{ + char path[MEOW_STR_PATH_MAX]; + g_ov_n = 0; + if (!str_override_path(path, sizeof path)) return; + + FILE *f = fopen(path, "r"); + if (!f) return; + char line[MEOW_STR_OVR_TEXT_MAX + 16]; + while (fgets(line, sizeof line, f) && g_ov_n < MEOW_STR_OVERRIDE_MAX) { + char *eq = strchr(line, '='); + if (!eq) continue; /* 注释/空行/非 key=value */ + char *k = line; + while (*k == ' ' || *k == '\t') k++; /* id 左去空白 */ + int klen = (int)(eq - k); + while (klen > 0 && (k[klen-1] == ' ' || k[klen-1] == '\t')) klen--; + if (klen <= 0 || klen >= MEOW_STR_OVR_ID_MAX) continue; + const char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; /* text 左去空白 */ + int vlen = 0; + while (v[vlen] && v[vlen] != '\n' && v[vlen] != '\r' && vlen < MEOW_STR_OVR_TEXT_MAX - 1) + vlen++; + while (vlen > 0 && (v[vlen-1] == ' ' || v[vlen-1] == '\t')) vlen--; /* text 右去空白 */ + if (vlen <= 0) continue; + + memcpy(g_ov_id[g_ov_n], k, (size_t)klen); + g_ov_id[g_ov_n][klen] = 0; + memcpy(g_ov_text[g_ov_n], v, (size_t)vlen); + g_ov_text[g_ov_n][vlen] = 0; + g_ov_n++; + } + fclose(f); +} + +/* 惰性初值:首次调用读 settings.conf ui.locale(缺省 "zh_cn")。幂等(只读一次)。 */ +static void str_init_locale(void) +{ + if (g_locale_loaded) return; + char buf[MEOW_STR_LOCALE_MAX]; + if (str_setting_get("ui.locale", buf, sizeof buf) == 0 && buf[0]) + snprintf(g_locale, sizeof g_locale, "%s", buf); + else + snprintf(g_locale, sizeof g_locale, "zh_cn"); + g_locale_loaded = 1; +} + +/* 惰性加载覆盖表(对当前 g_locale)。幂等(只加载一次直到 set_locale 重载)。 */ +static void str_ensure_override(void) +{ + if (g_override_loaded) return; + str_load_override(); + g_override_loaded = 1; +} + +/* meow_str / meow_str_locale 前置:确保 locale 初值 + 覆盖表就绪。 */ +static void str_init(void) +{ + str_init_locale(); + str_ensure_override(); +} + +/* 持久化 ui.locale(best-effort)。失败静默(运行时仍切换,仅不跨启动)。 */ +static void str_persist_locale(void) +{ + str_setting_set("ui.locale", g_locale); +} + +const char *meow_str_locale(void) +{ + str_init(); + return g_locale; +} + +const char *meow_str(const char *id) +{ + if (!id) return ""; + str_init(); + int en = (strcmp(g_locale, "en_us") == 0); + + /* ① 当前 locale 的 TF 覆盖表(最高优先) */ + for (int i = 0; i < g_ov_n; i++) + if (strcmp(g_ov_id[i], id) == 0) return g_ov_text[i]; + + /* ② 内嵌当前语言(en_us→en;其余→zh)③ 兜底内嵌内置默认(zh)——同一表内按语言取。 */ + for (size_t i = 0; i < MEOW_STR_EMBED_N; i++) + if (strcmp(g_embed[i].id, id) == 0) return en ? g_embed[i].en : g_embed[i].zh; + + /* ④ 未知 id:原样返回调用方(永不 NULL)。 */ + return id; +} + +int meow_strf(char *out, int outsz, const char *id, ...) +{ + if (!out || outsz <= 0) return -1; + const char *s = meow_str(id); + va_list ap; + va_start(ap, id); + int r = vsnprintf(out, (size_t)outsz, s, ap); + va_end(ap); + return r; +} + +void meow_str_set_locale(const char *locale) +{ + if (!locale || !locale[0]) return; + str_init(); /* 确保 locale 初值 + 覆盖表基座就绪 */ + snprintf(g_locale, sizeof g_locale, "%s", locale); + str_load_override(); /* 按新 locale 重载覆盖表 */ + g_override_loaded = 1; + str_persist_locale(); /* best-effort 持久化(失败静默) */ +} diff --git a/patch_v2/sdk/test_ime_fix.c b/patch_v2/sdk/test_ime_fix.c index d9034b4..fb321e0 100644 --- a/patch_v2/sdk/test_ime_fix.c +++ b/patch_v2/sdk/test_ime_fix.c @@ -7,7 +7,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm) diff --git a/patch_v2/sdk/test_meow_ui_anim.c b/patch_v2/sdk/test_meow_ui_anim.c index 531d9d3..9ac1720 100644 --- a/patch_v2/sdk/test_meow_ui_anim.c +++ b/patch_v2/sdk/test_meow_ui_anim.c @@ -5,7 +5,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global): * gcc -o test_meow_ui_anim test_meow_ui_anim.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm diff --git a/patch_v2/sdk/test_meow_ui_box.c b/patch_v2/sdk/test_meow_ui_box.c index 5567eee..2ec9aaa 100644 --- a/patch_v2/sdk/test_meow_ui_box.c +++ b/patch_v2/sdk/test_meow_ui_box.c @@ -5,7 +5,7 @@ * 编译(宿主 gcc,链真实 widget/draw/activity/theme/gfx + 打桩 font/global): * gcc -o test_meow_ui_box test_meow_ui_box.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm diff --git a/patch_v2/sdk/test_meow_ui_dispatch.c b/patch_v2/sdk/test_meow_ui_dispatch.c index d045983..aaea24a 100644 --- a/patch_v2/sdk/test_meow_ui_dispatch.c +++ b/patch_v2/sdk/test_meow_ui_dispatch.c @@ -14,7 +14,7 @@ * 打桩 font/global;完整超集便于链接): * gcc -Wall -Wextra -o test_meow_ui_dispatch test_meow_ui_dispatch.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm diff --git a/patch_v2/sdk/test_meow_ui_listvirt.c b/patch_v2/sdk/test_meow_ui_listvirt.c index 8d99e60..27c2417 100644 --- a/patch_v2/sdk/test_meow_ui_listvirt.c +++ b/patch_v2/sdk/test_meow_ui_listvirt.c @@ -11,7 +11,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_listvirt test_meow_ui_listvirt.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm diff --git a/patch_v2/sdk/test_meow_ui_scroll.c b/patch_v2/sdk/test_meow_ui_scroll.c index 693701c..9aef33f 100644 --- a/patch_v2/sdk/test_meow_ui_scroll.c +++ b/patch_v2/sdk/test_meow_ui_scroll.c @@ -5,7 +5,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global): * gcc -o test_meow_ui_scroll test_meow_ui_scroll.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm diff --git a/patch_v2/sdk/test_meow_ui_str.c b/patch_v2/sdk/test_meow_ui_str.c new file mode 100644 index 0000000..6932331 --- /dev/null +++ b/patch_v2/sdk/test_meow_ui_str.c @@ -0,0 +1,174 @@ +/* + * 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=/data,写 /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 +#include +#include +#include +#include + +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) { + /* 建 /data + /strings/en_us.txt(meow_str 由 MEOW_DATA_DIR 去尾 /data + * 得 /,再拼 strings/.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; +} diff --git a/patch_v2/sdk/test_meow_ui_theme.c b/patch_v2/sdk/test_meow_ui_theme.c index 6a11280..b9acdeb 100644 --- a/patch_v2/sdk/test_meow_ui_theme.c +++ b/patch_v2/sdk/test_meow_ui_theme.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_theme test_meow_ui_theme.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * diff --git a/patch_v2/sdk/test_meow_ui_widgets.c b/patch_v2/sdk/test_meow_ui_widgets.c index c0f04c5..99160c2 100644 --- a/patch_v2/sdk/test_meow_ui_widgets.c +++ b/patch_v2/sdk/test_meow_ui_widgets.c @@ -4,7 +4,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕) diff --git a/patch_v2/sdk/test_notif_gesture.c b/patch_v2/sdk/test_notif_gesture.c index 2f7c206..4c5db97 100644 --- a/patch_v2/sdk/test_notif_gesture.c +++ b/patch_v2/sdk/test_notif_gesture.c @@ -16,7 +16,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/ime/media/notify_ipc + 打桩 font/global): * gcc -Wall -Wextra -o test_notif_gesture test_notif_gesture.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_media.c meow_notify_ipc.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_media.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm * 运行:./test_notif_gesture */ diff --git a/patch_v2/sdk/test_overlay_suppress.c b/patch_v2/sdk/test_overlay_suppress.c index 4fa5893..064ebab 100644 --- a/patch_v2/sdk/test_overlay_suppress.c +++ b/patch_v2/sdk/test_overlay_suppress.c @@ -13,7 +13,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -Wextra -o test_overlay_suppress test_overlay_suppress.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread */ diff --git a/patch_v2/sdk/test_p1p2p3_repro.c b/patch_v2/sdk/test_p1p2p3_repro.c index 27438eb..474480b 100644 --- a/patch_v2/sdk/test_p1p2p3_repro.c +++ b/patch_v2/sdk/test_p1p2p3_repro.c @@ -23,7 +23,7 @@ * 2) 编译本文件 + 其余 hello 模块 + SDK 源 + 打桩,链接 app.o: * gcc -Wall -Wextra -DMEOW_UI_HOST_TEST -o test_p1p2p3_repro test_p1p2p3_repro.c app.o \ * test_host_stub.c meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_img.c meow_ui_config.c meow_ui_notify.c meow_notify_ipc.c meow_ui_draw.c \ * meow_ui_view.c meow_ui_nav.c meow_ui_media.c stb_image.c \ * ../hello_app/ui/home/home.c ../hello_app/ui/full.c ../hello_app/ui/demo/demo.c \ diff --git a/patch_v2/sdk/test_pinyin.c b/patch_v2/sdk/test_pinyin.c index fd8e143..6b9a764 100644 --- a/patch_v2/sdk/test_pinyin.c +++ b/patch_v2/sdk/test_pinyin.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,同 test_ime_fix): * gcc -Wall -Wextra -o test_pinyin test_pinyin.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \ * meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm */ diff --git a/patch_v2/sdk/test_sdk_fixes.c b/patch_v2/sdk/test_sdk_fixes.c index 3392e6f..1134668 100644 --- a/patch_v2/sdk/test_sdk_fixes.c +++ b/patch_v2/sdk/test_sdk_fixes.c @@ -9,7 +9,7 @@ * 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global): * gcc -Wall -o test_sdk_fixes test_sdk_fixes.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ - * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ + * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \ * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread */ diff --git a/patch_v2/sdk/test_umbrella.c b/patch_v2/sdk/test_umbrella.c index 5e801e1..a5d5124 100644 --- a/patch_v2/sdk/test_umbrella.c +++ b/patch_v2/sdk/test_umbrella.c @@ -82,6 +82,8 @@ static void umbrella_smoke(void) (void)&meow_notify_send; (void)&meow_notify_category_str; (void)&meow_notify_update_progress; /* 配置(§9.2) */ (void)&meow_config_dir; (void)&meow_config_int; (void)&meow_config_remove; + /* 字符串资源/i18n(§13,追加式) */ + (void)&meow_str; (void)&meow_strf; (void)&meow_str_set_locale; (void)&meow_str_locale; /* 字体(§5.7/§10) */ (void)&meow_font_enumerate; (void)&meow_font_tier; (void)&meow_font_select_by_id; /* 音频(§11) */