PowerToys 本地化开发指南:从 LocProject.json、lcl 文件到 rc 转换与卫星程序集打包
2026/9/5 18:36:47 网站建设 项目流程

PowerToys 本地化开发指南:从 LocProject.json、lcl 文件到 rc 转换与卫星程序集打包

【免费下载链接】PowerToysMicrosoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows项目地址: https://gitcode.com/GitHub_Trending/po/PowerToys

本篇基于 PowerToys 本地化开发文档 展开,系统讲解 PowerToys 的 CDPX 流水线本地化机制、LocProject.json配置、C++/C#/UWP 三类项目的本地化启用步骤、lcl文件格式与安全防护机制,以及如何把本地化资源(C++ 的 rc 字符串表、UWP 的resources.pri、C# 的卫星程序集)正确接入 MSI 安装包。读完本文,你将能够在 PowerToys 中为新模块启用本地化、为字符串建立可翻译的资源文件,并理解本地化产物从流水线到安装包的完整流转链路。

一、本地化体系总览

PowerToys 的本地化围绕一个核心原则:代码中不允许硬编码 UI 显示字符串,所有可展示文本必须来自资源文件。按项目类型分为三条资源链路:

项目类型资源格式本地化产物打包方式
C#(.NET)Resources.resx各语言卫星程序集langId\ProjName.resources.dll卫星 dll 需额外加入 MSI
C++Resources.resx(转换源)→ 生成的.rc/.h字符串表编译进 dll/exe 本身无需额外文件
UWPResources.resw编译进resources.pri无需额外文件

从仓库结构可以印证这套体系:C# 模块的英文资源位于模块根目录,例如 ActionRunner 的 Resources.resx 与 KeyboardManagerEditor 的 Resources.resx;UWP 风格模块则使用Strings\en-us\Resources.resw,例如 ShortcutGuide.Ui 的 Resources.resw 与 cmdpal UI 的 Resources.resw。

二、流水线上的本地化(CDPX)

2.1 build-localization 步骤与 LocProject.json

本地化步骤在 CDPX 流水线中先于解决方案构建执行:它运行build-localization脚本,调用Localization.XLoc包为所有启用了本地化的项目生成 resx 文件。Localization.XLoc在仓库根目录运行,扫描每一个LocProject.json文件。每个本地化项目的项目根目录下都有这样一份LocProject.json,它描述:

  • 英文 resx 源文件位置
  • 本地化的语言集合
  • 生成后的本地化 resx 文件复制到的输出路径
  • 以及其他参数,例如语言 ID 是以目录名还是文件名形式体现在输出路径中。

一个典型的LocProject.json(项目位于src\path,英文资源在resources\Resources.resx):

{ "Projects": [ { "LanguageSet": "Azure_Languages", "LocItems": [ { "SourceFile": "src\\path\\resources\\Resources.resx", "CopyOption": "LangIDOnName", "OutputPath": "src\\path\\resources" } ] } ] }

字段说明:

字段含义
LanguageSet语言集合名称,Azure_Languages表示使用 Azure 语言集(含 27 种语言)
SourceFile英文 resx 源文件(相对仓库根目录的路径)
CopyOption语言 ID 放置方式:LangIDOnName(拼进文件名)或LangIDOnFolder(作为目录)
OutputPath生成的各语言 resx 复制到的目录

当 CDPX 流水线运行且英文 resx 发生变更时,本地化团队会收到通知。对每个启用本地化的项目,会在LocProject.json同目录生成一个loc文件夹(例如 Microsoft.Launcher 模块下的loc目录),其中按语言建立子目录,子目录下再按LocProject.jsonOutputPath对应的嵌套路径组织,每个目录中有一个lcl文件。lcl文件包含英文资源及其对应语言的译文,详见 第四节。resx 文件生成后,会在Build PowerToys步骤中被用于构建各模块的本地化版本。

2.2 restore-localization 与网络隔离

本地化脚本依赖特定的 NuGet 包,因此build-localization之前必须先运行restore-localization脚本安装所需包。该脚本必须放在流水线的restore阶段执行,因为 CDPX 流水线在build阶段处于网络隔离状态,无法在线还原包;还原时使用流水线中配置的 Toolset 包源完成。

2.3 IsPipeline 变量与 MSI 的联动

C# 项目的本地化资源 dll 只在流水线构建时才被加入 MSI。判断方式是检查IsPipeline变量是否已定义——该变量在流水线构建安装器之前被设置。之所以需要这个开关,是因为本地化 resx 文件只存在于流水线上:本地开发者机器上没有这些文件,若安装器工程无条件引用它们,本地构建安装器项目会直接失败。

2.4 当前仓库中的本地化流水线入口

当前仓库快照中,本地化流水线的入口是 loc.yml。该流水线:

  • 采用定时触发(cron: "0 3 * * 2-6",太平洋工作时间每周一至五结束后于 03:00 UTC 运行),且always: false,即仅在代码有变更时执行;
  • 通过MicrosoftTDBuild.tdbuild-task(Touchdown Build)任务把资源文件推送到本地化团队,资源匹配路径为:
resourceFilePath: | src\**\Resources.resx src\**\Resource.resx src\**\Resources.resw
  • pseudoSetting: Included表示包含伪本地化(pseudo)输出,便于在未获得真实译文前验证多语言布局;
  • 输出目录LocOutput会被打包为LocOutput.tar.gz发布为流水线工件,方便排查本地化输出问题。

从这份配置可以确认:本地化系统的输入即仓库中所有模块的英文Resources.resx/Resource.resx/Resources.resw,与第二、三节的资源约定完全一致。

三、为新项目启用本地化

第一步对所有类型相同:在项目根目录创建LocProject.json(格式见 2.1 节)。把本地化文件加入 MSI 的步骤见第六节。

3.1 C++ 项目:resx 到 rc 的转换链

C++ 项目原生不支持resx,而是使用.rc+resource.h。由于 CDPX 流水线不支持直接本地化rc文件(其替代方案是直接从二进制翻译资源,难以维护),PowerToys 采用了一条自定义转换链:以 resx 为本地化源,再用脚本把各语言 resx 转换为带字符串表的 rc 文件与 resource.h

第一步:把已有字符串表转成 resx。如果项目已有.rc文件,把字符串表拷贝到一个单独的 txt 文件,然后运行 convert-stringtable-to-resx.ps1 脚本。该脚本对输入格式要求较严格:每行必须是IDS_ResName L"ResourceValue"形式,IDS_ResNameL"..."之间可以有任意多个空格。脚本将其转换为resgen工具可识别的格式后再转成 resx。转换过程中资源名从全大写改为标题式(Title Case),并去掉IDS_前缀。转义字符可能需要手工处理,例如.rc中双引号写作"",转 resx 前需替换为单个"

第二步:拆分 base 文件并挂接构建事件。resx 生成后,把现有 rc 和 h 文件重命名为ProjName.base.rcresource.base.h;在 rc 文件中删除需要本地化的字符串表,在 h 文件中删除所有对应本地化资源的#define。然后在 C++ 工程的 vcxproj 中添加如下构建事件:

<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild"> <Exec LogStandardErrorAsError="false" Command="powershell -NonInteractive -executionpolicy Unrestricted -NoProfile $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ProjName.base.rc ProjName.rc" /> </Target>

第三步:理解 convert-resx-to-rc.ps1 的生成逻辑。convert-resx-to-rc.ps1 接收 5 个必填参数(resx 所在目录、base 头文件名、目标头文件名、base rc 文件名、目标 rc 文件名)和 1 个可选参数(资源起始 ID,默认101,见脚本 第 18-26 行)。它的处理流程:

  1. 递归遍历目录中所有.resx文件,从文件名或父目录名解析语言代码(脚本 第 95-118 行;对zh-CN这类"语言+地区"形式会回退匹配纯语言zh);
  2. resgen把 resx 转成 rc 所需的字符串表格式,资源名恢复为IDS_前缀 + 全大写(还原为原始命名),字符串中的"一律转义为""以避免构建错误;
  3. 资源#define声明从 101 起依次编号,且只根据其中一种语言生成一次(避免重复编号);
  4. 每种语言的字符串表按如下格式追加到 rc 文件:
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US STRINGTABLE BEGIN strings END #endif

关键限制:语言代码表是硬编码的。由于没有 API 可以从流水线给出的 langId 反查AFX_TARG_*LANG_*SUBLANG_*值,脚本在 第 48-76 行 维护了一个语言哈希表,覆盖 en、zh-Hans/zh-CN、cs、hu、pl、ro、sk、bg、ru、ca、de、es、fr、it、nl、nb-NO、pt-BR、eu-ES、tr、he、ar、ja、ko、sv、pt-PT、zh-Hant/zh-TW 等语言。若未来本地化团队新增语言,必须同步更新这个哈希表,否则脚本会输出Unknown language警告并跳过该语言。要确定某个语言的代码,可以在 Resource View 中右键字符串表选Insert Copy并选择对应语言,工具会自动生成所需代码供参考。

生成物写入Generated Files目录(该目录被.gitignore忽略,且文件头部带有"auto-generated"警告注释)。因此:

  • 这两个生成文件内部的#include需要多加一层..\
  • 使用resource.h的代码要写成#include "Generated Files\resource.h"
  • base 文件加入 vcxproj 时应改为不参与构建的<None>项,避免与生成物冲突:
<None Include="Resources.resx" />

多工程共享 rc/resource.h 的情况:有些 rc/resource.h 被多个项目共用(例如 Keyboard Manager)。此时把构建事件提升到目录级Directory.Build.targets,保证任何项目开始构建前 rc 文件已生成。仓库中现成的例子是 keyboardmanager 的 Directory.Build.targets:

<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild"> <Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 ..\dll resource.base.h resource.h KeyboardManager.base.rc KeyboardManager.rc" /> </Target>

消费字符串:C++ 侧统一使用GET_RESOURCE_STRING(resource_id)宏读取字符串表,该宏定义在 src/common/utils/resources.h 第 210-211 行:

#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)

GET_RESOURCE_STRING_FALLBACK在资源缺失时提供回退字符串,是本地化资源尚未就绪时的稳健选择。

3.2 C# 项目:直接纳入 resx

C# 项目原生支持resx,唯一要做的是把生成的各语言 resx 纳入构建:

  • .NET Core 项目:自动包含,csproj无需改动;
  • 其他项目:在 csproj 中加一行:
<EmbeddedResource Include="Properties\Resources.*.resx" />

两个已知注意事项:

  1. 带本地化资源构建时可能出现警告Referenced assembly 'mscorlib.dll' targets a different processor,这是 Visual Studio 的已知 bug,可忽略;
  2. XAML 资源迁移到 resx:若项目原来用 XAMLSystem.String资源,最简迁移路径是把资源改成=分隔的纯文本(手工全局替换或脚本),再用resgen转为 resx。例如把
<system:String x:Key="wox_plugin_calculator_plugin_name">Calculator</system:String> <system:String x:Key="wox_plugin_calculator_plugin_description">Allows to do mathematical calculations.(Try 5*3-2 in Wox)</system:String> <system:String x:Key="wox_plugin_calculator_not_a_number">Not a number (NaN)</system:String>

改写为

wox_plugin_calculator_plugin_name=Calculator wox_plugin_calculator_plugin_description=Allows to do mathematical calculations.(Try 5*3-2 in Wox) wox_plugin_calculator_not_a_number=Not a number (NaN)

然后在Developer Command Prompt for VS中运行resgen转成 resx。resx 加入工程并配置资源生成器后,代码中对字符串的引用要改为Properties.Resources.resName,替换掉原来的自定义 API。

3.3 UWP 项目:resw 通配包含

UWP 项目期望resw文件(格式与 resx 几乎相同),但文件组织形式不同:必须位于fullLangId\Resources.resw路径下。因此要把 csproj 中单语言的包含:

<PRIResource Include="Strings\en-us\Resources.resw" />

替换为通配形式,以纳入流水线生成的全部语言目录:

<PRIResource Include="Strings\*\Resources.resw" />

当前仓库中 cmdpal UI、PowerDisplay、RegistryPreview 等模块均采用这种Strings\en-us\Resources.resw布局。

四、lcl 文件格式与防失效机制

lcl文件包含英文 resx 中的全部资源;若某条资源已有译文,则一并附上。一条资源的 lcl 条目形如:

<Item ItemId=";EditKeyboard_WindowName" ItemType="0;.resx" PsrId="211" Leaf="true"> <Str Cat="Text"> <Val><![CDATA[Remap keys]]></Val> <Tgt Cat="Text" Stat="Loc" Orig="New"> <Val><![CDATA[Remapper des touches]]></Val> </Tgt> </Str> <Disp Icon="Str" /> </Item>

结构要点:

  • <Val><Str>直属)是英文原文;
  • <Tgt>元素是译文容器,Stat="Loc"表示已翻译,Orig="New"表示新字符串。lcl 文件的初始提交中只有英文,没有<Tgt>元素
  • 条目结构对应 KeyboardManagerEditor 的 Resources.resx 中EditKeyboard_WindowName这样的资源键。

防失效(fail-safe)机制:CDPX 本地化系统对 lcl 文件做一致性检查——若<Val><![CDATA[*]]></Val>中的英文字符串与英文Resources.resx中的值不一致,则该条译文不会被复制到本地化 resx 中。这样设计的目的是:当英文资源被修改后,过期的旧译文不会被加载,程序会回退使用英文原文,等待本地化团队更新译文。这决定了上游团队的实践约束:修改英文 resx 字符串时,旧译文会自动失效回退为英文,不会出现"旧译文配新语境"的错乱

五、LEGO 本地化 PR 的常见合并问题

LEGO PR(本地化团队提交的批量翻译 PR)一次只更新部分字符串,多个 PR 可能同时修改同一批文件,从而产生合并冲突。大多数冲突会在 GitHub 上明确显示,但偶尔会出现"坏合并":文件表面合并成功,实际格式已损坏,例如单个资源出现两个<Tgt>元素。排查与修复手段:

  • 按第四节的 lcl 条目格式校正损坏文件,确保每条资源至多一个<Tgt>
  • 每个 LEGO PR 都应跑一遍 build farm,若本地化步骤报错,检查对应项目的 resx/lcl 文件是否存在残留冲突标记或重复元素。

六、为新项目启用本地化 MSI

6.1 C++ 与 UWP:无需额外操作

C++ 项目的所有资源编译进 dll/exe 本身,UWP 项目的资源进入resources.pri(未本地化的项目同样有该文件),因此这两类项目不产生需要额外加入 MSI 的本地化文件

验证 UWP 资源是否成功写入resources.pri的方法:

  1. 打开Developer Command Prompt for VS
  2. 进入 pri 文件所在目录,运行:
makepri.exe dump /if .\resources.pri
  1. 检查生成的resources.pri.xml,其末尾包含各语言的资源候选项,例如:
<NamedResource name="GeneralSettings_RunningAsAdminText" uri="ms-resource://f4f787a5-f0ae-47a9-be89-5408b1dd2b47/Resources/GeneralSettings_RunningAsAdminText"> <Candidate qualifiers="Language-FR" type="String"> <Value>Running as administrator</Value> </Candidate> <Candidate qualifiers="Language-EN-US" isDefault="true" type="String"> <Value>Running as administrator</Value> </Candidate> </NamedResource>

6.2 C#:卫星程序集加入 MSI

C# 项目构建时会为每种语言生成卫星程序集:项目ProjName会产出langId\ProjName.resources.dlllangId格式与 lcl 文件一致)。这些卫星 dll 必须加入 MSI,但只能来自流水线构建的解决方案——本地机器上没有本地化 resx,无条件引用会导致本地安装器构建失败。

做法是在 installer 目录下的 Product.wxs 中,把项目目录名加入受IsPipeline检查控制的本地化资源列表,并按以下模式为项目创建资源组件:

<Component Id="ProjName_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)ProjNameInstallFolder"> <File Id="ProjName_$(var.IdSafeLanguage)_File" Source="$(var.BinX64Dir)modules\ProjName\$(var.Language)\ProjName.resources.dll" /> </Component>

两个配套要点:

  1. 签名:确保新增 dll 被流水线签名。当前所有*.resources.dll形式的程序集都在流水线签名清单中;
  2. 时机:卫星 dll 的 MSI 组件应在本地化团队完成 lcl 文件初始提交之后再加——否则流水线上不存在任何 resx 可用来生成 dll,流水线会失败。

七、字符串使用规范(Working With Strings)

要支持本地化,代码中不得出现硬编码的 UI 显示字符串,必须通过资源文件取字符串。

7.1 C++

StringTable资源存储字符串,用resource.h存储与字符串绑定的 ID,配合 Visual Studio 资源编辑器维护:

resource.h(XXX 必须唯一,通常取最后一个字符串 ID + 1):

#define IDS_MODULE_DISPLAYNAME XXX

资源定义脚本validmodulename.rc

STRINGTABLE BEGIN IDS_MODULE_DISPLAYNAME L"Module Name" END

代码中消费:

#include <common.h> std::wstring s = GET_RESOURCE_STRING(IDS_MODULE_DISPLAYNAME);

7.2 C#

用 XML 资源文件(.resx)存储 UI 字符串,用ResourceManager消费:

<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>

手工消费:

System.Resources.ResourceManager manager = new System.Resources.ResourceManager(baseName, assembly); string validUIDisplayString = manager.GetString("ValidUIDisplayString", resourceCulture);

若资源文件由 Visual Studio 生成,直接使用自动生成的Resources.Designer.cs封装的Resources类即可:

string validUIDisplayString = Resources.ValidUIDisplayString;

八、小结

PowerToys 的本地化体系可以归纳为一条主链:英文 resx/resw 是唯一的本地化源头→ CDPX 流水线通过LocProject.jsonLocalization.XLoc生成loc目录下的 lcl 文件 → 本地化团队在 lcl 中补充译文 → 流水线生成各语言 resx/resw → C# 产出卫星 dll(经IsPipeline检查加入 MSI)、C++ 经convert-resx-to-rc.ps1生成本地化 rc/resource.h 编译进二进制、UWP 汇入resources.pri。开发者的日常职责落在两端:一端是按第三、七节的规范建立可翻译资源(新模块配LocProject.json、C++ 项目挂接GenerateResourceFiles构建事件、UWP 用通配PRIResource);另一端是理解 lcl 防失效机制与 LEGO PR 冲突处理,保证英文字符串变更与译文更新之间的安全衔接。需要扩展支持语言时,记得同步维护 convert-resx-to-rc.ps1 中硬编码的语言代码表。

【免费下载链接】PowerToysMicrosoft PowerToys is a collection of utilities that supercharge productivity and customization on Windows项目地址: https://gitcode.com/GitHub_Trending/po/PowerToys

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询