前言
公司最近接到欧美订单,需要做相应的软件英文版本,项目即有WPF也有Winform。介绍一下所用方法。
- WPF项目使用网上推荐的Resx Manager,把所有需要翻译的绑定都定义一个key,然后就是原文和翻译,总体感觉这种方式挺好就是大项目操作太过繁琐。
- Winform项目使用遍历主窗口及其子控件的所有Text,使用百度API翻译并保存本地,优点是对付大项目够快,缺点是每次切换都要来回遍历效率差点意思。
Winform
先说Winform,参考了winform项目实现国际化(语言切换)功能,该文章使用了作者自定义的sqllite操作封装,需要自行替换。因为是大项目,在此基础上做优化。下面为主要优化项:
- 使用System.Data.SQLite原始库,复制即用。
- 添加调用百度翻译API的QPS为8,即每次调用间隔时间约为125毫秒,根据百度文档定义10qps自行调整,未限制会报频繁访问错误,翻译结果为null;
- 过滤控件Text为数字、字符符号、空、未包含中文的情况,否则保存文件一大堆空和数字符号条目,且浪费API调用次数;
- 语言选择直接保存在app.config中Language字段,记录语言选择;
- 考虑可选优化项:存储在sqllite的数据可在启动时读取到内存,避免每次切换都读取数据库,但考虑到语言切换非常用功能,即设置后正常加载后不会再进行切换,暂时搁置。
- 数据库主键也可以优化为控件Name,包括业务代码方法内的where字句,我这样直接使用name+text主要为项目老项目,重命名返工定义唯一任务量比较大,设计时未定义好不同页面控件Name名称,规范命名为页面名_控件名,使之实现程序内控件name唯一。这样就可使之成为主键,或者最起码Where字句只需要筛选name。
- 保存到sqllite的方案当然也可以自行实现替换为Json文件。
public class SqliteDAL
{
private readonly string _connectionString;
public SqliteDAL()
{
var config = ConfigManager.Instance;
_connectionString = string.Format("Data Source={0};Version=3;"
, config.GetSetting("path", $"{App.Path}\\DataBase\\你的数据库.db")
);
InitializeDatabase();
}
/// <summary>
/// 初始化数据库,创建 ControlTexts 表(如果不存在)。
/// </summary>
private void InitializeDatabase()
{
using (var conn = new SQLiteConnection(_connectionString))
{
conn.Open();
string createTableQuery = @"
CREATE TABLE IF NOT EXISTS ControlTexts (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ControlName TEXT NOT NULL,
OriginalText TEXT NOT NULL,
TranslatedText TEXT
)";
using (var cmd = new SQLiteCommand(createTableQuery, conn))
{
cmd.ExecuteNonQuery();
}
}
}
/// <summary>
/// 递归遍历主窗体控件form,并保存其文本(原文和翻译)到数据库
/// </summary>
/// <param name="form"></param>
/// <returns></returns>
public async Task SaveControlTextsToDatabase(Form form)
{
if (form == null) return;
var controlsData = await CollectControlTexts(form);
SaveControlTextsToDatabase(controlsData);
}
/// <summary>
/// 递归遍历控件并翻译文本
/// </summary>
private async Task<(string ControlName, string OriginalText, string TranslatedText)[]> CollectControlTexts(Control parent)
{
var controlDataList = new System.Collections.Generic.List<(string, string, string)>();
async Task ProcessControl(Control control)
{
// 如果是中文字符,进行翻译
if (!string.IsNullOrWhiteSpace(control.Name) && !string.IsNullOrWhiteSpace(control.Text) && ContainsChinese(control.Text))
{
await Task.Delay(125);
string translatedText = await TranslateText(control.Text);
if (string.IsNullOrEmpty(translatedText))
{
Console.WriteLine($"翻译失败:{control.Text}");
translatedText = control.Text;
}
controlDataList.Add((control.Name, control.Text, translatedText));
}
foreach (Control child in control.Controls)
{
await ProcessControl(child);
}
}
await ProcessControl(parent);
return controlDataList.ToArray();
}
/// <summary>
/// 翻译文本
/// </summary>
private async Task<string> TranslateText(string originalText)
{
var translationResult = await TranslateUtil.TranslatePost(originalText, Language.中文, Language.英语);
if (translationResult?.ResponseJson != null)
{
try
{
using (var jsonDocument = JsonDocument.Parse(translationResult.ResponseJson))
{
var root = jsonDocument.RootElement;
if (root.TryGetProperty("error_code", out _))
{
return null;
}
if (root.TryGetProperty("trans_result", out var transResult))
{
return transResult[0].GetProperty("dst").GetString();
}
}
}
catch (Exception)
{
return null;
}
}
return null;
}
/// <summary>
/// 将控件文本数据保存到 SQLite 数据库
/// </summary>
private void SaveControlTextsToDatabase((string ControlName, string OriginalText, string TranslatedText)[] controlsData)
{
using (var conn = new SQLiteConnection(_connectionString))
{
conn.Open();
using (var transaction = conn.BeginTransaction())
{
foreach (var (controlName, originalText, translatedText) in controlsData)
{
string selectQuery = @"SELECT COUNT(*) FROM ControlTexts WHERE ControlName = @ControlName AND OriginalText = @OriginalText";
using (var selectCmd = new SQLiteCommand(selectQuery, conn))
{
selectCmd.Parameters.AddWithValue("@ControlName", controlName);
selectCmd.Parameters.AddWithValue("@OriginalText", originalText);
long count = (long)selectCmd.ExecuteScalar();
if (count > 0)
{
string updateQuery = @"UPDATE ControlTexts SET TranslatedText = @TranslatedText WHERE ControlName = @ControlName AND OriginalText = @OriginalText";
using (var updateCmd = new SQLiteCommand(updateQuery, conn))
{
updateCmd.Parameters.AddWithValue("@TranslatedText", translatedText);
updateCmd.Parameters.AddWithValue("@ControlName", controlName);
updateCmd.Parameters.AddWithValue("@OriginalText", originalText);
updateCmd.ExecuteNonQuery();
}
}
else
{
string insertQuery = @"INSERT INTO ControlTexts (ControlName, OriginalText, TranslatedText) VALUES (@ControlName, @OriginalText, @TranslatedText)";
using (var insertCmd = new SQLiteCommand(insertQuery, conn))
{
insertCmd.Parameters.AddWithValue("@ControlName", controlName);
insertCmd.Parameters.AddWithValue("@OriginalText", originalText);
insertCmd.Parameters.AddWithValue("@TranslatedText", translatedText);
insertCmd.ExecuteNonQuery();
}
}
}
}
transaction.Commit();
}
}
}
/// <summary>
/// 替换窗体控件的文本为数据库中的翻译文本
/// </summary>
public void ReplaceToEnglish(Form form)
{
ReplaceControlTexts(form, true);
}
/// <summary>
/// 替换窗体控件的文本为数据库中的原文本
/// </summary>
public void ReplaceToChinese(Form form)
{
ReplaceControlTexts(form, false);
}
private void ReplaceControlTexts(Form form, bool useTranslatedText)
{
if (form == null) return;
using (var conn = new SQLiteConnection(_connectionString))
{
conn.Open();
ReplaceControlTextsRecursive(form, conn, useTranslatedText);
}
}
private void ReplaceControlTextsRecursive(Control parent, SQLiteConnection conn, bool useTranslatedText)
{
if (!string.IsNullOrEmpty(parent.Text))
{
var controlName = parent.Name;
var column = useTranslatedText ? "TranslatedText" : "OriginalText";
var sql = useTranslatedText ? "AND OriginalText = @Text" : $"AND TranslatedText = @Text";
var query = $"SELECT {column} FROM ControlTexts WHERE ControlName = @ControlName {sql}";
using (var cmd = new SQLiteCommand(query, conn))
{
cmd.Parameters.AddWithValue("@ControlName", controlName);
cmd.Parameters.AddWithValue("@Text", parent.Text);
var result = cmd.ExecuteScalar();
if (result is string text && !string.IsNullOrEmpty(text))
{
parent.Text = text;
}
}
}
foreach (Control control in parent.Controls)
{
if (!string.IsNullOrEmpty(control.Text))
{
var controlName = control.Name;
var column = useTranslatedText ? "TranslatedText" : "OriginalText";
var sql = useTranslatedText ? "AND OriginalText = @Text" : $"AND TranslatedText = @Text";
var query = $"SELECT {column} FROM ControlTexts WHERE ControlName = @ControlName {sql}";
using (var cmd = new SQLiteCommand(query, conn))
{
cmd.Parameters.AddWithValue("@ControlName", controlName);
cmd.Parameters.AddWithValue("@Text", control.Text);
var result = cmd.ExecuteScalar();
if (result is string text && !string.IsNullOrEmpty(text))
{
control.Text = text;
}
}
}
if (control.HasChildren)
{
ReplaceControlTextsRecursive(control, conn, useTranslatedText);
}
}
}
// 检查字符串是否包含中文字符
private bool ContainsChinese(string text)
{
return Regex.IsMatch(text, @"[\u4e00-\u9fa5]");
}
}
主窗口调用
#region 多语言
private void uiSwitch1_ValueChanged(object sender, bool value)
{
try
{
if (value)
{
SwitchLanguage(Language.英语);
}
else
{
SwitchLanguage(Language.中文);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void SwitchLanguage(string language)
{
SqliteDAL sqliteDAL = new();
if (language == Language.英语)
{
/* //遍历翻译并保存
await sqliteDAL.SaveControlTextsToDatabase(this);*/
sqliteDAL.ReplaceToEnglish(this);
SaveLanguageSetting(Language.英语);
}
else
{
sqliteDAL.ReplaceToChinese(this);
SaveLanguageSetting(Language.中文);
}
}
private void InitLanguageSetting()
{
currentLanguage = ConfigurationManager.AppSettings["Language"];
if (string.IsNullOrEmpty(currentLanguage))
{
currentLanguage = Language.中文; // 默认语言
}
if (currentLanguage == Language.英语)
{
SwitchLanguage(Language.英语);
}
}
private void SaveLanguageSetting(string lang)
{
// 1. 获取当前的配置文件
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
// 2. 修改 appSettings 中的值
config.AppSettings.Settings["Language"].Value = lang;
// 3. 保存配置文件
config.Save(ConfigurationSaveMode.Modified);
// 4. 强制重新加载配置
ConfigurationManager.RefreshSection("appSettings");
}
#endregion
程序启动初始化选择好时机放置语言初始化方法,完成以上就完成了翻译工作,当然并没有结束,程序多语言还有个大问题是语言界面排版,需要使用缩写缩短翻译+调整控件大小间距。
大家有什么更好的方法,或者优化项欢迎留言讨论!