平时我们知道,要想把自己的程序推广出去,国际化&本地化必不可少。下面就来讲讲我在我的程序中用到的一个本地化方法。
这个方法是我从Minecraft中得到的灵感,就是在控件的Text
属性上动动手脚。
1. 准备
我们需要一个稳定的、便捷的、可随时调用的Ini文件读取库/类(当然,如果你觉得XML, JSON这些你更擅长的话,也可以用你擅长的;这里的Ini文件是一个泛指,不一定指后缀名为.ini的文件,而是文件内容符合Ini文件语法/标准的任何文件),这里推荐SunnyUI.Net
。
SunnyUI.Net程序地址: Github|Gitee
(当然我们都知道SunnyUI.Net
其实有一套自己的本地化系统,但Wiki里也说了仅限于SunnyUI.Net
自己的控件,也就是说它不能用于Windows窗体控件(系统原生控件)的本地化。)
好在SunnyUI.Net
给我们提供了一个非常强大的Ini文件读取类,我们可以自己搞定本地化。(PS:别忘了给SunnyUI一个Star!)
1.1.安装SunnyUI.Net
在Visual Studio里,工具>NuGet 包管理器>管理解决方案的 NuGet 程序包…>浏览>搜索SunnyUI
点击第一个,选中自己的解决方案,点击安装即可。
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 |
比如这样。
注意:你的语言文件最好是放在程序的bin
目录下,要么就将这些文件的生成操作设置成“嵌入的资源”(当然这里不推荐,毕竟不是所有人都想自己的程序是完全封闭的),不然程序将很难读取到这些文件!
2. 操作
好的,当准备工作完成后,就可以往下走了!
2.1.新建文件
在你的窗体所在目录下新建C#类文件,名为
你的窗体名称.Initializer.cs
。当然也可以换成你想要的名字,只是需要注意在文件名前最好加上你的窗体名字,毕竟我们要用到分部类。
新建之后应该是这样:
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自动生成的分部类之外的分部类时,会出现一个空窗体,这时我们不要管它,右键>查看代码即可解决。
2.2.1. 第一步
首先,我们新建一个变量_I18nFile
和方法InitializeI18n
(变量名和方法名随你的意思,不一定要死搬硬套,不过方法名还是推荐写成Initialze
+I18n
或Translation
或Localize
)
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中的IniFile
和IniFileEx
类给我们提供了一套完整的读/写Ini文件的方法,我们直接使用即可。
但请注意: IniFileEx
类没有提供IniFile
类的编码功能(或者说我目前还没发现),如果使用IniFileEx
类读取内容中有中文的文件可能会导致乱码!(手持两把锟斤拷,口中疾呼烫烫烫。脚踏千朵屯屯屯,笑看万物锘锘锘)
所以我们还是推荐使用IniFile
类。
IniFile
类的构造方法本来只需要一个参数,也就是文件路径,但我们这里使用了它的重载,也就是
IniFile(string file, System.Text.Encoding encoding)
前面的file
就是文件路径没什么说的,后面的encoding
才是重点。
System.Text.Encoding
类给我们提供了如下属性:
其中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属性。
就像这样:
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));
}
(此处的工具TToolStripMenuItem
是menuStrip1
的子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)。
流程大致如下:
如果另外还有控件需要本地化,就直接使用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目录)),运行试试:
可以看到本地化成功。
其他的窗体,如法炮制即可。
3.总结
- SunnyUI提供了强大的Ini文件读取类,可以让我们方便地读取到Ini文件里的内容;
- 我们可以利用控件的Text属性实现本地化。