动态

详情 返回 返回

dotnet 讀 WPF 源代碼 學習使用 Microsoft.DotNet.Arcade.Sdk 處理代碼裏的多語言 - 动态 详情

在 WPF 開源代碼裏面,可以看到是從各個項目的 Strings.resx 和對應的 xlf 文件,生成對應項目的多語言程序集。這裏的多語言程序集可用於拋出異常時,給出本地化的消息提示

在 dotnet 龐大的生態集裏,打包工具鏈是開源中很重要的部分工作。通過 https://github.com/dotnet/arcade 將打包中重複的工作放在一個倉庫中,減少基礎設施能力在多個項目中重複進行。就像我所在的團隊開源的 DotNETBuildSDK 項目一樣,提供各種構建工具用在各個項目裏面

翻遍整個 WPF 倉庫,都無法直接找到任何的從 Strings.resx 和對應的 xlf 文件生成多語言衞星程序集的邏輯。這是因為多語言的核心轉換是放在 Microsoft.DotNet.Arcade.Sdk 裏面,在 WPF 倉庫裏面只有一些配置項

整個 WPF 開源倉庫的組織是相對清晰的,所有和構建相關的配置都放在 eng 文件夾裏面。其中對 Microsoft.DotNet.Arcade.Sdk 的引用分別放在 eng\WpfArcadeSdk\Sdk\Sdk.propseng\WpfArcadeSdk\Sdk\Sdk.targets 文件裏。核心代碼只有以下這兩句

  <!-- Importing Arcade's Sdk.props should always be the first thing we do. However this is not a hard rule, 
       it's just a convention for ensuring correctness and consistency in our build environment. If anything 
       does need to be imported before, it should be documented why it is needed. -->
  <Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />

  <Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />

多語言配置部分的邏輯放在 eng\WpfArcadeSdk\tools\SystemResources.props 文件裏,其代碼較多,咱就先不展開細看

從 WPF 代碼倉庫裏面是沒有看到詳盡的多語言轉換過程邏輯的,但看了這幾個文件也夠咱自己學習模仿 WPF 用 Microsoft.DotNet.Arcade.Sdk 處理代碼裏的多語言的方式。接下來我將新建一個 WPF 空項目,在此和大家演示使用 Microsoft.DotNet.Arcade.Sdk 處理多語言,相信大家能夠學會用此構建工具生成多語言程序集

新建一個空白的 WPF 項目

雖然按照 .NET 的慣例,使用一個庫的第一件事就是用 NuGet 進行庫的安裝。但 Microsoft.DotNet.Arcade.Sdk 比較特殊,這是一個 SDK 而不是一個 Library 庫。直接使用 NuGet 安裝會報告以下錯誤

包“Microsoft.DotNet.Arcade.Sdk 11.0.0-beta.25556.1”具有一個包類型“MSBuildSdk”,項目“Xxxxx”不支持該類型。

正確的使用方法如下

第一步是添加 NuGet.config 文件,設置使用 dotnet-eng 源。因為 Microsoft.DotNet.Arcade.Sdk 庫是沒有放在公網 NuGet 源裏面的。修改之後的 NuGet.config 文件內容如下

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
    <add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
   
  </packageSources>
</configuration>

第二步是添加 global.json 文件,設置 Microsoft.DotNet.Arcade.Sdk 的版本。這一步就類似於使用 NuGet 進行安裝的過程,只不過用的是 SDK 的方式

{
  "msbuild-sdks": 
  {
    "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.25411.109"
  }
}

第三步就是在 csproj 項目文件裏面添加引用,代碼如下

  <Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />
  <Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />

如此三步就可以完成 Microsoft.DotNet.Arcade.Sdk 庫安裝

完成安裝之後,就可以嘗試多語言的加入了。只需放入 resx 文件,無論命名和放在哪個文件夾內。為了簡單起見,我隨便從 WPF 倉庫拷貝了一個 Strings.resx 文件,編輯之後的內容如下

此時直接構建肯定是沒有效果的,因為還沒有設置 GenerateResxSource 屬性為 true 值,用於配置讓 Arcade 進行多語言生成

  <PropertyGroup>
    <GenerateResxSource>true</GenerateResxSource>
  </PropertyGroup>

再設置 EmbeddedResource 屬性,配置好生成的類型的命名空間和類名,配置的代碼如下

  <ItemDefinitionGroup>
    <EmbeddedResource>
      <GenerateSource>true</GenerateSource>
      <ManifestResourceName>FxResources.$(AssemblyName).SR</ManifestResourceName>
      <ClassName>MS.Utility.SR</ClassName>
    </EmbeddedResource>
  </ItemDefinitionGroup>

以上代碼裏面的 GenerateSource 設置為 true 表示當前項用來配置多語言的生成。以上代碼的 ManifestResourceName 只是一個用來標識資源存在的程序集,用來執行 typeof 獲取 ResourceManager 的資源,命名上比較隨意。以上的 ClassName 為重點部分,用來表示從 resx 文件應該生成的類型全名,採用命名空間加類型名的表示法。如 MS.Utility.SR 將生成命名空間為 MS.Utility 且類型名為 SR 的類型

通過 ClassName 的配置,即可讓各個程序集採用不同的命名空間配置。如在 WPF 倉庫的 eng\WpfArcadeSdk\tools\SystemResources.props 文件裏,就使用了以下類似的代碼為各個程序集配置不同的命名空間

<ItemDefinitionGroup>
    <EmbeddedResource>
      <GenerateSource>true</GenerateSource>
      <ManifestResourceName>FxResources.$(AssemblyName).SR</ManifestResourceName>

      <ClassName Condition="'$(AssemblyName)'=='PresentationBuildTasks'">MS.Utility.SR</ClassName>
      <ClassName Condition="'$(AssemblyName)'=='UIAutomationTypes'">System.SR</ClassName>
      <ClassName Condition="'$(AssemblyName)'=='WindowsBase'">MS.Internal.WindowsBase.SR</ClassName>
      ...
      <ClassName Condition="'$(AssemblyName)'=='PresentationCore'">MS.Internal.PresentationCore.SR</ClassName>
      <ClassName Condition="'$(AssemblyName)'=='System.Xaml'">System.SR</ClassName>
      <Classname Condition="'%(ClassName)'==''">System.SR</Classname>
    </EmbeddedResource>
  </ItemDefinitionGroup>

以上邏輯就能夠完成多語言生成的配置

然而現在還不能通過構建,一構建將提示類似如下的錯誤

C:\Users\lindexi\.nuget\packages\microsoft.dotnet.arcade.sdk\10.0.0-beta.25411.109\tools\Version.BeforeCommonTargets.targets(88,5): error MSB4184: 無法計算表達式“"".GetValue(1)”。Index was outside the bounds of the array.

這是因為在 Version.BeforeCommonTargets.targets 文件裏面存在如下代碼

  <PropertyGroup>
    <VersionPrefix Condition="'$(MajorVersion)' != '' and '$(MinorVersion)' != ''">$(MajorVersion).$(MinorVersion).$([MSBuild]::ValueOrDefault('$(PatchVersion)', '0'))</VersionPrefix>
  </PropertyGroup>

  <PropertyGroup Condition="'$(PreReleaseVersionLabel)' == ''">
    <_VersionPrefixMajor>$(VersionPrefix.Split('.')[0])</_VersionPrefixMajor>
    <_VersionPrefixMinor>$(VersionPrefix.Split('.')[1])</_VersionPrefixMinor>
    <VersionPrefix>$(_VersionPrefixMajor).$(_VersionPrefixMinor).$([MSBuild]::ValueOrDefault($(_PatchNumber), '0'))</VersionPrefix>
    <VersionSuffix/>
  </PropertyGroup>

儘管我認為這是 Microsoft.DotNet.Arcade.Sdk 庫的設計不夠開箱即用,但考慮到這是一個專用的庫,這一點也能接受。繼續編輯 csproj 項目文件,添加如下代碼,添加版本號信息

  <PropertyGroup>
    <MajorVersion>1</MajorVersion>
    <MinorVersion>2</MinorVersion>
  </PropertyGroup>

如此即可完成構建準備,嘗試構建一下。此時細心的夥伴也許就發現了,在 obj 文件夾下,生成了 obj\Debug\net9.0-windows\MS.Utility.SR.cs 文件,且在此文件裏面填滿了在 Strings.resx 資源字典定義的多語言項。其生成代碼大概如下

using System.Reflection;

namespace FxResources.QewheefanallJabayhejage
{
    internal static class SR { }
}
namespace MS.Utility
{
    internal static partial class SR
    {
        private static global::System.Resources.ResourceManager s_resourceManager;
        internal static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof(FxResources.QewheefanallJabayhejage.SR)));
        internal static global::System.Globalization.CultureInfo Culture { get; set; }
#if !NET20
        [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
#endif
        internal static string GetResourceString(string resourceKey, string defaultValue = null) =>  ResourceManager.GetString(resourceKey, Culture);
        /// <summary>Enumerating attached properties on object '{0}' threw an exception.</summary>
        internal static string @APSException => GetResourceString("APSException");
        /// <summary>Add value to collection of type '{0}' threw an exception.</summary>
        internal static string @AddCollection => GetResourceString("AddCollection");
        /// <summary>Add value to dictionary of type '{0}' threw an exception.</summary>
        internal static string @AddDictionary => GetResourceString("AddDictionary");
    }
}

細心的夥伴還能看到,此時在項目裏面被新建了 xlf 文件夾,在此文件夾內充滿了各個語言文化對應的 xlf 文件。這些 xlf 文件是為翻譯人員準備的,方便對接翻譯平台進行翻譯。每個 xlf 文件都會在 obj 文件夾生成對應的 resx 文件,再由 resx 文件生成對應的程序集

這裏的 xlf 文件是採用 https://en.wikipedia.org/wiki/XLIFF 多語言翻譯規範的文件,這是一個現有的規範的格式。其內容大概如下

<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
  <file datatype="xml" source-language="en" target-language="zh-Hans" original="../Strings.resx">
    <body>
      <trans-unit id="APSException">
        <source>Enumerating attached properties on object '{0}' threw an exception.</source>
        <target state="translated">枚舉對象“{0}”的附加屬性時引發了異常。</target>
        <note />
      </trans-unit>
      <trans-unit id="AddCollection">
        <source>Add value to collection of type '{0}' threw an exception.</source>
        <target state="translated">向類型為“{0}”的集合中添加值引發了異常。</target>
        <note />
      </trans-unit>
      <trans-unit id="AddDictionary">
        <source>Add value to dictionary of type '{0}' threw an exception.</source>
        <target state="new">Add value to dictionary of type '{0}' threw an exception.</target>
        <note />
      </trans-unit>
    </body>
  </file>
</xliff>

可以看到 XLIFF 格式裏面可以為翻譯人員提供雙語對照,也能通過 state="translated" 還是 state="new" 標記出已經翻譯的還是新添加的多語言項

從這裏也能看到 Microsoft.DotNet.Arcade.Sdk 的好用之處,只需添加 resx 文件,就會自動生成各個語言文化對應的 xlf 文件,方便翻譯人員對接

以下是我的最簡使用 Microsoft.DotNet.Arcade.Sdk 對接多語言的 csproj 項目的代碼

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <PropertyGroup>
    <MajorVersion>1</MajorVersion>
    <MinorVersion>2</MinorVersion>
  </PropertyGroup>

  <PropertyGroup>
    <GenerateResxSource>true</GenerateResxSource>

    <!-- <GenerateResxSourceOmitGetResourceString>true</GenerateResxSourceOmitGetResourceString> -->
  </PropertyGroup>

  <Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />
  <Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />

  <ItemDefinitionGroup>
    <EmbeddedResource>
      <GenerateSource>true</GenerateSource>
      <ManifestResourceName>FxResources.$(AssemblyName).SR</ManifestResourceName>
      <ClassName>MS.Utility.SR</ClassName>
      </EmbeddedResource>
  </ItemDefinitionGroup>
</Project>

以上被註釋掉的 GenerateResxSourceOmitGetResourceString 屬性用來配置 Microsoft.DotNet.Arcade.Sdk 生成的類型裏面,不要生成 GetResourceString 等代碼。如此即可在自己程序集裏面自己定義多語言獲取的類型,提供更高的自由。在 WPF 倉庫裏面,就是自己定義的 GetResourceString 方法,用來處理多語言找不到的情況

也許有夥伴好奇在 Microsoft.DotNet.Arcade.Sdk 底層是如何對接多語言代碼的生成的。事實上這部分邏輯也十分簡單,從 https://github.com/dotnet/arcade 倉庫可以找到明確的代碼

先是在 GenerateResxSource.targets 文件裏面執行對接邏輯,核心代碼如下

  <Target Name="_GenerateResxSource"
          BeforeTargets="BeforeCompile;CoreCompile"
          DependsOnTargets="PrepareResourceNames;
                            _GetEmbeddedResourcesWithSourceGeneration;
                            _BatchGenerateResxSource">
    <ItemGroup>
      <GeneratedResxSource Include="@(EmbeddedResourceSGResx->'%(SourceOutputPath)')" />
      <FileWrites Include="@(GeneratedResxSource)" />
      <Compile Include="@(GeneratedResxSource)" />
    </ItemGroup>
  </Target>

  <Target Name="_BatchGenerateResxSource"
          Inputs="@(EmbeddedResourceSGResx)"
          Outputs="%(EmbeddedResourceSGResx.SourceOutputPath)">

    <Microsoft.DotNet.Arcade.Sdk.GenerateResxSource
      Language="$(Language)"
      ResourceFile="%(EmbeddedResourceSGResx.FullPath)"
      ResourceName="%(EmbeddedResourceSGResx.ManifestResourceName)"
      ResourceClassName="%(EmbeddedResourceSGResx.ClassName)"
      AsConstants="%(EmbeddedResourceSGResx.GenerateResourcesCodeAsConstants)"
      OmitGetResourceString="$(GenerateResxSourceOmitGetResourceString)"
      IncludeDefaultValues="$(GenerateResxSourceIncludeDefaultValues)"
      EmitFormatMethods="$(GenerateResxSourceEmitFormatMethods)"
      OutputPath="%(EmbeddedResourceSGResx.SourceOutputPath)" />
  </Target>

可見就是從 _BatchGenerateResxSource 調用 Microsoft.DotNet.Arcade.Sdk.GenerateResxSource 執行生成邏輯。在 _GenerateResxSource 裏面將生成的文件加入構建

上面代碼的 EmbeddedResourceSGResx 內容僅是取出本文在 csproj 的 ItemDefinitionGroup 裏面定義的屬性內容,再配合添加一些過濾條件而已

核心的 GenerateResxSource 生成類的定義代碼如下

    public sealed class GenerateResxSource : Microsoft.Build.Utilities.Task
    {
        private const int maxDocCommentLength = 256;

        /// <summary>
        /// Language of source file to generate.  Supported languages: CSharp, VisualBasic
        /// </summary>
        [Required]
        public string Language { get; set; }

        /// <summary>
        /// Resources (resx) file.
        /// </summary>
        [Required]
        public string ResourceFile { get; set; }

        /// <summary>
        /// Name of the embedded resources to generate accessor class for.
        /// </summary>
        [Required]
        public string ResourceName { get; set; }

        /// <summary>
        /// Optionally, a namespace.type name for the generated Resources accessor class.  Defaults to ResourceName if unspecified.
        /// </summary>
        public string ResourceClassName { get; set; }

        /// <summary>
        /// If set to true the GetResourceString method is not included in the generated class and must be specified in a separate source file.
        /// </summary>
        public bool OmitGetResourceString { get; set; }

        /// <summary>
        /// If set to true, emits constant key strings instead of properties that retrieve values.
        /// </summary>
        public bool AsConstants { get; set; }

        /// <summary>
        /// If set to true calls to GetResourceString receive a default resource string value.
        /// </summary>
        public bool IncludeDefaultValues { get; set; }

        /// <summary>
        /// If set to true, the generated code will include .FormatXYZ(...) methods.
        /// </summary>
        public bool EmitFormatMethods { get; set; }

        [Required]
        public string OutputPath { get; set; }

        private enum Lang
        {
            CSharp,
            VisualBasic,
        }

        ...
    }

其生成邏輯是根據 C# 或 VB 進行拼接字符串方式生成的多語言代碼的

讀取 resw 字典也是直接使用 XDocument 的方式讀取,核心代碼如下

            string classIndent = (namespaceName == null ? "" : "    ");
            string memberIndent = classIndent + "    ";

            var strings = new StringBuilder();

            foreach (var node in XDocument.Load(ResourceFile).Descendants("data"))
            {
                string name = node.Attribute("name")?.Value;
                string value = node.Elements("value").FirstOrDefault()?.Value.Trim();

                strings.AppendLine($"{memberIndent}internal static string @{identifier} => GetResourceString(\"{name}\"{defaultValue});");
            }

實際的代碼比我以上有刪減部分略微複雜,如果大家感興趣,還請自行去查看源代碼

本文代碼放在 github 和 gitee 上,可以使用如下命令行拉取代碼。我整個代碼倉庫比較龐大,使用以下命令行可以進行部分拉取,拉取速度比較快

先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 69bd783e97b03e767017ebbbe61aad89b9a8104d

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令行繼續輸入以下代碼,將 gitee 源換成 github 源進行拉取代碼。如果依然拉取不到代碼,可以發郵件向我要代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 69bd783e97b03e767017ebbbe61aad89b9a8104d

獲取代碼之後,進入 WPFDemo/QewheefanallJabayhejage 文件夾,即可獲取到源代碼

更多技術博客,請參閲 博客導航

user avatar vanve 头像 u_9849794 头像 leoyi 头像 shumile_5f6954c414184 头像
点赞 4 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.