Files
MeowOS-SDK/meow_ui_str.c
T
AskaEth 2e8fac2a07 sdk(ui): 字符串资源外置(i18n 雏形 §13)—— meow_str_* + 内嵌双语表 + TF 覆盖表 + 控件接入
公开 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 / 动态字体回退(雏形)。
2026-09-02 02:37:38 +08:00

289 lines
12 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.
/*
* meow_ui_str.c — SDK 字符串资源框架(i18n 雏形,§13.meow_str_*)。
*
* 背景:SDK/系统 UI 文案此前全部硬编码在 C 源码(dialog 的 "OK/CANCEL"、IME 的 "空格/确定/
* 拼音/(无候选)" 等),第三方开发者无法按语言取串,公开 SDK 前必须外置。本 TU 提供
* 「按 locale 取整串」的最小框架:内嵌双语默认表(编译进 meow_ui.a,必可用)+ TF 卡覆盖表
* (MeowData/<pkg>/strings/<locale>.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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#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 覆盖表路径:<MEOW_DATA_DIR 去尾 /data>/strings/<locale>.txt(= MeowData/<pkg>/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 = <pkg>/data;去尾 "/data" 回到 <pkg>,再拼 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 持久化(失败静默) */
}