LocBaml + MsBuild + ClickOnce 部署






4.64/5 (7投票s)
如何将 LOCBAML 与 MSBuild 集成以创建 WPF ClickOnce 部署包
引言
本文提供了一种将 LocBaml 和 MSBuild 与 ClickOnce 部署集成的可能解决方案。我的解决方案已在 VS2008 SP1 中进行了测试,但它也应该适用于早期版本的 Visual Studio。
Microsoft 慷慨地提供了 LocBaml.exe,但这只是一个示例应用程序。这意味着该应用程序有一些古怪的行为,但没有人会构建一个完整的商业应用程序来处理本地化,在此之前,我们被迫使用这个工具。如果您想在非玩具应用程序中使用它,我们需要能够为每个项目构建生成包含所有本地化卫星程序集的完整应用程序。
障碍
合并 (Merging)
这带来了第一个障碍,我们需要将上次翻译的结果合并回构建中。LocBaml 主要将二进制 XAML 数据转换为 CSV 文件。这意味着 XAML 文件中的每个更改都会产生不同的 CSV,而且在每次构建后翻译 CSV 有点麻烦。我通过编写一个应用程序 (MergeLocBamlCsv
) 来解决这个问题,该应用程序将新生成的 CSV 与上次构建的翻译过的 CSV 合并。应用程序 EXE 和源代码都包含在 zip 文件中。
合并过程很简单
- 解析原始翻译文件
- 将每一行按逗号分割成 7 部分
- 记住带有翻译文本部分的每行的第 7 部分。这意味着对于字符串资源或第 3 列不是“None”且文本不以“#”(这是链接)开头的字符串。我们使用前 2 部分作为键与新的 CSV 文件进行比较。
- 我们用新的 CSV 文件覆盖翻译文件。
- 我们分割新 CSV 文件的每一行并比较前 2 部分。如果它们与翻译过的 CSV 的一对匹配,我们就替换第 7 部分,并将部分用逗号连接成一个完整的新行。
剥离
第二个障碍是 CSV 文件中所有我们不需要翻译的东西。在一个大型程序中,CSV 文件可能会充斥着细节和“误报(链接)”。为了简化翻译过程,我编写了第二个程序 (StripLocBamlCsv
) 来去除所有不必要的翻译行。这样我们就能得到一个 CSV 文件,其中最后一列都可以翻译,而不是 5-10 行中只有 1 行可翻译。应用程序 EXE 和源代码都包含在 zip 文件中。
剥离过程更简单
- 解析原始翻译文件
- 将每一行按逗号分割成 7 部分
- 检查该行是否具有(翻译的)文本部分。这意味着对于字符串资源或第 3 列不是“None”且文本不以“#”(这是链接)开头的字符串。我们写回这些行,所有其他行都被跳过。
Visual Studio/MSBuild
Visual Studio 没有开箱即用的功能来为带 ClickOnce 部署的 WPF 应用程序构建卫星程序集。为了解决这个问题,我创建了一个专门用于 LocBaml 的新项目目标。我的解决方案期望只使用 XAML 文件,不使用自定义 resx 文件。这对应于 CodeProject 文章 使用 Locbaml 本地化 WPF 应用程序 中 brunzefb 的方法 2。我建议阅读这篇文章,因为我不想重复他对此方法的精彩解释。
解决方案
我的解决方案基于 3 个应用程序和一个单独的 msbuild 目标文件
- LocBaml.exe
- MergeLocBamlCsv.exe
- StripLocBamlCsv.exe
- LocBaml.Target.xml
它们包含在本文附带的示例应用程序中。只有在第二阶段,当我们有了原始语言的应用程序时,才需要它们。
额外的 msbuild 规则有两个部分
- 添加一个额外的构建操作‘
LocBamlCsv
’,这样我们就可以将所有代码放在 msbuild 文件列表中。 - 覆盖‘
CreateSatelliteAssemblies
’目标。调用此目标是在 EXE 构建完成后,应用程序清单创建之前。我们可以在此处创建我们的卫星程序集并将它们添加到‘IntermediateSatelliteAssembliesWithTargetPath
’列表中。完成后,所有 DLL 都将自动添加到部署文件中的应用程序清单中。因此,当我们发布应用程序时,它们会自动添加,无需我们进行任何额外工作。
构建规则如下所示
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Adds the build action 'LocBamlCsv' -->
<ItemGroup>
<AvailableItemName Include="LocBamlCsv" />
</ItemGroup>
<Target Name="CreateSatelliteAssemblies" DependsOnTargets=
"$(CreateSatelliteAssembliesDependsOn)">
<!-- Locbaml needs the runtime assemblies in the intermediate dir -->
<Copy SourceFiles="$(ProjectDir)Translation\locBaml.exe"
DestinationFolder="$(IntermediateOutputPath)" />
<Copy SourceFiles="@(ReferenceCopyLocalPaths)"
DestinationFiles="@(ReferenceCopyLocalPaths->'
$(IntermediateOutputPath)%(DestinationSubDirectory)%
(Filename)%(Extension)')" SkipUnchangedFiles="true" />
<!-- Locbaml, do it in 4 steps.
1) parse us culture and create csv.
2) merge new csv with last known translated csv
3) generate a new assembly from the merged csv
4) strip translation CSV -->
<MakeDir Directories="$(IntermediateOutputPath)%(LocBamlCsv.Culture)" />
<Exec Command="LocBaml /parse $(UICulture)\$(TargetName).resources.dll
/out:$(ProjectDir)Translation\translate.csv"
WorkingDirectory="$(IntermediateOutputPath)" />
<Exec Command="$(ProjectDir)\Translation\MergeLocBamlCsv
%(LocBamlCsv.FullPath) $(ProjectDir)Translation\translate.csv" />
<Exec Command="LocBaml /generate $(UICulture)\$(TargetName).resources.dll
/trans:%(LocBamlCsv.FullPath) /out:%(LocBamlCsv.Culture) /cul:%(LocBamlCsv.Culture)"
WorkingDirectory="$(IntermediateOutputPath)"
Outputs="$(IntermediateOutputPath)%(LocBamlCsv.Culture)\$(TargetName).resources.dll" />
<Exec Command="$(ProjectDir)\Translation\StripLocBamlCsv %(LocBamlCsv.FullPath)"/>
<Delete Files="$(IntermediateOutputPath)\locbaml.exe" />
<!-- Add the new satellite DLLs to the list, so they are added to the manifest.-->
<ItemGroup>
<IntermediateSatelliteAssembliesWithTargetPath Include=
"$(IntermediateOutputPath)%(LocBamlCsv.Culture)\$(TargetName).resources.dll">
<Culture>%(LocBamlCsv.Culture)</Culture>
<TargetPath>%(LocBamlCsv.Culture)\$(TargetName).resources.dll</TargetPath>
</IntermediateSatelliteAssembliesWithTargetPath>
</ItemGroup>
</Target>
</Project>
以下是使其正常工作的分步列表。
步骤 1 | 构建一个 WPF 应用程序 |
第二步 | 将标签<UICulture>en-US</UICulture> 添加到项目文件中。 |
步骤 3 | 在 AssemblyInfo.cs 中取消注释 NeutralResourcesLanguage 行 |
步骤 4 | 向每个 XAML 文件添加 UIDMsbuild /t:updateuid <project file> |
步骤 5 | 将应用程序复制到项目目录中的(新的)子文件夹 Translation。 |
步骤 6 | 将 XML 文件放在项目目录的根目录中,并在项目文件中导入。一个不错的位置是在文件末尾,在其他导入标签之后。<Import Project="$(ProjectDir)LocBamlCsv.Target.xml" /> |
步骤 7 | 将一个空的 CSV 文件放在目录中,并将构建操作设置为‘LocBamlCsv ’ |
步骤 8 | 出于某种原因,无法从 Visual Studio 更改区域设置。请在项目文件中手动将正确的区域设置添加到项中,这样我们就会得到一个条目,例如:<LocBamlCsv Include="Translation\Translate.nl-NL.csv" > |
步骤 9 | 构建应用程序 |
第 10 步 | 翻译步骤 7 中的 CSV 文件,它们现在已填充。 |
第 11 步 | 再次构建,您将拥有一个本地化的应用程序。 |
第 12 步 | 在开始新的翻译之前,请不要忘记重复步骤 4。否则,构建和发布可以毫无顾虑地重复。 |
示例应用
我包含了一个示例应用程序,以便有一个可用的起点。它由一个带有关闭按钮的主应用程序窗口和一个登录窗口组成。登录窗口底部有一些标志,用于在运行时切换区域设置。该应用程序目前有 3 种语言:英语、德语和荷兰语。每个窗口都有一些静态文本部分和一些动态文本部分。
关注点
切换语言的代码需要重新加载 XAML 和资源文件,否则即使在 UI 区域设置更改后,您仍然会保留相同的语言。因此,窗口被关闭并且必须重新显示。其次,合并的字典被清除并重新加载,这会重新加载我们的字符串表。
执行此操作的代码如下所示
private void btGb_MouseDown( object sender, MouseButtonEventArgs e )
{
Image img = sender as Image;
if (( img != null ) && ( img.Tag != null))
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo( (string)img.Tag );
Thread.CurrentThread.CurrentCulture = new CultureInfo( (string)img.Tag );
// Reload all the merged dictionaries to reset the resources.
List<Uri> dictionaryList = new List<Uri>();
foreach (ResourceDictionary dictionary in
Application.Current.Resources.MergedDictionaries)
{
dictionaryList.Add(dictionary.Source);
}
Application.Current.Resources.MergedDictionaries.Clear();
foreach (Uri uri in dictionaryList)
{
ResourceDictionary resourceDictionary1 = new ResourceDictionary();
resourceDictionary1.Source = uri;
Application.Current.Resources.MergedDictionaries.Add( resourceDictionary1 );
}
IsLanguageChange = true;
DialogResult = false;
Close();
}
}
历史
- 1.00
- 原始
- 1.01
- 已修改示例以包含图像名称
- 更新了
MergeLocbamlCsv
和StripLocbamlCsv
增强方法,该方法确定一行是否可翻译 - 图像源现在是包含翻译行的一个原因(由 laduran 提供)
- 1.02
- 从步骤 4 中删除了一个拼写错误