作者注

前言

由于现在在做的游戏是面向全球用户的嘛,那势必要准备一套本地化解决方案。

当然,翻译文本和处理图片都是需要成本的,在游戏开发过程中应当尽量避免大段的文本,少用包含文字的图片,这样可以有效减轻工作量。

截止到写这篇文章时,已经实现了部分功能,方案基本可用,且不影响其他模块的开发

需求

已经实现了的

下面的几项都是已经可用了的功能,不过可能易用性和健壮性上还有提升空间。

可配置可导出的本地化表

本地化的一个重要目的就是使用于显示给玩家的文本处于随时可调整的状态,所以一定要有一个表格来存放程序用到的 ID 和对应实际显示的文本。

我的做法是使用 Excel,为每种语言建立一个 Sheet 页,每个 Sheet 有3列,如下表:


程序可以通过 ID 来获取对应的字符串,当然只是一个数字的话太难以理解,为了提高程序可读性,建议使用别名(Alias)来标识某个字符串。

完成表格后就可以准备导出了,我用 Python 写了一个脚本用于把 Excel 表格导出为 Sqlite 数据库,核心模块是xlrd和sqlite3。每种语言一个单独的文件,方便程序选择性加载和后期的针对性更新。

优化空间

导出时检查,对比每种语言的 Sheet 页,前两列(ID和别名)是否一致

导出时检查,有无重复的 ID 或别名

Unity 中加载对应语言

上一步中我们已经有了若干 Sqlite 数据库文件,记得放在 Unity 的 StreamingAsset 目录里


Unity 在打包的时候不会打包StreamingAsset目录中的文件,如果不放进这个目录的话 Unity 会在打包后就加载不到数据库文件了,另外一个好处是方便更新,以及玩家自制的语言包

游戏启动时会加载指定的语言(以中文为例)的数据库文件,部分代码如下:
public void SetLanguage(LocalizationType languageType)
{
m_curLanguage = languageType;
// 根据语言设置重载本地化表
string dbPath = Application.streamingAssetsPath + "/Localization/" + m_curLanguage.ToString().ToLower() + ".db"; // 语言包文件路径
m_dictTextWithId.Clear();
m_dictTextWithAlias.Clear();
// 从dbPath读取数据,加入两个dict
SqliteHelper helper = new SqliteHelper(dbPath);
var reader = helper.Query("SELECT * FROM main");
int idOrdinal = reader.GetOrdinal("Id");
int aliasOrdinal = reader.GetOrdinal("Alias");
int textOridinal = reader.GetOrdinal("Text");
while (reader.Read())
{
int nId = reader.GetInt32(idOrdinal);
string strAlias = reader.GetString(aliasOrdinal);
string strText = reader.GetString(textOridinal);
m_dictTextWithId[nId] = strText;
m_dictTextWithAlias[strAlias] = strText;
}
reader.Close();
helper.Dispose();
Debug.LogFormat("加载了{0}条本地化记录", m_dictTextWithId.Count);
}

之后程序在需要显示文本的地方调用 GetString 方法即可,为了同时支持使用 ID 和别名,GetString 方法有两个重载

// 根据ID获取字符串
public string GetString(int id)
{
if (m_dictTextWithId.ContainsKey(id))
{
return m_dictTextWithId[id];
}
Debug.LogWarningFormat("[本地化]未知的ID:{0}", id);
return string.Format("{{{0}_{1}}}", m_dictLangAbbr[m_curLanguage], id);
}
// 根据别名获取字符串
public string GetString(string alias)
{
if (m_dictTextWithAlias.ContainsKey(alias))
{
return m_dictTextWithAlias[alias];
}
Debug.LogWarningFormat("[本地化]未知的别名:{0}", alias);
return string.Format("{{{0}_{1}}}", m_dictLangAbbr[m_curLanguage], alias);
}

为了提高易用性,我弄了一个组件脚本,可以把它挂载在 UI 对象上,以减少工作复杂度,还能在设计 UI 时更直观。

public class UILocalization : MonoBehaviour
{
void Start()
{
m_textCom = GetComponent();
if (m_textCom != null &&
m_textCom.text.StartsWith("{") &&
m_textCom.text.EndsWith("}"))
{
// Text组件中的文本是{xxx}形式的,那就认为中间的xxx是一个别名
m_alias = m_textCom.text.Substring(1, m_textCom.text.Length - 2);
m_textCom.text = LocalizationManager.GetString(m_alias);
}
}
}

于是在设计 UI 时就可以这样使用:


在游戏启动后{coin}会被自动替换成金币

本地化字符串支持术语和参数

需要术语的情况:我们游戏的某种语言由多人共同翻译,如果没有专人校对的话,不同人对于同一个单词或短语的翻译可能都会有差距,这样会使玩家困惑,同样的一个东西游戏中居然会对应多个同义词,如果能规范统一基本术语的翻译就好了。

需要参数的情况:我们想要在UI中显示名次,此时不同语言之间的语序是有区别的(中文“第3名”英文“Rank 3”),如果只是使用字符串拼接的话就会很难实现效果比较理想的翻译。

所以我们支持在本地化字符串中填写术语和参数,多说无益,直接看怎么配:


嗯,一目了然。。应该吧233

为了方便演示,i_have_zh 表示中文字符串,i_have_en 则是英文的,用{数字}来表示参数,具体内容由程序调用时设定,用 #alias# 表示术语,这里是使用了 coin 的统一翻译,避免了上面提到过的情况。

动态调整语言

在之前创建的 UILocalization 组件里加入一个事件监听,监听的内容为游戏语言设置发生变化,当玩家变更语言时,LocalizationManager 会给每一个上述组件发送事件,组件收到事件后重新执行初始化操作即可。

// Start方法中追加一行
LocalizationManager.instance.dispatcher.AddEventListener(Event.EVENT_LANGUAGE_CHANGE, OnLanguageChange);
// ...
void OnLanguageChange(Event e)
{
if (m_textCom != null)
{
m_textCom.text = LocalizationManager.GetString(m_alias);
}
}

游戏启动时自动匹配系统语言

这一条比较简单,使用.Net提供的方法即可获得系统设置的语言代号,代号符合ISO-639标准,具体可查阅相关资料,对于不支持的系统语言,使用默认语言英文即可。

string localLangId = System.Globalization.CultureInfo.InstalledUICulture.Name
if (localLangId == "zh-CN")
{
SetLanguage(LocalizationType.CHINESE_SIMPLIFIED);
}
// else if more...
else
{
SetLanguage(LocalizationType.ENGLISH);
}

计划中尚未实现

下面几项是已经在计划中还没做的功能,这些功能并不是可选项,在游戏上线前是一定要做完的,之后心情好的话说不定还会有这篇文章的后续(doge

包含文字的图片的本地化替换

现在支持本地化替换的只有Text和TextMesh,之后会考虑支持图片(UGUI中Image)的替换。图片不像文本,不能直接配置在 Excel 表中,不过倒是可以配置图片文件的路径,导出时为每一种语言特有的 UI 图片统一打包。

支持第三方翻译(社区翻译)

游戏计划面向全球用户发售,我们不可能为每一种语言都准备本地化翻译,所以让热心的玩家来帮忙做这些事情会更好,开放上述配置表的接口,让普通用户也可以方便地把游戏翻译成自己的母语并导出分享给其他玩家。