首页
/ OBS Studio 文本查找接口(Text Lookup):libobs 本地化字符串存储与查询机制详解

OBS Studio 文本查找接口(Text Lookup):libobs 本地化字符串存储与查询机制详解

2026-09-04 22:59:52作者:柯茵沙

OBS Studio 的界面、插件与工具库全部依赖一套轻量的本地化(i18n)机制来实现多语言支持。本文以 libobs 的文本查找接口(Text Lookup)为核心,完整讲解 lookup_t 对象的四个 API 函数——text_lookup_createtext_lookup_addtext_lookup_destroytext_lookup_getstr——的语义与参数,并结合 text-lookup.c 的源码剖析其基于哈希表的存储结构与基于 ini 风格文件的解析实现,最后展示 OBS 主程序 OBSApp.cpp 中"英文兜底 + 目标语言覆盖"的实际调用链,帮助你既会调用接口,也理解它背后的设计原理。

1. 接口定位:一个为本地化而生的键值存储

Text Lookup 接口的定位在头文件 text-lookup.h 中交代得很清楚:

Used for storing and looking up localized strings. Stores localization strings in a hashmap to efficiently look up associated strings via a unique string identifier name.

即:用于存储并按唯一标识符高效查找本地化字符串。它不依赖 Qt 的翻译机制,而是 libobs 自带的纯 C 设施,使用类似 ini 文件的格式保存"键 = 译文"的映射。使用者只需 #include <util/text-lookup.h>,获得一个不透明(opaque)类型:

#include <util/text-lookup.h>

/* opaque typedef */
struct text_lookup;
typedef struct text_lookup lookup_t;

头文件对外仅暴露四个函数(均带 EXPORT 修饰以导出符号):

EXPORT lookup_t *text_lookup_create(const char *path);
EXPORT bool text_lookup_add(lookup_t *lookup, const char *path);
EXPORT void text_lookup_destroy(lookup_t *lookup);
EXPORT bool text_lookup_getstr(lookup_t *lookup, const char *lookup_val, const char **out);

这种"不透明结构 + 四个函数"的极简 API 是 libobs util 层的典型风格——调用方不需要知道内部是哈希表还是其他实现。

2. 本地化文件的格式:类 ini 的键值行

接口以 path 指向一个"ini 风格"的本地化文件。以仓库中真实的英文资源文件 en-US.ini 为例,其格式如下:

# Note to translators: *DO NOT* translate this file directly...
# Language of this file
Language="English"

OK="OK"
Cancel="Cancel"
DroppedFrames="Dropped Frames %1 (%2%)"
Projector.Open.Program="Open Program Projector"

格式要点(均可在 text-lookup.c 的解析代码中得到印证):

  • 注释:以 # 开头的行是注释,一直持续到行尾(见 lookup_gettoken()ch == '#' 时跳到行尾的逻辑);
  • 键值对:每行为 键=值,等号前后可以带空白;
  • 引号:值可以用双引号包裹,引号内的转义字符在 convert_string() 中被还原:\n → 换行、\t → 制表符、\r → 回车、\" → 双引号;
  • 编码:文件以 UTF-8 读取(os_fread_utf8),且读取后会把 \r 替换为空格以兼容 Windows 行尾;
  • 键的命名约定:OBS 的本地化键采用带命名空间的风格,如 Projector.Open.Program,普通通用词则直接用 OKCancelSettings

除了各语言的资源文件(frontend/data/locale/ 下共 77 个 .ini 文件,从 en-USzh-CN 等),locale.ini 则用标准 ini 分节格式登记每种语言的原生名称(如 [ja-JP]Name=日本語),供设置界面显示语言选择列表——注意这个文件是标准 ini,由 config-file 接口解析,与 text-lookup 的扁平键值格式不同。

3. 四个 API 函数的完整语义

3.1 text_lookup_create:从文件创建对象

lookup_t *text_lookup_create(const char *path);

从指定的本地化文件创建一个 text lookup 对象。

  • 参数 path:本地化文件的路径;
  • 返回值:成功时返回新建的 lookup 对象;出错时(例如文件无法打开、读取内容为空)返回 NULL

从源码 text_lookup_create() 可以看到,它的实现就是对一个 bzalloc 出来的空对象调用 text_lookup_add(),若 add 失败则释放内存并返回 NULL——即创建与加载是同一套逻辑,创建失败意味着文件加载失败。

3.2 text_lookup_add:追加加载并覆盖已有值

bool text_lookup_add(lookup_t *lookup, const char *path);

从另一个本地化文件中加载条目并替换(replace)已存在的键

  • 参数 lookup:目标 lookup 对象;
  • 参数 path:本地化文件的路径;
  • 返回值:成功为 true,失败为 false(文件不存在或读取失败)。

这个"后加载者覆盖先加载者"的语义正是它的设计精髓:官方文档给出的典型用法是先加载默认兜底语言(如英语),再加载目标语言,这样当目标语言没有翻译某个键时,查询会自动落回英语译文,而不会被整份文件缺失卡死。这一点在 text-lookup.c 中通过 HASH_REPLACE_STR 实现——同键条目直接替换旧条目并销毁旧值:

HASH_REPLACE_STR(lookup->items, lookup, item, old);
if (old)
    text_item_destroy(old);

注意区分两种失败:text_lookup_add() 返回 false 只在文件打开/读取层面失败;文件里某一行写错只会导致该行被跳过,不会使整个加载失败。

3.3 text_lookup_destroy:销毁对象

void text_lookup_destroy(lookup_t *lookup);

销毁 lookup 对象并释放其占用的内存。

  • 参数 lookup:要销毁的 lookup 对象。

NULL 调用是安全的(实现中有 if (lookup) 判空)。text_lookup_destroy() 通过 HASH_ITER/HASH_DELETE 遍历删除哈希表中的全部条目,逐个 bfree 键与值字符串,最后释放对象本体——调用方不需要逐条清理。

3.4 text_lookup_getstr:按键查询译文

bool text_lookup_getstr(lookup_t *lookup, const char *lookup_val, const char **out);

获取某个键对应的本地化字符串。

  • 参数 lookup:lookup 对象;
  • 参数 lookup_val:要查找的键(唯一字符串标识);
  • 参数 out:接收翻译后字符串指针的出参;
  • 返回值:键存在返回 true,不存在返回 false

需要注意返回值指针的生命周期out 指向的是对象内部字符串的指针,在 text_lookup_destroy() 之前一直有效,无需(也不应)由调用方释放。实现见 lookup_getstring(),就是一次 HASH_FIND_STR 哈希查找,平均复杂度 O(1)。

4. 源码剖析:解析器与内部结构

理解实现细节有助于正确使用接口并排查问题。

4.1 内部数据结构

内部仅有两层结构,均以 uthash 宏挂接为字符串哈希表(text-lookup.c):

struct text_item {
    char *lookup, *value;   /* 键、值,各自独立分配 */
    UT_hash_handle hh;
};

struct text_lookup {
    struct text_item *items;  /* uthash 字符串哈希表头 */
};

查找用的键与存储的键是同一块内存HASH_FIND_STR 直接对 lookup_val 计算哈希),因此查询无需拷贝;这也是 getstr 能零开销返回内部指针的原因。

4.2 解析流程

text_lookup_add() 的完整流程为:

  1. os_fopen(path, "rb") 打开文件,失败返回 false
  2. os_fread_utf8() 一次性读入全部内容(跨平台按 UTF-8 处理),关闭文件;
  3. dstr_replace(&file_str, "\r", " ") 把回车符统一替换为空格,归一化行尾;
  4. 交给 lookup_addfiledata() 做词法解析。

词法解析建立在 libobs util 的通用 lexer 之上,lookup_gettoken() 按空白切分 token,并识别三类特殊 token:# 注释(跳到行尾)、= 赋值符、" 开头的字符串字面量(由 lookup_getstringtoken() 处理,支持反斜杠转义,直到匹配的闭引号)。主循环的配对逻辑是:取一个非空白 token 作为候选键,跳过 = 之后取下一个非空白 token 作为值,成对写入哈希表,再跳到下一行。任何不符合该模式的行(空行、孤立 token、缺 = 的行)都会被安全跳过,因此解析器对格式错误相当宽容。

值写入前经过 convert_string() 做转义还原(\n\t\r\")。这也解释了为什么本地化文件里想写一个真正的新行必须写成 \\n

5. 实战:OBS 主程序的"英语兜底 + 语言覆盖"调用链

Text Lookup 在 OBS 中的核心消费者是前端 OBSAppInitLocale() 就是前文所述"典型用法"的完整落地:

// 1) 确定目标语言:读取用户配置 General/Language,未设置时用 DEFAULT_LANG("en-US")
const char *lang = config_get_string(userConfig, "General", "Language");
bool userLocale = config_has_user_value(userConfig, "General", "Language");
if (!userLocale || !lang || lang[0] == '\0')
    lang = DEFAULT_LANG;   // frontend/OBSApp.cpp 第 289 行: #define DEFAULT_LANG "en-US"

// 2) 先创建对象:加载英语兜底文件 locale/en-US.ini
string englishPath;
if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) {
    OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini");
    return false;
}
textLookup = text_lookup_create(englishPath.c_str());
if (!textLookup) {
    OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str());
    return false;
}

// 3) 目标语言不是英语时,在其之上再叠加目标语言文件
stringstream file;
file << "locale/" << lang << ".ini";
string path;
if (GetDataFilePath(file.str().c_str(), path)) {
    if (!text_lookup_add(textLookup, path.c_str()))
        blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str());
}

这段代码精确体现了 text_lookup_add() 文档语义的两个要点:

  • 英语作为第一层text_lookup_create() 只加载 en-US.ini,保证每个键在兜底层必有值(英语是翻译的源语言,键全集即英语文件);
  • 目标语言作为覆盖层text_lookup_add() 再叠加用户所选语言(如 zh-CN.ini),哈希表的 replace 语义使得已翻译的键覆盖英语值,未翻译的键保持英语值——"部分翻译的语言"不会出现空白字符串。

此外还有一个细节:当用户没有显式设置语言时(!userLocale),代码会遍历系统偏好语言列表 GetPreferredLocales(),逐个尝试 text_lookup_add() 直到命中仓库中实际存在的 locale 文件(frontend/data/locale/ 下每个 .ini 对应一种语言),命中后把 QLocale::setDefault() 同步到该语言并记录 blog(LOG_INFO, "Using preferred locale ...")。也就是说 text-lookup 同时承担了"系统语言探测 → 实际可用语言"的收敛逻辑。

查询侧则由 OBSApp::TranslateString() 统一收口:

bool OBSApp::TranslateString(const char *lookupVal, const char **out) const
{
    for (obs_frontend_translate_ui_cb cb : translatorHooks) {
        if (cb(lookupVal, out))
            return true;
    }
    return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out);
}

即:先给已注册的前端翻译钩子(obs_frontend_translate_ui_cb,例如语言切换插件可用它动态提供字符串)一次机会,全部落空后再回落到 text_lookup_getstr()。界面代码通常不直接调用 text-lookup,而是经由这一层封装取字符串。

6. 使用建议与注意事项

综合文档语义与源码实现,实际使用(无论是写 OBS 插件还是自行复用该库)应遵循以下要点:

  1. 生命周期管理text_lookup_create()text_lookup_add() 都不检查 lookup 判空之外的参数合法性,文件路径错误只返回 NULL/false,务必检查返回值;对象用完必须 text_lookup_destroy()(传 NULL 安全)。
  2. 加载顺序决定优先级:兜底语言先加载、目标语言后加载;顺序颠倒会导致未翻译键查不到而界面出现键名裸串。
  3. 不要自行 free out 指针text_lookup_getstr() 返回的是对象内部字符串,对象销毁后指针失效;如需跨生命周期使用必须复制。
  4. 文件格式宽容但有边界:行内 # 即注释、行首空白可跳过、= 缺失的行被丢弃;值内换行必须用 \\n 转义表达,直接折行会被解析为两条不完整的记录。
  5. 编码约定:文件按 UTF-8 读取,\r 会被替换为空格,因此 Windows 风格行尾无需预处理。
  6. 键的稳定性:本地化键(如 Projector.Open.Program)是界面代码与语言文件之间的契约,新增界面字符串时应先在 en-US.ini 定义键,其他语言文件再逐步补全。

7. 小结

Text Lookup 是 OBS Studio 本地化体系的基座:一个不透明的哈希表对象、四个函数组成的极简 C API、一种宽容的类 ini 文件格式,加上"create 加载兜底语言、add 覆盖目标语言"的组合语义,就支撑起了 77 种语言资源文件的运行时查询。理解 text-lookup.cHASH_REPLACE_STR 的覆盖语义与 lexer 词法解析后,你可以直接参照 OBSApp.cppInitLocale() 为自己的 C/C++ 项目复刻一套带兜底语言的多语言机制——这正是 libobs util 层"小而正确"设计的典型范例。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390