关于C#程序本地化的一个解决方案

平时我们知道,要想把自己的程序推广出去,国际化&本地化必不可少。下面就来讲讲我在我的程序中用到的一个本地化方法。

我的程序地址: Github|Gitee

这个方法是我从Minecraft中得到的灵感,就是在控件的Text属性上动动手脚。

1. 准备

我们需要一个稳定的、便捷的、可随时调用的Ini文件读取库/类(当然,如果你觉得XML, JSON这些你更擅长的话,也可以用你擅长的;这里的Ini文件是一个泛指,不一定指后缀名为.ini的文件,而是文件内容符合Ini文件语法/标准的任何文件),这里推荐SunnyUI.Net
SunnyUI.Net
SunnyUI.Net程序地址: Github|Gitee
(当然我们都知道SunnyUI.Net其实有一套自己的本地化系统,但Wiki里也说了仅限于SunnyUI.Net自己的控件,也就是说它不能用于Windows窗体控件(系统原生控件)的本地化。)
好在SunnyUI.Net给我们提供了一个非常强大的Ini文件读取类,我们可以自己搞定本地化。(PS:别忘了给SunnyUI一个Star!)
Star

1.1.安装SunnyUI.Net

在Visual Studio里,工具>NuGet 包管理器>管理解决方案的 NuGet 程序包…>浏览>搜索SunnyUI
NuGet: GetSunnyUI

点击第一个,选中自己的解决方案,点击安装即可。

1.2. 确定自己的语言文件

一个合格的文件,后缀名是不可少的。为自己程序的语言文件取一个恰当的后缀名十分重要。语言文件,就最好取.lng, .lang这些简洁易懂的后缀名,当然你也可以加上你的程序名。
如果你的程序需要本地化的控件很少,那么你就可以直接写上 语言代码+你的文件后缀名,比如zh-CN.relang
语言文件的文件名通常按照“语言+(-或_)+国家/地区”代码标准。
下面列举了常用的语言与其国家/地区的对照表:

国家/地区语言编码国家/地区语言编码
简体中文(中国)zh-CN繁体中文(台湾地区)zh-TW
繁体中文(香港)zh-HK英语(香港)en-HK
英语(美国)en-US英语(英国)en-GB
英语(全球)en-WW英语(加拿大)en-CA
英语(澳大利亚)en-AU英语(爱尔兰)en-IE
英语(芬兰)en-FI芬兰语(芬兰)fi-FI
英语(丹麦)en-DK丹麦语(丹麦)da-DK
英语(以色列)en-IL希伯来语(以色列)he-IL
英语(南非)en-ZA英语(印度)en-IN
英语(挪威)en-NO英语(新加坡)en-SG
英语(新西兰)en-NZ英语(印度尼西亚)en-ID
英语(菲律宾)en-PH英语(泰国)en-TH
英语(马来西亚)en-MY英语(阿拉伯)en-XA
韩文(韩国)ko-KR日语(日本)ja-JP
荷兰语(荷兰)nl-NL荷兰语(比利时)nl-BE
葡萄牙语(葡萄牙)pt-PT葡萄牙语(巴西)pt-BR
法语(法国)fr-FR法语(卢森堡)fr-LU
法语(瑞士)fr-CH法语(比利时)fr-BE
法语(加拿大)fr-CA西班牙语(拉丁美洲)es-LA
西班牙语(西班牙)es-ES西班牙语(阿根廷)es-AR
西班牙语(美国)es-US西班牙语(墨西哥)es-MX
西班牙语(哥伦比亚)es-CO西班牙语(波多黎各)es-PR
德语(德国)de-DE德语(奥地利)de-AT
德语(瑞士)de-CH俄语(俄罗斯)ru-RU
意大利语(意大利)it-IT希腊语(希腊)el-GR
挪威语(挪威)no-NO匈牙利语(匈牙利)hu-HU
土耳其语(土耳其)tr-TR捷克语(捷克共和国)cs-CZ
斯洛文尼亚语sl-SL波兰语(波兰)pl-PL
瑞典语(瑞典)sv-SE
如果控件很多,还有很多窗体,那么你就可以像我一样,直接使用一个文件夹存储所有的本地化文件,文件夹名就是 `语言代码`,里面的文件名就是窗体名~

example
比如这样。
注意:你的语言文件最好是放在程序的bin目录下,要么就将这些文件的生成操作设置成“嵌入的资源”(当然这里不推荐,毕竟不是所有人都想自己的程序是完全封闭的),不然程序将很难读取到这些文件!

2. 操作

好的,当准备工作完成后,就可以往下走了!

2.1.新建文件

在你的窗体所在目录下新建C#类文件,名为
你的窗体名称.Initializer.cs。当然也可以换成你想要的名字,只是需要注意在文件名前最好加上你的窗体名字,毕竟我们要用到分部类
newfile
新建之后应该是这样:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IDE;
internal class Main
{

}

接下来我们要给他动动手脚。
internal class Main这里,我们在internal后面再输一个partial表示这是Main类的一个分部类。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IDE;
internal partial class Main
{

}

2.2. 代码编写

接下来,我们学习学习微软的方法。
提示:点击VS自动生成的分部类之外的分部类时,会出现一个空窗体,这时我们不要管它,右键>查看代码即可解决。
fk

2.2.1. 第一步

首先,我们新建一个变量_I18nFile和方法InitializeI18n
(变量名和方法名随你的意思,不一定要死搬硬套,不过方法名还是推荐写成Initialze+I18nTranslationLocalize)

using System.Collections.Generic;
using System.Windows.Forms;
using Sunny.UI;

namespace IDE;
internal partial class Main
{
	 private IniFile _I18nFile = new(Application.StartupPath + $"\\Languages\\{GlobalSuppressions.language}\\main.relang", System.Text.Encoding.UTF8);

     private void InitializeTranslation()
     {
     }
}

*注:本文章中出现的C#语言版本均大于等于 C# 9.0
这里就出现了我们本篇文章的重要嘉宾:SunnyUI。
SunnyUI中的IniFileIniFileEx类给我们提供了一套完整的读/写Ini文件的方法,我们直接使用即可。
但请注意: IniFileEx类没有提供IniFile类的编码功能(或者说我目前还没发现),如果使用IniFileEx类读取内容中有中文的文件可能会导致乱码!(手持两把锟斤拷,口中疾呼烫烫烫。脚踏千朵屯屯屯,笑看万物锘锘锘)
所以我们还是推荐使用IniFile类。
IniFile类的构造方法本来只需要一个参数,也就是文件路径,但我们这里使用了它的重载,也就是

IniFile(string file, System.Text.Encoding encoding)

前面的file就是文件路径没什么说的,后面的encoding才是重点。
System.Text.Encoding类给我们提供了如下属性:
Codings
其中ASCII是C#默认的文件编码,但因为中文乱码而“人人喊打”。如果你需要读取中文文件,就需要使用Encoding.UTF8编码进行读取/写入。

2.2.2. 第二步

文件名我们也确定了,IniFile类我们也配置好了,接下来就是喜闻乐见的改控件时间了~
因为我们的文件本质还是Ini文件,所以必须符合Ini文件的规范。下面就是一个很好的Ini版语言文件示例:
newfile.relang

[LangSet]
Name=简体中文(中国)

[I18n]
text.newfile.filename=文件名: 
text.newfile.filetype=文件类型: 
text.newfile.yes=确定
text.newfile.no=取消

Ini文件格式详解(点击传送)
在窗体里,我们也要相应地更改控件的Text属性。
就像这样:
e

e1

2.2.3. 第三步

IniFile类中提供了很多方法供我们读写Ini文件。当然我们不会用到全部方法。
在你的Initializer类中,新建这两个方法(如果你的程序没有Strip类控件就只用写一个,下同),用来读取所有控件名。

private List<Control> GetAllControls(Control control)
{
   List<Control> controls = new List<Control>();
   foreach (Control c in control.Controls)
   {
       controls.Add(c);
       controls.AddRange(GetAllControls(c));
   }
   return controls;
}

private List<ToolStripItem> GetAllToolStripItems(ToolStripItemCollection items)
{
   List<ToolStripItem> toolStripItems = new List<ToolStripItem>();
   foreach (ToolStripItem item in items)
   {
       toolStripItems.Add(item);
       if (item is ToolStripDropDownItem dropDownItem)
       {
           toolStripItems.AddRange(GetAllToolStripItems(dropDownItem.DropDownItems));
       }
   }
   return toolStripItems;
}

接着,在InitializeTranslation()方法中,初始化2个List,一个是主控件,一个是Strip类控件上的控件。
Main.Initializer.cs(部分)

private void InitializeTranslation()
{
	List<Control> controls = GetAllControls(this);
	List<ToolStripItem> toolStripItems = GetAllToolStripItems(this.statusStrip1.Items);
}

接着如果你还有其他的Strip类控件,也可以直接利用这个方法。
Main.Initializer.cs(部分)

private void InitializeTranslation()
{
	List<Control> controls = GetAllControls(this);
	List<ToolStripItem> toolStripItems = GetAllToolStripItems(this.statusStrip1.Items);
	toolStripItems.AddRange(GetAllToolStripItems(this.statusStrip2.Items));
    // 省略部分代码...
}

需要注意的是,如果你想要完整地本地化一个MenuStrip,那么你需要把你的MenuStrip里所有的ToolStripMenuItem都使用GetAllToolStripItems()方法Get一遍。
Main.Initializer.cs(部分)

private void InitializeTranslation()
{
	List<Control> controls = GetAllControls(this);
	List<ToolStripItem> toolStripItems = GetAllToolStripItems(this.statusStrip1.Items);
	toolStripItems.AddRange(GetAllToolStripItems(this.statusStrip2.Items));
    // 省略部分代码...
    toolStripItems.AddRange(GetAllToolStripItems(this.menuStrip1.Items));
    toolStripItems.AddRange(GetAllToolStripItems(this.工具TToolStripMenuItem.DropDownItems));
}

(此处的工具TToolStripMenuItemmenuStrip1的子Item,也要Get一遍)
接着来两个foreach让每个控件乖乖地被本地化:
Main.Initializer.cs(部分)

private void InitializeTranslation()
{
	List<Control> controls = GetAllControls(this);
	List<ToolStripItem> toolStripItems = GetAllToolStripItems(this.statusStrip1.Items);
    // 省略部分代码...
    foreach (Control control in controls)
    {
        control.Text = _I18nFile.ReadString("I18n", control.Text, control.Text);
    }

    foreach (ToolStripItem item in toolStripItems)
    {
        item.Text = _I18nFile.ReadString("I18n", item.Text, item.Text);
    }
}

这里就出现了我们今天的主角——IniFile类的ReadString()方法。
ReadString()方法需要我们提供3个参数:

ReadString(string section, string key, string Default)

section,key分别对应Ini格式的section和name,Default则是若配置文件有值则从配置文件取值,如没有取Default值。
因为我们的本地化文本是直接写在控件的Text属性上的,所以我们需要在文件中读取对应Text对应的值(Value)。
流程大致如下:


Created with Raphaël 2.3.0 开始 获取所有控件 读取控件的Text属性值 用Ini文件中的值替换控件的Text属性值 是否有值? 是否是最后一个控件? 结束 替换为Default值 yes no yes no

如果另外还有控件需要本地化,就直接使用ReadString()方法即可。如:

private void InitializeTranslation()
{
	// 省略部分代码...
	foreach (ToolStripItem item in toolStripItems)
    {
   		item.Text = _I18nFile.ReadString("I18n", item.Text, item.Text);
    }

    this.openFileDialog1.Title = _I18nFile.ReadString("I18n", "this.openFileDialog1.Title", "this.openFileDialog1.Title");
}
2.2.4. 第四步

Ini文件也写好了,本地化的方法也写好了,下面我们来测试一下。
首先将InitializeTranslation()方法放进主类的构造函数中(注意不要放进Load方法里)
(主类就是你的分部类所属的类,即分部类的类名)

public Main()
{
	InitializeComponent(); // Visual Studio自动生成,不能删除
	InitializeTranslation(); // 我们的本地化方法
}

将Ini文件放置于Application.StartupPath\Languages\en-US文件夹下(注意Application.StartupPath是Application类提供的一个属性,可以获取程序运行目录(即Debug目录)),运行试试:
test
可以看到本地化成功。
其他的窗体,如法炮制即可。

3.总结

  1. SunnyUI提供了强大的Ini文件读取类,可以让我们方便地读取到Ini文件里的内容;
  2. 我们可以利用控件的Text属性实现本地化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值