/* * 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 持久化(失败静默) */ }