在旧应用或游戏中使用 Windows 10 资源管理系统

.NET 和 Win32 应用和游戏通常已本地化为不同的语言,以扩展其潜在市场总额。 有关对应用进行本地化的价值主张的详细信息,请参阅全球化和本地化。 通过将 .NET 或 Win32 应用或游戏打包为 .msix 或 .appx 包,可以利用资源管理系统加载已为运行时上下文定制的应用资源。 本主题对方法进行了深入描述。

可通过许多方法本地化传统 Win32 应用程序,但 Windows 8 引入了新的资源管理系统,适用于不同的编程语言、应用程序类型,并提供除简单的本地化之外的更多功能。 此系统在本主题中称为“MRT”。 过去,这代表“现代资源技术”,但现在已去掉“现代”一词。 资源管理器也称为现代资源管理器 (MRM) 或包资源索引 (PRI)。

与基于 MSIX 或基于 .appx 的部署(例如,来自 Microsoft Store)相结合,MRT 可以自动为指定用户/设备提供最适用的资源,这可以最大程度地减小应用程序下载和安装的大小。 对于具有大量本地化内容的应用程序来说,此大小缩减可能较为显著,对于 AAA 游戏,可能达到数 GB 阶次。 MRT 的其他优势包括 Windows Shell 和 Microsoft Store 中的本地化列表,当用户的首选语言与可用资源不匹配时,自动回退逻辑。

本文档介绍 MRT 的高级体系结构,并提供一个移植指南,以帮助将旧版 Win32 应用程序移动到 MRT,同时尽可能减少代码更改。 迁移到 MRT 后,开发人员可以使用其他优势,例如能够按比例系数或系统主题细分资源。 请注意,基于 MRT 的本地化适用于通过桌面桥(即“Centennial”)处理的 UWP 应用程序和 Win32 应用程序。

在许多情况下,你可以继续使用现有本地化格式和源代码,同时与 MRT 集成以在运行时解析资源,并尽可能降低下载大小 - 这不是一种全有或全无方法。 下表汇总了每个阶段的工作和估计成本/效益。 此表不包括非本地化任务,例如提供高分辨率或高对比度应用程序图标。 有关为磁贴、图标等提供多个资源的详细信息,请参阅针对语言、比例、高对比度和其他限定符定制资源

工作 好处 估计成本
本地化程序包清单 在 Windows Shell 和 Microsoft Store 中显示本地化内容所需的最低工作量 小型
使用 MRT 识别和定位资源 尽可能减小下载和安装大小的先决条件;自动语言回退
生成资源包 尽可能减小下载和安装大小的最后一步 小型
迁移到 MRT 资源格式和 API 显著减小的文件大小(具体取决于现有资源技术) 大型

介绍

大多数重要应用程序都包含与应用程序代码(这与在源代码本身中创作的硬编码值不同)无关的用户界面元素(称为资源)。 出于多种原因,首选资源而不是硬编码值,例如方便非开发人员编辑,但关键原因之一是支持应用程序在运行时选取逻辑资源相同的不同表示形式。 例如,在按钮上显示的文本(或在图标中显示的图像)可能有所不同,具体取决于用户理解的语言、显示设备的特征,或者用户是否启用了任何辅助技术。

因此,任何资源管理技术的主要目的是在运行时将逻辑或符号资源名称(例如 SAVE_BUTTON_LABEL)请求转换为可能候选项集(例如,“保存”、“Speichern”或“저장”)中的最佳可行实际。 MRT 提供此类功能,并支持应用程序使用各种属性(称为限定符)识别资源候选项,例如用户语言、显示比例系数、用户选择的主题和其他环境因素。 MRT 甚至支持需要它的应用程序的自定义限定符(例如,应用程序可以为使用帐户登录的用户和来宾用户提供不同的图形资源,而无需将此检查显式添加到其应用程序的每个部分)。 MRT 适用于字符串资源和基于文件的资源,其中基于文件的资源作为对外部数据(文件本身)的引用实施。

示例

下面是在两个按钮(openButtonsaveButton)上使用文本标签并将 PNG 文件用于徽标 (logoImage) 的应用程序的简单示例。 文本标签已本地化为英语和德语,徽标已针对普通桌面显示器(100% 比例系数)和高分辨率手机(300% 比例系数)进行优化。 请注意,此关系图提供了模型的高级概念视图;它不完全映射到实施。

源代码标签、查阅表标签和磁盘标签上的文件的屏幕截图。

在该图中,应用程序代码引用了三个逻辑资源名称。 在运行时,GetResource 伪函数使用 MRT 在资源表(称为 PRI 文件)中查找这些资源名称,并根据环境条件(用户语言和显示器比例系数)找到最合适的候选项。 对于标签,直接使用字符串。 对于徽标图像,字符串将解释为文件名,并将从磁盘读取文件。

如果用户使用英语或德语以外的语言,或者显示比例系数不是 100% 或 300%,MRT 将基于一组回退规则选取“最接近”的匹配候选项(请参阅资源管理系统以了解更多背景)。

请注意,MRT 支持针对多个限定符定制的资源 - 例如,如果徽标图像包含的嵌入文本也需要本地化,则徽标将具有四个候选项:EN/Scale-100、DE/Scale-100、EN/Scale-300 和 DE/Scale-300。

本文档中的各节

以下几节概述了将 MRT 与应用程序集成所需的高级任务。

第 0 阶段:生成应用程序包

本节概述了如何将现有桌面应用程序生成为应用程序包。 在此阶段中未使用任何 MRT 功能。

第 1 阶段:本地化应用程序清单

本节概述了如何本地化应用程序清单(以在 Windows Shell 中正确显示),同时仍使用旧版资源格式和 API 打包和定位资源。

第 2 阶段:使用 MRT 识别和定位资源

本节概述了如何修改应用程序代码(以及可能的资源布局)以使用 MRT 定位资源,同时仍使用现有资源格式和 API 来加载和使用资源。

第 3 阶段:生成资源包

本节概述了将资源分成单独的资源包从而最大程度地减少应用下载(和安装)大小所需的最终更改。

本文档不包含的内容

完成上述阶段 0-3 之后,你将有一个可以提交到 Microsoft Store 的应用程序“捆绑包”,它们还可以让用户通过忽略不需要的资源(例如,他们不使用的语言)最小化下载和安装大小。 可通过采取最后一步进一步改进应用程序大小和功能。

第 4 阶段:迁移到 MRT 资源格式和 API

本文档不包含此阶段;它需要将资源(尤其是字符串)从旧格式(如 MUI DLL 或 .NET 资源程序集)移动到 PRI 文件中。 这能够为下载和安装进一步节省空间。 它还允许使用其他 MRT 功能,例如,根据比例系数、辅助功能设置等最大程度地减小映像文件的下载和安装大小。

第 0 阶段:生成应用程序包

在对应用程序资源进行任何更改之前,必须先将当前的打包和安装技术替换为标准的 UWP 打包和部署技术。 可通过三种方法执行此操作:

  • 如果你有安装程序复杂的较大的桌面应用程序,或利用大量操作系统扩展点,你可以使用 Desktop App Converter 工具从现有的应用安装程序(例如 MSI)生成 UWP 文件布局和清单信息。
  • 如果你有文件相对较少或安装程序简单的较小的桌面应用程序,且不连接扩展点,你可以手动创建文件布局和清单信息。
  • 如果你在从源重建,并且想要将应用更新为纯 UWP 应用程序,你可以在 Visual Studio 中创建新项目,并依靠 IDE 为你完成大量工作。

如果你想要使用 Desktop App Converter,请参阅使用 Desktop App Converter 打包桌面应用程序了解有关转换过程的详细信息。 有关完整的桌面转换器示例集,请参阅桌面桥到 UWP 示例 GitHub 存储库

如果你想要手动创建程序包,你将需要创建包含所有应用程序文件(可执行文件和内容,但不包括源代码)和程序包清单文件 (.appxmanifest) 的目录结构。 示例可以在 Hello, World GitHub 示例中找到,但运行名为 ContosoDemo.exe 的桌面可执行文件的基本程序包清单文件如下所示,其中的突出显示文本会被替换为你自己的值。

<?xml version="1.0" encoding="utf-8" ?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
         xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
         xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
         xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
         IgnorableNamespaces="uap mp rescap">
    <Identity Name="Contoso.Demo"
              Publisher="CN=Contoso.Demo"
              Version="1.0.0.0" />
    <Properties>
    <DisplayName>Contoso App</DisplayName>
    <PublisherDisplayName>Contoso, Inc</PublisherDisplayName>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>
    <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" 
                        MaxVersionTested="10.0.14393.0" />
  </Dependencies>
    <Resources>
    <Resource Language="en-US" />
  </Resources>
    <Applications>
    <Application Id="ContosoDemo" Executable="ContosoDemo.exe" 
                 EntryPoint="Windows.FullTrustApplication">
    <uap:VisualElements DisplayName="Contoso Demo" BackgroundColor="#777777" 
                        Square150x150Logo="Assets\Square150x150Logo.png" 
                        Square44x44Logo="Assets\Square44x44Logo.png" 
        Description="Contoso Demo">
      </uap:VisualElements>
    </Application>
  </Applications>
    <Capabilities>
    <rescap:Capability Name="runFullTrust" />
  </Capabilities>
</Package>

有关程序包清单文件和程序包布局的详细信息,请参阅应用程序包清单

最后,如果你使用 Visual Studio 创建新项目并横向迁移现有代码,请参阅创建“Hello, World”应用。 你可以将现有代码包含到新项目中,但你可能必须进行大范围的代码更改(尤其是在用户界面)以便作为纯 UWP 应用运行。 本文档未介绍这些更改。

第 1 阶段:本地化清单

步骤 1.1:更新清单中的字符串和资源

在阶段 0 中,你为应用程序创建了一个基本程序包清单 (.appxmanifest) 文件(基于提供给转换器、从 MSI 提取或手动输入到清单的值),但它既不包含本地化的信息,也不会支持其他功能,如高分辨率“开始”磁贴资源等。

若要确保你的应用程序的名称和描述能够正确本地化,必须在一组资源文件中定义一些资源,并更新程序包清单来引用它们。

创建默认资源文件

第一步是使用默认语言(例如美式英语)创建默认资源文件。 可以使用文本编辑器或通过 Visual Studio 中的资源设计器手动执行此操作。

如果要手动创建资源:

  1. 创建一个名为 resources.resw 的 XML 文件,并将其置于项目的 Strings\en-us 子文件夹中。 如果你的默认语言不是美国英语,应使用合适的 BCP-47 代码。
  2. 在 XML 文件中,添加以下内容,其中突出显示的文本将替换为适用于您的应用的文本(采用默认语言)。

注意

其中有一些字符串的长度受到限制。 有关详细信息,请参阅 VisualElements

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="ApplicationDescription">
    <value>Contoso Demo app with localized resources (English)</value>
  </data>
  <data name="ApplicationDisplayName">
    <value>Contoso Demo Sample (English)</value>
  </data>
  <data name="PackageDisplayName">
    <value>Contoso Demo Package (English)</value>
  </data>
  <data name="PublisherDisplayName">
    <value>Contoso Samples, USA</value>
  </data>
  <data name="TileShortName">
    <value>Contoso (EN)</value>
  </data>
</root>

如果要使用 Visual Studio 中的设计器:

  1. 在你的项目中创建 Strings\en-us 文件夹(或视情况使用其他语言),将新项目添加到你的项目的根文件夹中(使用 resources.resw 默认名称)。 请务必选择资源文件 (.resw) 而非资源字典 - 资源字典是 XAML 应用程序使用的文件
  2. 使用设计器输入以下字符串(使用相同的 Names,但将 Values 替换为适用于应用程序的文本):

显示 Resources.resw 文件的屏幕截图,其中显示了“名称和值”列。用于资源。

注意

如果你从 Visual Studio 设计器,你始终可以通过按 F7 直接编辑 XML。 但是,如果首先使用的是最小的 XML 文件,设计器将无法识别该文件,因为它缺少大量附加元数据;要解决此问题,可通过将样本 XSD 信息从设计器生成的文件复制到手动编辑的 XML 文件中。

更新清单以引用资源

.resw 文件中定义值后,下一步是更新清单以引用资源字符串。 同样,可以直接编辑 XML 文件,或依赖于 Visual Studio 清单设计器。

如果直接编辑 XML,打开 AppxManifest.xml 文件,对突出显示的值进行以下更改 - 使用此精确文本,而不是特定于应用程序的文本。 对于使用这些具体的资源名称没有要求(你可以选择自己的名称),但不论你如何选择,所选名称都必须与 .resw 文件中的名称完全一致。 这些名称应与在 .resw 文件中创建的 Names 匹配,其前缀为 ms-resource: 方案和 Resources/ 命名空间。

注意

已从此代码段省略了清单的许多元素 - 不要再删除任何内容!

<?xml version="1.0" encoding="utf-8"?>
<Package>
  <Properties>
    <DisplayName>ms-resource:Resources/PackageDisplayName</DisplayName>
    <PublisherDisplayName>ms-resource:Resources/PublisherDisplayName</PublisherDisplayName>
  </Properties>
  <Applications>
    <Application>
      <uap:VisualElements DisplayName="ms-resource:Resources/ApplicationDisplayName"
        Description="ms-resource:Resources/ApplicationDescription">
        <uap:DefaultTile ShortName="ms-resource:Resources/TileShortName">
          <uap:ShowNameOnTiles>
            <uap:ShowOn Tile="square150x150Logo" />
          </uap:ShowNameOnTiles>
        </uap:DefaultTile>
      </uap:VisualElements>
    </Application>
  </Applications>
</Package>

如果你使用 Visual Studio 清单设计器,打开 .appxmanifest 文件,更改“应用程序”选项卡和“打包”选项卡中的突出显示

Visual Studio 清单设计器的屏幕截图,其中显示了“应用程序”选项卡,其中标出了“显示名称和说明”文本框。

Visual Studio 清单设计器的屏幕截图,其中显示了“打包”选项卡,其中标出了“包显示名称和发布者显示名称”文本框。

步骤 1.2:生成 PRI 文件,创建 MSIX 程序包,确认是否能够正常使用

现在,应能够生成 .pri 文件并部署应用程序,以验证“开始”菜单中是否显示正确的信息(采用默认语言)。

如果要在 Visual Studio 中生成,只需按下 Ctrl+Shift+B 生成项目,然后右键单击该项目并从关联菜单中选择 Deploy

如果你手动生成,请按照以下步骤创建 MakePRI 工具的配置文件,并生成 .pri 文件本身(可以在手动应用打包中找到更多信息):

  1. 从“开始”菜单中的 Visual Studio 2017 或 Visual Studio 2019 文件夹打开开发人员命令提示符

  2. 切换到项目根目录(包含 .appxmanifest 文件和 Strings 文件夹的目录)。

  3. 键入以下命令,将“contoso_demo.xml”替换为适用于项目的名称,并将“en-US”替换为应用的默认语言(或保留 en-US 不变,如果适用)。 请注意,XML 文件在父目录中创建(不是在项目目录中),因为它不是应用程序的一部分(你可以选择你需要的任何其他目录,但请务必在将来的命令中替换它)

    makepri createconfig /cf ..\contoso_demo.xml /dq en-US /pv 10.0 /o
    

    可以键入 makepri createconfig /? 以查看每个参数的作用,总而言之:

    • /cf 设置配置文件(此命令的输出)
    • /dq 设置默认限定符,在此为语言 en-US
    • /pv 设置平台版本,在此为 Windows 10
    • /o 设置为“覆盖输出文件”(如果存在)
  4. 现在,你具有一个配置文件,再次运行 MakePRI,在磁盘中实际搜索资源并将其打包到 PRI 文件中。 将“contoso_demop.xml”替换为在上一步中使用的 XML 文件名,并确保为输入和输出指定父目录:

    makepri new /pr . /cf ..\contoso_demo.xml /of ..\resources.pri /mf AppX /o
    

    可以键入 makepri new /? 以查看每个参数的作用,总而言之:

    • /pr 设置项目根目录(在此为当前目录)
    • /cf 设置配置文件名,在上一步中创建
    • /of 设置输出文件
    • /mf 创建映射文件(以便我们可以在后面的步骤排除包中的文件)
    • /o 设置为“覆盖输出文件”(如果存在)
  5. 现在,你具有包含默认语言资源(例如 en-US)的 .pri 文件。 要验证其是否正常运行,可以运行以下命令:

    makepri dump /if ..\resources.pri /of ..\resources /o
    

    可以键入 makepri dump /? 以查看每个参数的作用,总而言之:

    • /if 设置输入文件名
    • /of 设置输出文件名(.xml 将自动附加)
    • /o 设置为“覆盖输出文件”(如果存在)
  6. 最后,可以在文本编辑器中打开 ..\resources.xml,验证其是否列出 <NamedResource> 值(如 ApplicationDescriptionPublisherDisplayName),以及针对所选默认语言的 <Candidate> 值(文件开头会有其他内容;暂时忽略该内容)。

你可以打开映射文件 ..\resources.map.txt 以确认其中含有项目所需的文件(包括 PRI 文件,它不是项目目录的一部分)。 重要的是,映射文件包括对 resources.resw 文件的引用,因为该文件的内容已嵌入 PRI 文件中。 但是,它将包含其他资源,例如图像的文件名。

生成包并进行签名

现已生成 PRI 文件,可以生成包并进行签名:

  1. 若要创建应用包,运行以下命令,将 contoso_demo.appx 替换为你要创建的 .msix/.appx 文件的名称,并确保为文件选择不同目录(本示例使用父目录;它可以在任何位置,但不应该是项目目录)。

    makeappx pack /m AppXManifest.xml /f ..\resources.map.txt /p ..\contoso_demo.appx /o
    

    可以键入 makeappx pack /? 以查看每个参数的作用,总而言之:

    • /m 设置要使用的清单文件
    • /f 设置要使用的映射文件(在上一步中创建)
    • /p 设置输出程序包名称
    • /o 设置为“覆盖输出文件”(如果存在)
  2. 创建程序包后,必须对它进行签名。 获取签名证书的最简单方法是在 Visual Studio 中创建一个空的通用 Windows 项目,并复制项目创建的 .pfx 文件,不过你可以使用 MakeCertPvk2Pfx 实用工具手动创建,如如何创建应用包签名证书中所述。

    重要

    如果你手动创建签名证书,请确保将文件放在不同于源项目或包源的目录中,否则它可能会被作为程序包的一部分,包括私钥!

  3. 要对包进行签名,请使用以下命令。 请注意,在 AppxManifest.xmlIdentity 元素中指定的 Publisher 必须与证书的 Subject 匹配(这不是 <PublisherDisplayName>,后者是要向用户显示的本地化显示名称)。 像往常一样,将 contoso_demo... 文件名替换为适合项目的名称,并且(非常重要)确保 .pfx 文件不在当前目录下(否则,该文件将作为包的一部分进行创建,包括签名私钥!):

    signtool sign /fd SHA256 /a /f ..\contoso_demo_key.pfx ..\contoso_demo.appx
    

    可以键入 signtool sign /? 以查看每个参数的作用,总而言之:

    • /fd 设置文件摘要算法(SHA256 是 .appx 的默认值)
    • /a 将自动选择最佳证书
    • /f 指定包含签名证书的输入文件

最后,可以双击 .appx 文件进行安装,或者如果你更喜欢命令行,可以打开 PowerShell 提示符,更改为包含包的目录,然后键入以下内容(将 contoso_demo.appx 替换为包名称):

add-appxpackage contoso_demo.appx

如果收到有关证书不受信任的错误,确保该证书已添加到计算机存储(而不是用户存储)。 要将证书添加到计算机存储,可以使用命令行或 Windows 资源管理器。

要使用命令行:

  1. 以管理员身份运行 Visual Studio 2017 或 Visual Studio 2019 命令提示符。

  2. 切换到包含 .cer 文件的目录(切记确保该文件不在源目录或项目目录内!)

  3. 键入以下命令,并将 contoso_demo.cer 替换为文件名:

    certutil -addstore TrustedPeople contoso_demo.cer
    

    可以运行 certutil -addstore /? 以查看每个参数的作用,总而言之:

    • -addstore 将证书添加到证书存储
    • TrustedPeople 指示证书放置的存储

要使用 Windows 资源管理器:

  1. 导航到包含 .pfx 文件的文件夹
  2. 双击 .pfx 文件,随即应显示“证书导入向导”
  3. 选择“Local Machine”并单击“Next
  4. 接受用户帐户控制管理员提升权限提示(如果显示)并单击“Next
  5. 输入私钥的密码(如果有),然后单击“Next
  6. 选择 Place all certificates in the following store
  7. 单击 Browse,然后选择 Trusted People 文件夹(而不是“受信任的发布者”)
  8. 单击“Next”,然后单击“Finish

将证书添加到 Trusted People 存储后,请尝试再次安装包。

现在应看到应用在“开始”菜单的“所有应用”列表中显示,其中包含 .resw / .pri 文件中的正确信息。 如果看到空白字符串或字符串 ms-resource:...,则表示出现故障 - 请仔细检查编辑并确保其正确。 如果在“开始”菜单中右键单击应用,则可以将其固定为磁贴,并验证其中是否同时显示正确的信息。

步骤 1.3:添加更多受支持的语言

对程序包清单进行更改并且 resources.resw 文件已创建后,添加其他语言很简单。

创建其他本地化资源

首先,创建其他本地化资源值。

Strings 文件夹中,使用相应的 BCP-47 代码(例如 Strings\de-DE)为每种受支持的语言创建其他文件夹。 在每个文件夹中,创建包含已翻译资源值的 resources.resw 文件(使用 XML 编辑器或 Visual Studio 设计器)。 假设可用的本地化字符串位于某个位置,只需将其复制到 .resw 文件中;本文档不涵盖翻译步骤本身。

例如,Strings\de-DE\resources.resw 文件可能如下所示,其中突出显示的文本之前为 en-US

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="ApplicationDescription">
    <value>Contoso Demo app with localized resources (German)</value>
  </data>
  <data name="ApplicationDisplayName">
    <value>Contoso Demo Sample (German)</value>
  </data>
  <data name="PackageDisplayName">
    <value>Contoso Demo Package (German)</value>
  </data>
  <data name="PublisherDisplayName">
    <value>Contoso Samples, DE</value>
  </data>
  <data name="TileShortName">
    <value>Contoso (DE)</value>
  </data>
</root>

以下步骤假定你为 de-DEfr-FR 添加了资源,但对于任何语言,都可以遵循相同的模式。

更新程序包清单以列出支持的语言

程序包清单必须更新,以列出应用支持的语言。 Desktop App Converter 添加默认语言,但必须显式添加其他语言。 如果直接编辑 AppxManifest.xml 文件,请如下所示更新 Resources 节点,添加所需数量的元素,替换所支持的适当语言,并确保列表中的第一项是默认(回退)语言。 在此示例中,默认值为英语(美国),并额外支持德语(德国)和法语(法国):

<Resources>
  <Resource Language="EN-US" />
  <Resource Language="DE-DE" />
  <Resource Language="FR-FR" />
</Resources>

如果当前使用的是 Visual Studio,则无需执行任何操作;在查看 Package.appxmanifest 时,应看到特殊的 x-generate 值,该值会导致生成过程插入在项目中找到的语言(基于使用 BCP-47 代码命名的文件夹)。 请注意,这不是真正的程序包清单的有效值;仅适用于 Visual Studio 项目:

<Resources>
  <Resource Language="x-generate" />
</Resources>

使用本地化值重新生成

现在,你可以再次生成和部署应用程序,如果在 Windows 中更改了语言首选项,则应在“开始”菜单中看到新的本地化值(下文说明了如何更改语言)。

对于 Visual Studio,可以同样仅使用 Ctrl+Shift+B 进行生成,然后右键单击该项目进行 Deploy

如果要手动生成项目,请按照上述相同步骤执行操作,但在创建配置文件时将其他语言(以下划线分隔)添加到默认限定符列表 (/dq)。 例如,要支持上一步中添加的英语、德语和法语资源:

makepri createconfig /cf ..\contoso_demo.xml /dq en-US_de-DE_fr-FR /pv 10.0 /o

这将创建包含可轻松用于测试的所有指定语言的 PRI 文件。 如果总资源大小较小,或者仅支持少量语言,则对于交货应用来说,这可以接受;仅当你希望获得最大程度地减少资源安装/下载大小带来的好处时,才需要额外执行生成单独语言包操作。

使用本地化值进行测试

要测试新的本地化更改,只需向 Windows 添加新的首选 UI 语言。 无需下载语言包、重新启动系统或让整个 Windows UI 以外语显示。

  1. 运行 Settings 应用 (Windows + I)
  2. 转到 Time & language
  3. 转到 Region & language
  4. 单击 Add a language
  5. 键入(或选择)所需语言(例如 DeutschGerman
  • 如果存在子语言,请选择所需的语言(例如 Deutsch / Deutschland
  1. 在语言列表中选择新语言
  2. 单击 Set as default

现在打开“开始”菜单并搜索应用程序,应看到所选语言的本地化值(其他应用也可能显示为已本地化)。 如果未立即看到本地化名称,请等待几分钟,直到“开始”菜单缓存刷新。 要恢复为本机语言,只需在语言列表中选择默认语言。

步骤 1.4:本地化更多程序包清单部分(可选)

可以本地化程序包清单的其他部分。 例如,当应用程序处理文件扩展名时,它应在清单中具有 windows.fileTypeAssociation 扩展名,使用所示完全相同的绿色突出显示文本(因为它将引用资源),并将黄色突出显示文本替换为特定于应用程序的信息:

<Extensions>
  <uap:Extension Category="windows.fileTypeAssociation">
    <uap:FileTypeAssociation Name="default">
      <uap:DisplayName>ms-resource:Resources/FileTypeDisplayName</uap:DisplayName>
      <uap:Logo>Assets\StoreLogo.png</uap:Logo>
      <uap:InfoTip>ms-resource:Resources/FileTypeInfoTip</uap:InfoTip>
      <uap:SupportedFileTypes>
        <uap:FileType ContentType="application/x-contoso">.contoso</uap:FileType>
      </uap:SupportedFileTypes>
    </uap:FileTypeAssociation>
  </uap:Extension>
</Extensions>

还可以通过 Visual Studio 清单设计器添加此信息,使用 Declarations 选项卡并记下突出显示值

Visual Studio 清单设计器的屏幕截图,其中显示了显示名称和信息提示文本框的“声明”选项卡。

现在,将相应的资源名称添加到每个 .resw 文件,并将突出显示文本替换为适合应用的文本(请记得为每个受支持的语言执行此操作!):

... existing content...
<data name="FileTypeDisplayName">
  <value>Contoso Demo File</value>
</data>
<data name="FileTypeInfoTip">
  <value>Files used by Contoso Demo App</value>
</data>

然后,这将在 Windows shell 的某些部分中显示,例如文件资源管理器:

显示 Contoso 演示应用使用的文件的工具提示的文件资源管理器屏幕截图。

像以前一样生成和测试包,执行任何应显示新 UI 字符串的新应用场景。

第 2 阶段:使用 MRT 识别和定位资源

上一节介绍了如何使用 MRT 本地化应用的清单文件,以便 Windows Shell 能够正确显示应用名称和其他元数据。 为此,无需进行任何代码更改;只需要使用 .resw 文件和一些其他工具。 本节介绍如何使用 MRT 查找采用现有资源格式的资源,并在尽可能减少更改的情况下使用现有资源处理代码。

有关现有文件布局和应用程序代码的假设

由于可通过许多方法本地化 Win32 桌面应用,因此本文将对需要映射到特定环境的现有应用程序结构做出一些简化假设。 可能需要对现有代码库或资源布局进行一些更改,以符合 MRT 的要求,这些内容大部分未在本文档中介绍。

资源文件布局

本文假设你的本地化的资源均具有相同的文件名(例如,contoso_demo.exe.muicontoso_strings.dllcontoso.strings.xml),但放在具有 BCP-47 名称(en-USde-DE 等)的不同文件夹内。 你有多少资源文件、文件名是什么、文件格式/相关 API 是什么等无关紧要。只有每个逻辑资源具有相同的文件名(但位于不同的物理目录中)才重要。

举一个反例,如果应用程序使用平面文件结构,而该结构具有包含文件 english_strings.dllfrench_strings.dll 的单一 Resources 目录,则该应用程序无法正确映射到 MRT。 更好的结构是包含子目录以及文件 en\strings.dllfr\strings.dllResources 目录。 也可以使用相同的基础文件名,但嵌入限定符(例如 strings.lang-en.dllstrings.lang-fr.dll),而使用具有语言代码的目录在概念上更为简单,因此这才是我们的关注点。

注意

即使你不能遵循此文件命名约定,也仍然有可能可以使用 MRT 和打包的好处;只是需要更多工作。

例如,应用程序可能在名为 ui.txt 的简单文本文件(位于 UICommands 文件夹下)中包含自定义 UI 命令集(用于按钮标签等):

+ ProjectRoot
|--+ Strings
|  |--+ en-US
|  |  \--- resources.resw
|  \--+ de-DE
|     \--- resources.resw
|--+ UICommands
|  |--+ en-US
|  |  \--- ui.txt
|  \--+ de-DE
|     \--- ui.txt
|--- AppxManifest.xml
|--- ...rest of project...

资源加载代码

本文假定在某一时刻,在你的代码中,你想要查找包含本地化的资源的文件,加载它,然后使用它。 用于加载资源的 API、用于提取资源的 API 等并不重要。 在伪代码中,基本上共分三步:

set userLanguage = GetUsersPreferredLanguage()
set resourceFile = FindResourceFileForLanguage(MY_RESOURCE_NAME, userLanguage)
set resource = LoadResource(resourceFile) 
    
// now use 'resource' however you want

MRT 只要求更改此过程的前两步 - 如何确定最佳候选资源以及如何找到这些资源。 它不需要更改加载或使用资源的方式(但是如果你想要利用资源,它提供了用于执行该操作的功能)。

例如,应用程序可以使用 Win32 API GetUserPreferredUILanguages、CRT 函数 sprintf 和 Win32 API CreateFile 替换上述三个伪代码函数,然后手动分析文本文件以查找 name=value 对。 (细节并不重要;这只是为了说明 MRT 对找到资源时用于处理资源的技术没有影响)。

步骤 2.1:更改代码以使用 MRT 定位文件

切换代码以使用 MRT 定位资源并不困难。 它需要使用少量 WinRT 类型和几行代码。 要使用的主要类型如下:

  • ResourceContext,用于封装当前处于活动状态的限定符值集(语言、比例系数等)
  • ResourceManager(WinRT 版本,而不是 .NET 版本),支持从 PRI 文件访问所有资源
  • ResourceMap,用于表示 PRI 文件中的特定资源子集(在此示例中,基于文件的资源与字符串资源)
  • NamedResource,用于表示逻辑资源及其所有可行候选项
  • ResourceCandidate,用于表示单个具体候选资源

在伪代码中,解析给定资源文件名(如上述示例中的 UICommands\ui.txt)的方式如下所示:

// Get the ResourceContext that applies to this app
set resourceContext = ResourceContext.GetForViewIndependentUse()
    
// Get the current ResourceManager (there's one per app)
set resourceManager = ResourceManager.Current
    
// Get the "Files" ResourceMap from the ResourceManager
set fileResources = resourceManager.MainResourceMap.GetSubtree("Files")
    
// Find the NamedResource with the logical filename we're looking for,
// by indexing into the ResourceMap
set desiredResource = fileResources["UICommands\ui.txt"]
    
// Get the ResourceCandidate that best matches our ResourceContext
set bestCandidate = desiredResource.Resolve(resourceContext)
   
// Get the string value (the filename) from the ResourceCandidate
set absoluteFileName = bestCandidate.ValueAsString

请注意,代码请求特定语言文件夹(例如 UICommands\en-US\ui.txt),即使这就是文件在磁盘上的存在方式。 相反,它会要求逻辑文件名 UICommands\ui.txt,并依靠 MRT 在语言目录之一中查找适当的磁盘文件。

在这里,示例应用可以像以前一样继续使用 CreateFile 加载 absoluteFileName 并分析 name=value 对;不需要在应用中更改该逻辑。 如果使用 C# 或 C++/CX 进行编程,则实际代码并不比这复杂得多(事实上,许多中间变量都可以省略)- 请参阅下面有关加载 .NET 资源的一节。 由于使用基于 COM 的低级 API 激活和调用 WinRT API,因此基于 C++/WRL 的应用程序将更为复杂,但采取的基本步骤相同 - 请参阅下面有关加载 Win32 MUI 资源的一节。

加载 .NET 资源

由于 .NET 内置用于定位和加载资源(称为“附属程序集”)的机制,因此在上述综合示例中没有要替换的显式代码 - 在 .NET 中,只需适当目录中的资源 DLL,系统会自动为你进行定位。 当应用使用资源包打包为 MSIX 或 .appx 时,目录结构会稍有不同 - 不会将资源目录作为应用程序主目录的子目录,它们是对等的(或者如果用户没有在首选项中列出语言,它们根本不会显示)。

例如,假设有一个 .NET 应用程序具有以下布局,其中所有文件位于 MainApp 文件夹下:

+ MainApp
|--+ en-us
|  \--- MainApp.resources.dll
|--+ de-de
|  \--- MainApp.resources.dll
|--+ fr-fr
|  \--- MainApp.resources.dll
\--- MainApp.exe

在转换为 .appx 后,布局外观如下所示,假设 en-US 是默认语言,并且用户语言列表中同时列出了德语和法语:

+ WindowsAppsRoot
|--+ MainApp_neutral
|  |--+ en-us
|  |  \--- MainApp.resources.dll
|  \--- MainApp.exe
|--+ MainApp_neutral_resources.language_de
|  \--+ de-de
|     \--- MainApp.resources.dll
\--+ MainApp_neutral_resources.language_fr
   \--+ fr-fr
      \--- MainApp.resources.dll

由于已本地化资源不再存在于主可执行文件安装位置下的子目录中,因此内置 .NET 资源解析将失败。 幸运的是,.NET 具有一个定义完善的机制,用于处理失败的程序集加载尝试 - AssemblyResolve 事件。 使用 MRT 的 .NET 应用必须注册此事件,并为 .NET 资源子系统提供缺失的程序集。

下面是如何使用 WinRT API 定位 .NET 使用的附属程序集的简明示例:所呈现的代码是经过有意压缩的,旨在尽可能减少实现,尽管你可以看到它与上面的伪代码很接近,传入的 ResolveEventArgs 用于提供我们需要找到的程序集的名称。 可以在 GitHub .NET 程序集解析程序示例的 PriResourceRsolver.cs 文件中找到此代码的可运行版本(带有详细注释和错误处理)。

static class PriResourceResolver
{
  internal static Assembly ResolveResourceDll(object sender, ResolveEventArgs args)
  {
    var fullAssemblyName = new AssemblyName(args.Name);
    var fileName = string.Format(@"{0}.dll", fullAssemblyName.Name);

    var resourceContext = ResourceContext.GetForViewIndependentUse();
    resourceContext.Languages = new[] { fullAssemblyName.CultureName };

    var resource = ResourceManager.Current.MainResourceMap.GetSubtree("Files")[fileName];

    // Note use of 'UnsafeLoadFrom' - this is required for apps installed with .appx, but
    // in general is discouraged. The full sample provides a safer wrapper of this method
    return Assembly.UnsafeLoadFrom(resource.Resolve(resourceContext).ValueAsString);
  }
}

对于上述类,需要在应用程序的启动代码中提前添加以下代码(在需要加载任何本地化资源之前):

void EnableMrtResourceLookup()
{
  AppDomain.CurrentDomain.AssemblyResolve += PriResourceResolver.ResolveResourceDll;
}

.NET 运行时在找不到资源 DLL 时会引发 AssemblyResolve 事件,届时提供的事件处理程序将通过 MRT 找到所需的文件并返回程序集。

注意

如果你的应用已有用于其他用途的 AssemblyResolve 处理程序,你需要将资源解决代码与现有代码结合。

加载 Win32 MUI 资源

加载 Win32 MUI 资源与加载 .NET 附属程序集基本相同,但改用 C++/CX 或 C++/WRL 代码。 使用 C++/CX 允许使用与上述 C# 代码非常匹配的更简单代码,但会使用 C++ 语言扩展、编译器开关和其他你可能希望避免的运行时开销。 如果是这样,使用 C++/WRL 可提供影响更低的解决方案,但需要提供更详细的代码。 然而,如果你熟悉 ATL 编程(或大概了解 COM),应该对 WRL 不陌生。

以下示例函数演示如何使用 C++/WRL 加载特定资源 DLL 并返回 HINSTANCE 以用于通过常规 Win32 资源 API 加载更多资源。 请注意,与显式使用 .NET 运行时请求的语言初始化 ResourceContext 的 C# 示例不同,此代码依赖于用户的当前语言。

#include <roapi.h>
#include <wrl\client.h>
#include <wrl\wrappers\corewrappers.h>
#include <Windows.ApplicationModel.resources.core.h>
#include <Windows.Foundation.h>
   
#define IF_FAIL_RETURN(hr) if (FAILED((hr))) return hr;
    
HRESULT GetMrtResourceHandle(LPCWSTR resourceFilePath,  HINSTANCE* resourceHandle)
{
  using namespace Microsoft::WRL;
  using namespace Microsoft::WRL::Wrappers;
  using namespace ABI::Windows::ApplicationModel::Resources::Core;
  using namespace ABI::Windows::Foundation;
    
  *resourceHandle = nullptr;
  HRESULT hr{ S_OK };
  RoInitializeWrapper roInit{ RO_INIT_SINGLETHREADED };
  IF_FAIL_RETURN(roInit);
    
  // Get Windows.ApplicationModel.Resources.Core.ResourceManager statics
  ComPtr<IResourceManagerStatics> resourceManagerStatics;
  IF_FAIL_RETURN(GetActivationFactory(
    HStringReference(
    RuntimeClass_Windows_ApplicationModel_Resources_Core_ResourceManager).Get(),
    &resourceManagerStatics));
    
  // Get .Current property
  ComPtr<IResourceManager> resourceManager;
  IF_FAIL_RETURN(resourceManagerStatics->get_Current(&resourceManager));
    
  // get .MainResourceMap property
  ComPtr<IResourceMap> resourceMap;
  IF_FAIL_RETURN(resourceManager->get_MainResourceMap(&resourceMap));
    
  // Call .GetValue with supplied filename
  ComPtr<IResourceCandidate> resourceCandidate;
  IF_FAIL_RETURN(resourceMap->GetValue(HStringReference(resourceFilePath).Get(),
    &resourceCandidate));
    
  // Get .ValueAsString property
  HString resolvedResourceFilePath;
  IF_FAIL_RETURN(resourceCandidate->get_ValueAsString(
    resolvedResourceFilePath.GetAddressOf()));
    
  // Finally, load the DLL and return the hInst.
  *resourceHandle = LoadLibraryEx(resolvedResourceFilePath.GetRawBuffer(nullptr),
    nullptr, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
    
  return S_OK;
}

第 3 阶段:生成资源包

现在你具有包含所有资源的“胖包”,可通过两种方式生成单独的主包和资源包,以最大程度地减少下载和安装大小:

  • 获取现有胖包并通过捆绑包生成器工具运行以自动创建资源包。 如果你的生成系统已生成一个胖包,并且想要对其进行后处理以生成资源包,则这是首选方法。
  • 直接生成单个资源包并将其整合到捆绑包中。 如果你可以更好地控制生成系统,并且可以直接生成包,则这是首选方法。

步骤 3.1:创建捆绑包

使用捆绑包生成器工具

要使用捆绑包生成器工具,需要手动更新为包创建的 PRI 配置文件才能移除 <packaging> 部分。

如果你使用 Visual Studio,请参阅 确保在设备上安装资源,不管设备是否需要资源,了解有关如何通过创建文件 priconfig.packaging.xmlpriconfig.default.xml 将所有语言构建到主包中的信息。

如果要手动编辑文件,请执行以下步骤:

  1. 按照与之前相同的方式创建配置文件,并替换正确的路径、文件名和语言:

    makepri createconfig /cf ..\contoso_demo.xml /dq en-US_de-DE_es-MX /pv 10.0 /o
    
  2. 手动打开创建的 .xml 文件并删除整个 &lt;packaging&rt; 部分(但保留其他所有内容):

    <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> 
    <resources targetOsVersion="10.0.0" majorVersion="1">
      <!-- Packaging section has been deleted... -->
      <index root="\" startIndexAt="\">
        <default>
        ...
        ...
    
  3. 使用更新的配置文件以及相应的目录和文件名生成 .pri 文件和 .appx 包(有关这些命令的详细信息,请参阅上文):

    makepri new /pr . /cf ..\contoso_demo.xml /of ..\resources.pri /mf AppX /o
    makeappx pack /m AppXManifest.xml /f ..\resources.map.txt /p ..\contoso_demo.appx /o
    
  4. 创建程序包后,使用以下命令创建捆绑包,使用相应的目录和文件名:

    BundleGenerator.exe -Package ..\contoso_demo.appx -Destination ..\bundle -BundleName contoso_demo
    

现在,你可以进入到最后一步,即签名(见下方)。

手动创建资源包

要手动创建资源包,需要运行略有不同的命令集来生成单独的 .pri.appx 文件 - 这些命令与上述用于创建胖包的命令相似,因此给出了最低说明。 注意:所有命令都假定当前目录是包含 AppXManifest.xml 文件的目录,但所有文件都放置在父目录中(如有必要,可以使用其他目录,但不应使用上述任何文件污染项目目录)。 与往常一样,将“Contoso”文件名替换为自己的文件名。

  1. 使用以下命令创建将默认语言指定为默认限定符的配置文件 - 在本例中为 en-US

    makepri createconfig /cf ..\contoso_demo.xml /dq en-US /pv 10.0 /o
    
  2. 使用以下命令为主包创建默认 .pri 文件和 .map.txt 文件,并为在项目中找到的每种语言额外创建一组文件:

    makepri new /pr . /cf ..\contoso_demo.xml /of ..\resources.pri /mf AppX /o
    
  3. 使用以下命令创建主包(其中包含可执行代码和默认语言资源)。 与往常一样,根据需要更改名称,尽管应将包放在单独的目录中,以方便稍后创建捆绑包(此示例使用 ..\bundle 目录):

    makeappx pack /m .\AppXManifest.xml /f ..\resources.map.txt /p ..\bundle\contoso_demo.main.appx /o
    
  4. 在创建主包后,对每个附加语言使用一次以下命令(即,对上一步中生成的每个语言映射文件重复此命令)。 同样,需要输出到单独的目录中(主包所在的目录)。 请注意,将同时/f 选项和 /p 选项中指定语言,并使用新的 /r 参数(指示需要资源包):

    makeappx pack /r /m .\AppXManifest.xml /f ..\resources.language-de.map.txt /p ..\bundle\contoso_demo.de.appx /o
    
  5. 将捆绑包目录中的所有包合并到单一 .appxbundle 文件中。 新 /d 选项指定要用于捆绑包中的所有文件的目录(这就是将 .appx 文件放入上一步中的单独目录的原因):

    makeappx bundle /d ..\bundle /p ..\contoso_demo.appxbundle /o
    

生成程序包的最后一步是签名。

步骤 3.2:签名捆绑包

在创建 .appxbundle 文件(通过捆绑包生成器工具或手动)后,主包和所有资源包将包含在单一文件中。 最后一步是对文件进行签名,以便 Windows 进行安装:

signtool sign /fd SHA256 /a /f ..\contoso_demo_key.pfx ..\contoso_demo.appxbundle

这将生成包含主包和所有语言特定资源包的签名 .appxbundle 文件。 可以像包文件一样双击该文件,以根据用户的 Windows 语言首选项安装应用以及所有适当语言。