PowerToys 代码指引:C++ 与 C 中 UI 显示字符串的本地化规范及实现原理
本文为 PowerToys 贡献者的编码指引,聚焦“不要硬编码 UI 显示字符串”这一核心准则:分别给出 C++(StringTable 资源 + resource.h 资源 ID + GET_RESOURCE_STRING 宏)与 C#(.resx 资源文件 + ResourceManager/Resources.Designer.cs)两套字符串管理与消费方案的完整操作示例,并结合 src/common/utils/resources.h 的源码解析其语言回退链的实现原理。读完本文,你可以在 C++ 或 C# 模块中正确新增、引用本地化字符串,并理解运行时按语言取串与回退的完整流程。
总体原则:禁止硬编码 UI 显示字符串
PowerToys 的编码指引(见 doc/devdocs/guidance.md)开篇就确立了一条强制性规则:
为了支持本地化,**你不应(YOU SHOULD NOT)**在代码中硬编码任何 UI 显示字符串。相反,应使用资源文件来消费字符串。
这条规则的目的是让所有面向用户的文案都进入资源文件,从而可以参与翻译流程:C++ 项目的字符串进入 .rc 的 StringTable(由资源编译器按语言嵌入二进制),C# 项目的字符串进入 .resx 文件(由管道生成各语言的卫星资源)。PowerToys 的完整本地化流水线(CDPX 管道、LocProject.json、lcl 文件、卫星 dll 打包等)在 doc/devdocs/development/localization.md 中有专门说明,本文则聚焦指引中“Working With Strings”一节的两条技术路径,并向下钻取到仓库源码中的实际实现。
C++:用 StringTable + resource.h + GET_RESOURCE_STRING 管理字符串
C++ 侧的完整流程分三步:定义资源 ID、在 .rc 中写入 StringTable、在代码中用宏消费字符串。
第一步:在 resource.h 中定义资源 ID
每个 UI 显示字符串需要一个唯一的整数 ID,声明在资源头文件 resource.h 中:
#define IDS_MODULE_DISPLAYNAME XXX
指引明确要求:XXX 必须是列表中唯一的 int,通常取“当前最后一个字符串 ID 的整数加一”。这一约定保证了 LoadStringW 按 ID 查找时不会与其他资源冲突。
仓库中可以对照真实示例。例如 src/Update/resource.base.h 展示了 {{NO_DEPENDENCIES}} 风格的 base 资源头文件,其中定义了非本地化的 FILE_DESCRIPTION、INTERNAL_NAME、ORIGINAL_FILENAME 等常量;而 Localizable 字符串则在对应的 resource.h 中以 IDS_ 前缀命名并分配递增 ID。从源码结构看,PowerToys 的多个 C++ 模块采用 resource.base.h(非本地化常量)+ 生成/维护的 resource.h(本地化 ID)分离的模式,这也与本地化文档中描述的 resource.base.h / 生成 resource.h 的构建事件相吻合。
第二步:在 .rc 资源定义脚本中写入 StringTable
资源定义脚本(.rc 文件)中使用 Win32 的 STRINGTABLE 资源,把 ID 与 L"..." 宽字符串关联起来:
STRINGTABLE
BEGIN
IDS_MODULE_DISPLAYNAME L"Module Name"
END
指引建议用 Visual Studio 内置的资源编辑器(Resource Editor / Resource View)来创建和管理资源文件,可以直接在 IDE 中编辑 STRINGTABLE 并避免手写语法错误。仓库中的 src/ActionRunner/actionRunner.base.rc 是一个典型 base rc 文件示例:它包含 VERSIONINFO 块、StringFileInfo(CompanyName、FileDescription、FileVersion 等)与 VarFileInfo 的 Translation 声明(0x409, 1200,即 US English + Unicode 字符集)。本地化的 STRINGTABLE 会追加在对应的正式 .rc 文件中。
第三步:用 GET_RESOURCE_STRING 宏消费字符串
在 C++ 代码中消费字符串时,包含公共头文件并使用 GET_RESOURCE_STRING(UINT resource_id) 宏:
#include <common.h>
std::wstring GET_RESOURCE_STRING(IDS_MODULE_DISPLAYNAME)
该宏的完整定义位于 src/common/utils/resources.h:
extern "C" IMAGE_DOS_HEADER __ImageBase;
// Wrapper for getting a string from the resource file. Returns the resource id text when fails.
#define GET_RESOURCE_STRING(resource_id) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), L#resource_id)
#define GET_RESOURCE_STRING_FALLBACK(resource_id, fallback) get_resource_string(resource_id, reinterpret_cast<HINSTANCE>(&__ImageBase), fallback)
从宏定义可以看到两个关键设计:
- 默认回退是资源 ID 的名字本身:
L#resource_id利用字符串化宏把IDS_MODULE_DISPLAYNAME转成L"IDS_MODULE_DISPLAYNAME"作为最终兜底文案。也就是说,即使资源加载彻底失败,UI 也不会出现空白,而是显示 ID 名——这对排查资源缺失问题非常友好。若需要自定义兜底文案,则使用同文件中的GET_RESOURCE_STRING_FALLBACK(resource_id, fallback)变体。 - 实例句柄取当前模块基址:
reinterpret_cast<HINSTANCE>(&__ImageBase)让资源从调用方自身的模块(exe/dll)中查找,因此每个模块只需管理自己的资源文件,无需显式传递实例句柄。
源码级原理:语言覆盖与多级回退链
GET_RESOURCE_STRING 展开调用的 get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback) 实现在 src/common/utils/resources.h,其取串顺序是一条清晰的回退链:
inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
{
// Try to load en-us string as the first fallback.
std::wstring english_string = get_english_fallback_string(resource_id, instance);
std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance);
if (!language_override_resource.empty())
{
return language_override_resource;
}
// ... LoadStringW 失败时返回 english_string 或 fallback
}
具体优先级为:
- 语言覆盖(language override):
get_resource_string_language_override(同文件 L31 起)先通过LanguageHelpers::load_language()读取当前界面语言(static std::wstring language,只计算一次),再将其映射到LANG_*/SUBLANG_*常量并用ATL::CStringW::LoadStringW按该语言加载。该函数中的注释明确写道“Language list taken from Resources.wxs”,覆盖 ar-SA、cs-CZ、de-DE、en-US、es-ES、fa-IR、fr-FR、he-IL、hu-HU、it-IT、ja-JP、ko-KR、nl-NL、pl-PL、pt-BR、pt-PT、ru-RU、sv-SE、tr-TR、uk-UA、zh-CN、zh-TW 等语言——与 WiX 安装器资源文件中的语言集合保持一致。 - 当前线程语言直接加载:若覆盖语言为空,调用
LoadStringW(instance, resource_id, &text_ptr, 0)按系统默认语言加载。 - en-US 兜底:
LoadStringW返回 0(资源缺失)时,尝试get_english_fallback_string以MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)显式加载英语字符串。 - 最终兜底:连英语也取不到时,返回调用方提供的 fallback(对
GET_RESOURCE_STRING即资源 ID 名字面量)。
这条回退链解释了为什么“资源缺失”在 PowerToys C++ 模块中通常表现为显示 ID 名而非空白,也说明了新增本地化字符串时必须保证 ID 唯一且在各语言 rc 中同步更新。
仓库中一个真实消费方示例是键盘管理器的字符串封装层 src/modules/keyboardmanager/KeyboardManagerEditorLibrary/KeyboardManagerEditorStrings.h,它把 GET_RESOURCE_STRING(IDS_EDITSHORTCUTS_ALLAPPS)、GET_RESOURCE_STRING(IDS_MAPPING_TYPE_DROPDOWN_TEXT) 等调用封装成静态方法,供 UI 层统一取串——这正是“先定义 ID、再集中消费”的推荐写法。
C#:用 .resx + ResourceManager 管理字符串
C# 侧使用 XML 资源文件(.resx)存储 UI 显示字符串,用 System.Resources.ResourceManager 消费。.resx 可以直接在 Visual Studio 中创建和管理。
第一步:在 Resources.resx 中声明字符串
.resx 中每条 <data> 由 name(键名)、<value>(显示文案)和可选的 <comment>(给翻译人员的上下文说明)组成:
<data name="ValidUIDisplayString" xml:space="preserve">
<value>Description to be displayed on UI.</value>
<comment>This text is displayed when XYZ button clicked.</comment>
</data>
仓库中大量模块遵循这一模式,例如 src/ActionRunner/Resources.resx、src/Update/Resources.resx 与 src/common/ManagedCommon/CommonResources.resx。为翻译人员保留 <comment> 是良好实践:它解释了该字符串出现的场景,避免误译。
第二步:消费字符串的两种方式
方式一:直接使用 ResourceManager。 适合无法使用 Visual Studio 资源生成器的场景:
System.Resources.ResourceManager manager = new System.Resources.ResourceManager(baseName, assembly);
string validUIDisplayString = manager.GetString("ValidUIDisplayString", resourceCulture);
其中 baseName 是资源根命名空间(通常以命名空间加资源文件名构成),resourceCulture 指定要取用的 CultureInfo;取不到时 GetString 会按 .NET 资源规则逐级回退(区域回退、中性文化)。
方式二:使用自动生成的强类型访问器(推荐)。 若资源文件由 Visual Studio 创建,编译器会生成 Resources.Designer.cs,其中封装了 ResourceManager 的全部逻辑,代码中直接静态访问属性即可:
string validUIDisplayString = Resources.ValidUIDisplayString;
仓库中该模式有现成对照,例如 src/common/ManagedCommon/CommonResources.Designer.cs 即为 CommonResources.resx 生成的强类型封装,业务代码通过 CommonResources.Xxx 属性取串,而不再手工持有 ResourceManager 实例。这种方式在编译期就能检查键名拼写,减少“运行时才发现 key 缺失”的问题。
从源码结构看,PowerToys 的 C# 模块(如 ActionRunner、Update、ManagedCommon 等)普遍采用“XxxResources.resx + XxxResources.Designer.cs 成对出现”的组织方式;配合本地化管道(见 doc/devdocs/development/localization.md 中 LocProject.json 与卫星资源 dll 的说明),同一份英文 resx 会在构建期生成各语言的本地化 resx 并编译为卫星程序集,从而在运行时按 resourceCulture 自动命中对应翻译。
与其他编码规范的衔接
指引在“More On Coding Guidance”一节把读者引向两份配套文档,实际内容位于 devdocs 目录下:
- 编码风格(Coding Style):doc/devdocs/development/style.md。核心要点是:在既有类/函数中插入代码时,尽量贴近现有代码风格;全新代码或整体重构则尽量遵循 Modern C++,并参考 C++ Core Guidelines。格式化工具链包括:XAML 文件用 XamlStyler(可执行
.\.pipelines\applyXamlStyling.ps1 -Main),C++ 源码用仓库的.clang-format(Visual Studio 中Ctrl+K, Ctrl+D格式化当前文档),命令行场景可用 src/codeAnalysis/format_sources.ps1 对 git 中已修改的文件批量执行 clang-format(要求clang-format.exe在%PATH%中,或从 VS Native Tools Command Prompt 启动以自动定位随 VS 分发的 clang-format)。文档同时说明:CI 尚未强制代码格式,但对新代码要求遵循既有格式风格。 - 代码组织(Code Organization):doc/devdocs/readme.md 的 “Rules” 一节要求:遵循现有代码模式;新组件尽量封装为接口清晰的库或类;新增/修改类与方法时要补充或更新单元测试;开 PR 前确保本地构建成功且功能测试通过。
因此,字符串本地化规范并非孤立存在:它和“跟随现有模式、封装为库、补充测试”的总规则配合,共同保证新增模块的文案既可翻译、又与仓库既有资源文件组织方式一致。
实操核对清单
结合本文与源码,新增一个本地化 UI 字符串时可按以下清单自检:
C++ 模块
- 在
resource.h中为字符串分配唯一的IDS_ID(通常取现有最大 ID + 1); - 在
.rc的STRINGTABLE中加入IDS_XXX L"English text"条目(推荐用 VS 资源编辑器操作); - 代码中通过
#include <common.h>后使用GET_RESOURCE_STRING(IDS_XXX)或GET_RESOURCE_STRING_FALLBACK(IDS_XXX, L"YourFallback")取串; - 理解运行时行为:优先取语言覆盖命中的翻译,失败则回退 en-US,再失败则回退到 ID 名字面量(见 src/common/utils/resources.h)。
C# 模块
- 在模块的
Resources.resx(或对应XxxResources.resx)中添加<data>条目,保留<comment>说明使用场景; - 代码中优先使用自动生成的
XxxResources静态属性;仅在不具备生成器条件时使用ResourceManager+GetString; - 若项目参与管道本地化,按 doc/devdocs/development/localization.md 的配置接入
LocProject.json,由 CDPX 管道生成各语言 resx 与卫星 dll。
参考文件索引
| 内容 | 路径 |
|---|---|
| 本指引原文(Coding Guidance) | doc/devdocs/guidance.md |
| C++ 资源宏与回退链实现 | src/common/utils/resources.h |
| 字符串消费封装示例 | src/modules/keyboardmanager/KeyboardManagerEditorLibrary/KeyboardManagerEditorStrings.h |
| base 资源头文件示例 | src/Update/resource.base.h、src/ActionRunner/resource.base.h |
| base rc 文件(VERSIONINFO 示例) | src/ActionRunner/actionRunner.base.rc |
| C# 资源文件与生成器示例 | src/ActionRunner/Resources.resx、src/common/ManagedCommon/CommonResources.resx、src/common/ManagedCommon/CommonResources.Designer.cs |
| 本地化流水线完整说明 | doc/devdocs/development/localization.md |
| 编码风格与格式化 | doc/devdocs/development/style.md、src/codeAnalysis/format_sources.ps1 |
| 代码组织与贡献规则 | doc/devdocs/readme.md |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00