首页
/ PowerToys 代码指引:C++ 与 C 中 UI 显示字符串的本地化规范及实现原理

PowerToys 代码指引:C++ 与 C 中 UI 显示字符串的本地化规范及实现原理

2026-09-04 22:27:50作者:蔡丛锟

本文为 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_DESCRIPTIONINTERNAL_NAMEORIGINAL_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)

从宏定义可以看到两个关键设计:

  1. 默认回退是资源 ID 的名字本身L#resource_id 利用字符串化宏把 IDS_MODULE_DISPLAYNAME 转成 L"IDS_MODULE_DISPLAYNAME" 作为最终兜底文案。也就是说,即使资源加载彻底失败,UI 也不会出现空白,而是显示 ID 名——这对排查资源缺失问题非常友好。若需要自定义兜底文案,则使用同文件中的 GET_RESOURCE_STRING_FALLBACK(resource_id, fallback) 变体。
  2. 实例句柄取当前模块基址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
}

具体优先级为:

  1. 语言覆盖(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 安装器资源文件中的语言集合保持一致。
  2. 当前线程语言直接加载:若覆盖语言为空,调用 LoadStringW(instance, resource_id, &text_ptr, 0) 按系统默认语言加载。
  3. en-US 兜底LoadStringW 返回 0(资源缺失)时,尝试 get_english_fallback_stringMAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US) 显式加载英语字符串。
  4. 最终兜底:连英语也取不到时,返回调用方提供的 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.resxsrc/Update/Resources.resxsrc/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.mdLocProject.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++ 模块

  1. resource.h 中为字符串分配唯一IDS_ ID(通常取现有最大 ID + 1);
  2. .rcSTRINGTABLE 中加入 IDS_XXX L"English text" 条目(推荐用 VS 资源编辑器操作);
  3. 代码中通过 #include <common.h> 后使用 GET_RESOURCE_STRING(IDS_XXX)GET_RESOURCE_STRING_FALLBACK(IDS_XXX, L"YourFallback") 取串;
  4. 理解运行时行为:优先取语言覆盖命中的翻译,失败则回退 en-US,再失败则回退到 ID 名字面量(见 src/common/utils/resources.h)。

C# 模块

  1. 在模块的 Resources.resx(或对应 XxxResources.resx)中添加 <data> 条目,保留 <comment> 说明使用场景;
  2. 代码中优先使用自动生成的 XxxResources 静态属性;仅在不具备生成器条件时使用 ResourceManager + GetString
  3. 若项目参与管道本地化,按 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.hsrc/ActionRunner/resource.base.h
base rc 文件(VERSIONINFO 示例) src/ActionRunner/actionRunner.base.rc
C# 资源文件与生成器示例 src/ActionRunner/Resources.resxsrc/common/ManagedCommon/CommonResources.resxsrc/common/ManagedCommon/CommonResources.Designer.cs
本地化流水线完整说明 doc/devdocs/development/localization.md
编码风格与格式化 doc/devdocs/development/style.mdsrc/codeAnalysis/format_sources.ps1
代码组织与贡献规则 doc/devdocs/readme.md
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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