DotNetCore 3.0 助力 WPF本地化

2022-10-17,,,,

概览

随着我们的应用程序越来越受欢迎,我们的下一步将要开发多语言功能。方便越来越多的国家使用我们中国的应用程序,
基于 wpf 本地化,我们很多时候使用的是系统资源文件,可是动态切换本地化,就比较麻烦了。
有没有一种方法既可以适用系统的资源文件,又能方便快捷的切换本地化呢?

实现思路

现在我们将要实现的是基于 dotnetcore 3.0 以上版本 and wpf 桌面应用程序模块化的多语言功能。
动态切换多语言思路:

  • 把所有模块的资源文件添加到字典集合。
  • 将资源文件里的key,绑定到前台。
  • 通过通知更改 currentculture 多语言来使用改变的语言文件里的key。
  • 通过绑定 binding 拼接path 在输出。

动态切换

我们先来看实现结果

第一行是我们的主程序的数据展示,用于业务中的本地化
第二行是我们业务模块a的数据展示
第三行是我们业务模块b的数据展示

来看一下xaml展示

搭建模拟业务项目

创建一个wpf app(.net core)应用程序

创建完成后,我们需要引入业务a模块及业务b模块和业务帮助模块

ps:根据自己的业务需要来完成项目的搭建。本教程完全适配多语言功能。

使用.resx资源文件

在各个模块里添加strings 文件夹用来包含 各个国家和地区的语言文件。

多语言可以参考:https://github.com/unrundead/wpf---localization/blob/master/combolistlanguages.txt

资源文件可以放在任意模块内,比如业务模块a ,主程序,底层业务,控件工具集等

创建各个业务模块资源文件

strings文件夹可以任意命名
sr资源文件可以任意命名

帮助类

封装到底层供各个模块调用

    public class translationsource : inotifypropertychanged
    {
        public static translationsource instance { get; } = new translationsource();

        private readonly dictionary<string, resourcemanager> resourcemanagerdictionary = new dictionary<string, resourcemanager>();

        public string this[string key]
        {
            get
            {
                tuple<string, string> tuple = splitname(key);
                string translation = null;
                if (resourcemanagerdictionary.containskey(tuple.item1))
                    translation = resourcemanagerdictionary[tuple.item1].getstring(tuple.item2, currentculture);
                return translation ?? key;
            }
        }

        private cultureinfo currentculture = cultureinfo.installeduiculture;
        public cultureinfo currentculture
        {
            get { return currentculture; }
            set
            {
                if (currentculture != value)
                {
                    currentculture = value;
                    // string.empty/null indicates that all properties have changed
                    propertychanged?.invoke(this, new propertychangedeventargs(string.empty));
                }
            }
        }

        // wpf bindings register propertychanged event if the object supports it and update themselves when it is raised
        public event propertychangedeventhandler propertychanged;

        public void addresourcemanager(resourcemanager resourcemanager)
        {
            if (!resourcemanagerdictionary.containskey(resourcemanager.basename))
            {
                resourcemanagerdictionary.add(resourcemanager.basename, resourcemanager);
            }
        }

        public static tuple<string, string> splitname(string local)
        {
            int idx = local.tostring().lastindexof(".");
            var tuple = new tuple<string, string>(local.substring(0, idx), local.substring(idx + 1));
            return tuple;
        }
    }

    public class translation : dependencyobject
    {
        public static readonly dependencyproperty resourcemanagerproperty =
            dependencyproperty.registerattached("resourcemanager", typeof(resourcemanager), typeof(translation));

        public static resourcemanager getresourcemanager(dependencyobject dependencyobject)
        {
            return (resourcemanager)dependencyobject.getvalue(resourcemanagerproperty);
        }

        public static void setresourcemanager(dependencyobject dependencyobject, resourcemanager value)
        {
            dependencyobject.setvalue(resourcemanagerproperty, value);
        }
    }

    public class locextension : markupextension
    {
        public string stringname { get; }

        public locextension(string stringname)
        {
            stringname = stringname;
        }

        private resourcemanager getresourcemanager(object control)
        {
            if (control is dependencyobject dependencyobject)
            {
                object localvalue = dependencyobject.readlocalvalue(translation.resourcemanagerproperty);

                // does this control have a "translation.resourcemanager" attached property with a set value?
                if (localvalue != dependencyproperty.unsetvalue)
                {
                    if (localvalue is resourcemanager resourcemanager)
                    {
                        translationsource.instance.addresourcemanager(resourcemanager);

                        return resourcemanager;
                    }
                }
            }

            return null;
        }

        public override object providevalue(iserviceprovider serviceprovider)
        {
            // targetobject is the control that is using the locextension
            object targetobject = (serviceprovider as iprovidevaluetarget)?.targetobject;

            if (targetobject?.gettype().name == "shareddp") // is extension used in a control template?
                return targetobject; // required for template re-binding

            string basename = getresourcemanager(targetobject)?.basename ?? string.empty;

            if (string.isnullorempty(basename))
            {
                // rootobject is the root control of the visual tree (the top parent of targetobject)
                object rootobject = (serviceprovider as irootobjectprovider)?.rootobject;
                basename = getresourcemanager(rootobject)?.basename ?? string.empty;
            }

            if (string.isnullorempty(basename)) // template re-binding
            {
                if (targetobject is frameworkelement frameworkelement)
                {
                    basename = getresourcemanager(frameworkelement.templatedparent)?.basename ?? string.empty;
                }
            }

            binding binding = new binding
            {
                mode = bindingmode.oneway,
                path = new propertypath($"[{basename}.{stringname}]"),
                source = translationsource.instance,
                fallbackvalue = stringname
            };

            return binding.providevalue(serviceprovider);
        }
    }

前台绑定

//引用业务模块
xmlns:ext="clr-namespace:wpfutil.extension;assembly=wpfutil"
// 引用刚才你命名的文件夹名字
xmlns:resx="clr-namespace:modulea.strings"
// 每个模块通过帮助类,将当前模块的资源类,
// 加载到资源管理集合里面用于分配每个键值
// 引用刚才你命名的资源文件名字 -> sr
ext:translation.resourcemanager="{x:static resx:sr.resourcemanager}"

显示文字

//读取资源文件里的键值
<label content="{ext:loc test}" fontsize="21" />

后台实现

根据业务的需要,我们在界面上无法适用静态文字显示的,一般通过后台代码来完成,对于 code-behind 的变量使用,同样可以应用于资源字典。
比如在业余模块代码段里的模拟实现

// sr 是当前业务模块的资源文件类,管理当前模块的资源字符串。
// 根据不同的 `currentculture` 选择相对应的本地化
message = string.format(sr.resourcemanager.getstring("message",translationsource.instance.currentculture),system.datetime.now);

ps: 欢迎各位大佬慷慨指点,有不足之处,请指出!有疑问,请指出,喜欢它,请支持!

下载地址

https://github.com/androllen/wpfnetcorelocalization

相关链接

https://github.com/jinjinov/wpf-localization-multiple-resource-resx-one-language/blob/master/readme.md

《DotNetCore 3.0 助力 WPF本地化.doc》

下载本文的Word格式文档,以方便收藏与打印。