基于 WPF 模块化架构下的本地化设计实践

背景描述
最近接到一个需求,就是要求我们的 WPF 客户端具备本地化功能,实现中英文多语言界面。刚开始接到这个需求,其实我内心是拒绝的的,但是没办法,需求是永无止境的。所以只能想办法解决这个问题。
首先有必要说一下我们的系统架构。我们的系统是基于 Prism 来进行设计的,所以每个业务模块之间都是相互独立,互不影响的 DLL,然后通过主 Shell 来进行目录的动态扫描来实现动态加载。
为了保证在不影响系统现有功能稳定性的前提下,如何让所有模块支持多语言成为了一个亟待解决的问题。
刚开始,我 Google 了一下,查阅了一些资料,很多都是介绍如何在单体程序中实现多语言,但是在模块化架构中,我个人觉得这样做并不合适。做过本地化的朋友应该都知道,在进行本地化翻译的时候,都需要创建对应语言的资源文件,无论是使用 .xaml .resx 或 .xml,这里面会存放我们的本地化资源。对于单体系统而言,这些资源直接放到主程序下即可,方便快捷。但是对于模块化架构的程序,这样做就不太好,而是应该将这些资源都分别放到自己模块内部由自己来维护,主程序只需规定整个系统的区域语言即可。
设计思路
面对上面的背景描述,我们可以大致描述一下我们期望的解决方式,主程序只负责对整个系统进行区域语言设置,每个模块的本地化由本模块内部完成,所有模块的本地化切换方式保持一致,依赖于共有的一种实现。如下图所示:

实现方案
由于如何使用 Prism 不是本文的重点,所以这里就略过主程序和模块程序中相关的模板代码,感兴趣的小伙伴可以自行在园子里搜索相关技术文章。
参照上述的思路,我们可以做一个小示例来展示一下如何进行多模块多语言的本地化实践。
在这个示例中,我以 DotNetCore 3.0 版本的 WPF 和 Prism 进行示例说明。在我们的示例工程中创建三个项目
BlackApp
- 引用 Prism.Unity 包
- WPF App(.NET Core 版本),作为启动程序
BlackApp.ModuleA
- 引用 Prism.Wpf 包
- WPF UseControl(.NET Core 版本),作为示例模块
BlackApp.Common
- ClassLibrary(.NET Core 版本),作为基础的公共服务层
BlackApp.ModuleA 添加对 BlackApp.Common 的引用,并将 BlackApp 和 BlackApp.ModuleA 的项目输出修改为相同的输出目录。然后修改对应的基础代码,以确保主程序能正常加载并显示 ModuleA 模块及其内容。
上述操作完成后,我们就可以编写我们的测试代码了。按照我们的设计思路,我需要先在 BlackApp.ModuleA 定义我们的本地化资源文件,对于这个资源文件的类型选择,理论上我们是可以选择任何一种基于 XML 的文件,但是不同类型的文件对于后面是否是埋坑行为这个需要认真考虑一下。这里我建议使用 XAML 格式的文件。我们在 BlackApp.ModuleA 项目的根目录下创建一个 Strings 的文件夹,然后里面分别创建 en-US.xaml 和 zh-CN.xaml 文件。这里建议最好以语言名称作为文件名称,这样方便到时候查找。文件内容如下所示:
- en-US.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<system:String x:Key="string1">Hello world</system:String>
</ResourceDictionary>
- zh-CN.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<system:String x:Key="string1">世界你好</system:String>
</ResourceDictionary>
资源文件定义好了,接下来就是如何使用了。
对于我们需要进行本地化的 XAML 页面,首先我们需要指当前使用到的资源文件,这个时候就需要在我们的 BlackApp.Common 项目中定义一个依赖属性了,然后通过依赖属性的方式来进行设置。由于语言种类有很多,所以我们定义一个文件夹目录的依赖属性,来指定当前页面需要用到的资源的文件夹路径,然后由辅助类到时候依据具体的语言类型来到指定目录查找指当的资源文件。
示例代码如下所示:
[RuntimeNameProperty(nameof(ExTranslationManager))]
public class ExTranslationManager : DependencyObject
{
public static string GetResourceDictionary(DependencyObject obj)
{
return (string)obj.GetValue(ResourceDictionaryProperty);
}
public static void SetResourceDictionary(DependencyObject obj, string value)
{
obj.SetValue(ResourceDictionaryProperty, value);
}
// Using a DependencyProperty as the backing store for ResourceDictionary. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ResourceDictionaryProperty =
DependencyProperty.RegisterAttached("ResourceDictionary", typeof(string), typeof(ExTranslationManager), new PropertyMetadata(null));
}
本地化资源指定完毕后,我们就可以使用里面资源文件进行本地化操作。如果想在 XAML 对相应属性进行 标签式 访问,需要定义一个继承自 MarkupExtension 类的自定义类,并在该类中实现 ProvideValue 方法。接下来在我们的 BlackApp.Common 项目中定义该类,示例代码如下所示:
[RuntimeNameProperty(nameof(ExTranslation))]
public class ExTranslation : MarkupExtension
{
public string StringName { get; private set; }
public ExTranslation(string stringName)
{
this.StringName = stringName;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
object targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject;
ResourceDictionary dictionary = GetResourceDictionary(targetObject);
if (dictionary == null)
{
object rootObject = (serviceProvider as IRootObjectProvider)?.RootObject;
dictionary = GetResourceDictionary(rootObject);
}
if (dictionary == null)
{
if (targetObject is FrameworkElement frameworkElement)
{
dictionary = GetResourceDictionary(frameworkElement.TemplatedParent);
}
}
return dictionary != null && StringName != null && dictionary.Contains(StringName) ?
dictionary[StringName] : StringName;
}
private ResourceDictionary GetResourceDictionary(object target)
{
if (target is DependencyObject dependencyObject)
{
object localValue = dependencyObject.ReadLocalValue(ExTranslationManager.ResourceDictionaryProperty);
if (localValue != DependencyProperty.UnsetValue)
{
var local = localValue.ToString();
var (baseName,stringName) = SplitName(local);
var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";
var dict = new ResourceDictionary { Source = new Uri(str) };
return dict;
}
}
return null;
}
public static (string baseName, string stringName) SplitName(string name)
{
int idx = name.LastIndexOf('.');
return (name.Substring(0, idx), name.Substring(idx + 1));
}
}
此外,如果我们的 ViewModel 中也有数据需要进行本地化操作的化,我们可以定义一个扩展方法,示例代码如下所示:
public static class ExTranslationString
{
public static string GetTranslationString(this string key, string resourceDictionary)
{
var (baseName, stringName) = ExTranslation.SplitName(resourceDictionary);
var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";
var dictionary = new ResourceDictionary { Source = new Uri(str) };
return dictionary != null && !string.IsNullOrWhiteSpace(key) && dictionary.Contains(key) ? (string)dictionary[key] : key;
}
}
通过在 BlackApp.Common 中定义上述 3 个辅助类,基本可以满足我们的需求,我们可以却换到 BlackApp.ModuleA 项目中,并进行如下示例修改
- View 层使用示例
<UserControl
x:Class="BlackApp.ModuleA.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ex="clr-namespace:BlackApp.Common;assembly=BlackApp.Common"
xmlns:local="clr-namespace:BlackApp.ModuleA.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
d:DesignHeight="300"
d:DesignWidth="300"
ex:ExTranslationManager.ResourceDictionary="BlackApp.ModuleA.Strings"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding Message}" />
<TextBlock Text="{ex:ExTranslation string1}" />
</StackPanel>
</Grid>
</UserControl>
- ViewModel 层使用示例
"message".GetTranslationString("BlackApp.ModuleA.Strings")
最后,我们就可以在我们的 BlackApp 项目中的 App.cs 构造函数中来设置我们程序的语言类型,示例代码如下所示:
public partial class App
{
public App()
{
//CultureInfo ci = new CultureInfo("zh-cn");
CultureInfo ci = new CultureInfo("en-US");
Thread.CurrentThread.CurrentCulture = ci;
}
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = AppDomain.CurrentDomain.BaseDirectory };
}
}
写到这里,我们应该就可以进行本地化的测试工作了,尝试编译运行我们的示例程序,如果不出意外的话,应该是可以通过在 主程序中设置区域类型来更改模块程序中的对应本地化资源内容。
最后,整个示例项目的组织结构如下图所示:

总结
对于模块化架构的本地化实现,有很多的实现方式,我这里介绍的只是一种符合我们的业务场景的一种实现,期待大佬们在评论区留言提供更好的解决方案。
补充
经同事验证,使用 .resx 格式的资源文件会更简单一下,可以直接通过
BlackApp.ModuleA.Strings.zh_cn.ResourceManager("string1")
BlackApp.ModuleA.Strings.en_us.ResourceManager("string1")
的方式来访问。但前提是需要将对应资源文件的访问修饰符设置为 public。
参考
- Localization of a WPF app - the simple approach
- wpf-localization-multiple-resource-resx-one-language
- LocalizeMarkupExtension
- Markup Extensions and WPF XAML
基于 WPF 模块化架构下的本地化设计实践的更多相关文章
- 分布式架构下的会话追踪实践【基于Cookie和Redis实现】
分布式架构下的会话追踪实践[基于Cookie和Redis实现] 博客分类: NoSQL/Redis/MongoDB session共享rediscookie分布式架构session 在单台Tomcat ...
- 《MVC架构下网站的设计与实现》论文笔记(十八)
标题:MVC架构下网站的设计与实现 一.基本信息 时间:2017 来源:广东海洋大学数学与计算机学院 关键词:网站设计:MVC 框架:数据库:网络安全 二.研究内容 1.系统的整体架构设计(以广东海洋 ...
- China .NET Conf 2019-.NET技术架构下的混沌工程实践
这个月的8号.9号,个人很荣幸参加了China.NET Conf 2019 , 中国.NET开发者峰会,同时分享了技术专题<.NET技术架构下的混沌工程实践>,给广大的.NET开发小伙伴介 ...
- App后台开发运维和架构实践学习总结(3)——RestFul架构下API接口设计注意点
1. 争取相容性和统一性 这里就要求让API设计得是可预测的.按照这种方式写出所有接口和接口所需要的参数.现在就要确保命名是一致的,接口所需的参数顺序也是一致的.你现在应该有products,orde ...
- 基于B/S架构的在线考试系统的设计与实现
前言 这个是我的Web课程设计,用到的主要是JSP技术并使用了大量JSTL标签,所有代码已经上传到了我的Github仓库里,地址:https://github.com/quanbisen/online ...
- CI Weekly #5 | 微服务架构下的持续部署与交付
CI Weekly 围绕『 软件工程效率提升』 进行一系列技术内容分享,包括国内外持续集成.持续交付,持续部署.自动化测试. DevOps 等实践教程.工具与资源,以及一些工程师文化相关的程序员 Ti ...
- 大数据分析的下一代架构--IOTA架构设计实践[下]
大数据分析的下一代架构--IOTA架构设计实践[下] 原创置顶 代立冬 发布于2018-12-31 20:59:53 阅读数 2151 收藏 展开 IOTA架构提出背景 大数据3.0时代以前,Lam ...
- 基于WPF系统框架设计(5)-Ribbon整合Avalondock 2.0实现多文档界面设计(二)
AvalonDock 是一个.NET库,用于在停靠模式布局(docking)中排列一系列WPF/WinForm控件.最新发布的版本原生支持MVVM框架.Aero Snap特效并具有更好的性能. Ava ...
- PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品,产品设计严格遵循国际数据挖掘标准CRISP-DM(跨行业数据挖掘过程标准),具备完备的数据准备、模型构建、模型评估、模型管理、海量数据处理和高纬数据可视化分析能力。
http://www.meritdata.com.cn/article/90 PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品, ...
随机推荐
- python爬虫笔记之re.IGNORECASE
re.IGNORECASE有什么用?re.IGNORECASE是什么意思?(原谅我抓下seo..) 这里自己总结下: re.IGNORECASE是compile函数中的一个匹配模式. re.IGNOR ...
- 前端三剑客之HTML
目录 what is html html基本格式 html常用标签及其属性 @() what is html (hypertext marked language)超文本标记语言,负责页面文本.图片内 ...
- 个人永久性免费-Excel催化剂功能第61波-快速锁定解锁单元格及显示隐藏公式
Excel的所有功能都是需求导向的,正因为有客户在企业管理的过程中,有这样的需求出现了,然后相应的Excel就出现了相应的功能来辅助管理,学习Excel的功能,其实真的可以学习到先进企业的许多的管理思 ...
- linux下的FTP安装及调优
前言: 在之前交换平台的开发中,FTP的各种操作算是核心功能点. 在FTP的开发中,遇到了不少坑. 如FTP需要设置被动模式,否则10M以上的包可能会上传失败. 如FTP需要设置囚牢模式,否则访问的文 ...
- CentOS 下编译安装PHP
1. 去php官网下载源码 http://www.php.net/downloads.php ,我下载使用的版本是(php-5.4.8.tar.gz) 2. 安装环境 yum install ...
- .net持续集成sonarqube篇之 sonarqube触发webhook
系列目录 WebHook近些年来变得越来越流行,github,gitlab等代码托管平台都提供webhook功能.关于webhook这里不做详细介绍,大家可以参阅读相关互联网书籍或者材料来更深了解.可 ...
- web设计_6_图片/标题/说明文字布局
这个web中常见的单元布局,最好的布局方式就是利用float布局. 其中有个很关键的问题是需要清浮动.子集浮动是无法撑开父级的高度. 目前较完善的清浮动解决方案:在浮动的父级上添加.clear,达到清 ...
- context创建过程解析(三)之deployDirectories
HostConfig.deployApps() //在监听到start事件类型,也就是StandardHost调用startInternal protected void deployApps() { ...
- 10w数组去重,排序,找最多出现次数
配置在博客底部 主函数 package ooDay11.zy13; import ooDay11.zy13.hanshu.GetKeyList;import ooDay11.zy13.hanshu.G ...
- Shell基本语法---shell脚本的输入以及脚本拥有特效地输出
shell脚本的输入 语法:read -参数 -p:给出提示符.默认不支持"\n"换行 -s:隐藏输入的内容 -t:给出等待的时间,超时会退出read,单位是秒 -n:限制读取字符 ...