背景描述

最近接到一个需求,就是要求我们的 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.xamlzh-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

参考

基于 WPF 模块化架构下的本地化设计实践的更多相关文章

  1. 分布式架构下的会话追踪实践【基于Cookie和Redis实现】

    分布式架构下的会话追踪实践[基于Cookie和Redis实现] 博客分类: NoSQL/Redis/MongoDB session共享rediscookie分布式架构session 在单台Tomcat ...

  2. 《MVC架构下网站的设计与实现》论文笔记(十八)

    标题:MVC架构下网站的设计与实现 一.基本信息 时间:2017 来源:广东海洋大学数学与计算机学院 关键词:网站设计:MVC 框架:数据库:网络安全 二.研究内容 1.系统的整体架构设计(以广东海洋 ...

  3. China .NET Conf 2019-.NET技术架构下的混沌工程实践

    这个月的8号.9号,个人很荣幸参加了China.NET Conf 2019 , 中国.NET开发者峰会,同时分享了技术专题<.NET技术架构下的混沌工程实践>,给广大的.NET开发小伙伴介 ...

  4. App后台开发运维和架构实践学习总结(3)——RestFul架构下API接口设计注意点

    1. 争取相容性和统一性 这里就要求让API设计得是可预测的.按照这种方式写出所有接口和接口所需要的参数.现在就要确保命名是一致的,接口所需的参数顺序也是一致的.你现在应该有products,orde ...

  5. 基于B/S架构的在线考试系统的设计与实现

    前言 这个是我的Web课程设计,用到的主要是JSP技术并使用了大量JSTL标签,所有代码已经上传到了我的Github仓库里,地址:https://github.com/quanbisen/online ...

  6. CI Weekly #5 | 微服务架构下的持续部署与交付

    CI Weekly 围绕『 软件工程效率提升』 进行一系列技术内容分享,包括国内外持续集成.持续交付,持续部署.自动化测试. DevOps 等实践教程.工具与资源,以及一些工程师文化相关的程序员 Ti ...

  7. 大数据分析的下一代架构--IOTA架构设计实践[下]

    大数据分析的下一代架构--IOTA架构设计实践[下] 原创置顶 代立冬 发布于2018-12-31 20:59:53 阅读数 2151  收藏 展开 IOTA架构提出背景 大数据3.0时代以前,Lam ...

  8. 基于WPF系统框架设计(5)-Ribbon整合Avalondock 2.0实现多文档界面设计(二)

    AvalonDock 是一个.NET库,用于在停靠模式布局(docking)中排列一系列WPF/WinForm控件.最新发布的版本原生支持MVVM框架.Aero Snap特效并具有更好的性能. Ava ...

  9. PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品,产品设计严格遵循国际数据挖掘标准CRISP-DM(跨行业数据挖掘过程标准),具备完备的数据准备、模型构建、模型评估、模型管理、海量数据处理和高纬数据可视化分析能力。

    http://www.meritdata.com.cn/article/90 PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品, ...

随机推荐

  1. Python的函数, 返回值, 参数

    1. 函数 函数是对功能的封装 语法: def 函数名(形参): 函数体(代码块,return) 调用: 函数名(实参) 2. 返回值 return:在函数执行的时候, 遇到return 就直接返回, ...

  2. mysql查询语句出现sending data耗时解决

    在执行一个简单的sql查询,表中数据量为14万 sql语句为:SELECT id,titile,published_at from spider_36kr_record where is_analyz ...

  3. HTML认识二

    <!doctype html> <html lang="en"><head> <meta charset="UTF-8" ...

  4. Unix及Linux编辑器vi/vim基本使用方法

  5. shell_链接命令ln与nohup命令使用方法

    ln命令是一个链接命令,工作中用的比较多的就是对一个文件或者是目录建立起软连接.软连接的概念类似于windows下的快捷方式.比如,在win下,我们经常在安装完word.ppt等office程序后,在 ...

  6. BI之路学习笔记2--SSIS/ETL设计练习三:《DB->定期生成excel表》

    上次笔记记到,用sql任务给参数赋值,映射到变量,然后把数据流任务放到序列容器中进行执行,可以定期生成excel, 现在的问题是: 在EXCEL目标编辑过程中,必须选定某一个特定的excel目标,这样 ...

  7. python爬取新浪股票数据—绘图【原创分享】

    目标:不做蜡烛图,只用折线图绘图,绘出四条线之间的关系. 注:未使用接口,仅爬虫学习,不做任何违法操作. """ 新浪财经,爬取历史股票数据 ""&q ...

  8. 个人永久性免费-Excel催化剂功能第43波-文本处理类函数增强

    Excel的函数有400多个,真正常用的50多个,而常有的文本处理类函数也不多,不是因为文本类处理简单,而是Excel真的有点挤牙膏式的每个版本更新那么几个小函数,普通用户等得急切,但实际上这些小函数 ...

  9. TCP端口复用引发的异常,用setsockopt来解决

    TCP端口复用引发的异常,用setsockopt来解决 我们在并发连接一个服务端时候他会出现这种情况 OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允 ...

  10. classpath和classpath*区别

    classpath和classpath*区别: classpath:只会到你的class路径中查找找文件. classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找. ...