目录
- 0 前言
- 1 多语言配置
- 2 本地化实现
- 2.1 读取多语言文本的通用方法
- 2.2 动态文本本地化
- 2.3 静态文本本地化
- 3 方案扩展
- 3.1 LanguageText扩展
- 3.1.1 展示Excel里对应ID的文本
- 3.1.2 自动填充ID
- 3.1.3 同步prefab的修改
- 3.1.4 完整代码
- 3.2 自动LanguageText挂载
- 4* 内嵌文本图片的本地化
0 前言
这套本地化处理方案是基于Excel实现的,可以先扒一下我之前这篇文章的代码:Unity-Excel数据处理,这里会用到。
另外,这篇文章主要讲了纯文本的本地化方案,对于有内嵌字的图片,在最后会简单说一下处理方式。
1 多语言配置
通常情况下,项目内的文本会分散在各个表格之中,比如道具描述、技能说明等。但文本太过分散,会增加翻译难度,还可能会产生遗漏。所以我采取的方案是将多语言文本全部配置在同一张表格中,而其他表格则记录多语言文本的ID。
像这样:
Player.xlsx
LanguageLocalization.xlsx
像Player里的基础技能描述一般是便于制表人看,程序读取文本时用的是后面配置的本地化ID。
2 本地化实现
在开始之前先对LanguageLocalization.xlsx使用前言提到的excel数据处理方案,得到对应的CSharp和JsonData文件。
2.1 读取多语言文本的通用方法
这个通用方法应该是传入一个参数,然后根据当前语言环境返回对应的文本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LanguageTool
{
public static SystemLanguage? defaultLanguage = SystemLanguage.Chinese;
private static Dictionary<int, LanguageLocalization> languageDict;
public static string GetLanguage(int id) {
if (languageDict == null) {
languageDict = LanguageLocalization.LoadJson();
}
//默认语言>系统语言
if (defaultLanguage != null) {
return GetLanguage(id, defaultLanguage.Value);
}
else {
return GetLanguage(id, Application.systemLanguage);
}
}
private static string GetLanguage(int id,SystemLanguage curLanguage) {
switch (curLanguage) {
case SystemLanguage.Chinese:
//Excel里的换行符可能是\r\n的形式
return languageDict[id].Text_Cn.Replace("\r\n", "\n");
default:
return languageDict[id].Text_En.Replace("\r\n", "\n");
}
}
}
2.2 动态文本本地化
动态文本指的是
1.像多语言配置中提到的Player的例子,技能描述、道具描述等。
2.像“失去2个道具”这样数字不确定的文本。
对于1直接根据表格内配置的Id读取多语言文本。
对于2需要先在多语言表格里自己配置一项,如失去{0}个道具,然后在代码里通过这样的形式获取文本:
tipText.text = string.Format(LanguageTool.GetLanguage(Id),num);
2.3 静态文本本地化
静态文本指的是写死在项目里,如Prefab上的文本。这种文本我写了一个组件挂在Text下,该组件会在Awake时将Text替换为对应语言环境的文本。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Text))]
public class LanguageText : MonoBehaviour
{
public int LanguageId = -1;
private void Awake() {
if (LanguageId == -1) {
return;
}
var text = transform.GetComponent<Text>();
text.text = LanguageTool.GetLanguage(LanguageId);
}
}
3 方案扩展
除了以上必要的功能实现外,我们发现其实还可以从便利性出发去扩展当前这套方案。
3.1 LanguageText扩展
对于LanguageText组件,我们会希望有这些功能去辅助我们开发:
1.Inspector下展示所填写ID在Excel里对应的中英文,这样我们可以确定当前填写的ID正确。
2.当我们添加Prefab上的文本时,我们需要填写对应的多语言ID,如果去Excel里查找和编辑就有点麻烦,所以我们希望有一个方法可以实现:
1)如果Excel表里已经有这个文本了,那么自动填充对应ID。
2)如果Excel表里没有对应文本,那么在Excel里新增一行,然后填充对应ID。
3.修改Prefab上的文本时,我们希望有一个方法可以同步修改Excel里的文本。
表格:
3.1.1 展示Excel里对应ID的文本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
[CustomEditor(typeof(LanguageText))]
public class LanguageTextEx : Editor
{
private Dictionary<int, LanguageLocalization> languageDict;
private LanguageText languageText;
private Text textComponent;
public override void OnInspectorGUI() {
base.OnInspectorGUI();
if (languageDict == null) {
languageDict = LanguageLocalization.LoadJson();
}
if (textComponent == null) {
languageText = target as LanguageText;
textComponent = languageText.transform.GetComponent<Text>();
}
if (languageText.LanguageId <= -1 || !languageDict.ContainsKey(languageText.LanguageId)) {
GUILayout.Label("当前ID不合法");
return;
}
ShowExcelText();
}
/// <summary>
/// 展示Excel对应ID的文本
/// </summary>
private void ShowExcelText() {
GUILayout.Label($"Prefab上文本:{textComponent.text}");
GUILayout.Label($"Excel上英文:{languageDict[languageText.LanguageId].Text_Cn}");
GUILayout.Label($"Excel上中文:{languageDict[languageText.LanguageId].Text_En}");
}
}
ID为0
ID为-1
3.1.2 自动填充ID
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using System.Linq;
using System.IO;
using OfficeOpenXml;
[CustomEditor(typeof(LanguageText))]
public class LanguageTextEx : Editor
{
private string languageExcelPath = "/Excel/LanguageLocalization.xlsx";
//...
public override void OnInspectorGUI() {
//...
if (GUILayout.Button("匹配中文填充LanguageId")) {
RecalculateLanguageId();
}
if (languageText.LanguageId <= -1 || !languageDict.ContainsKey(languageText.LanguageId)) {
GUILayout.Label("当前ID不合法");
return;
}
ShowExcelText();
}
//...
/// <summary>
/// 填充LanguageID
/// </summary>
private void RecalculateLanguageId() {
var list = languageDict.Where(x => IsTwoStrsSame(textComponent.text, x.Value.Text_Cn)).Select(x => x.Key).ToList();
if (list.Count > 0) {
//有匹配项
languageText.LanguageId = list[0];
}
else {
//无匹配项->写入Excel
var file = new FileInfo(Application.dataPath+languageExcelPath);
using (ExcelPackage package = new ExcelPackage(file)) {
var worksheet = package.Workbook.Worksheets["Sheet1"];
if (worksheet == null) {
Debug.Log($"没有找到路径为{languageExcelPath}的工作表");
}
else {
var endRow = worksheet.Dimension.End.Row;
var endIndex = -1;
for (var i = endRow; i >= 0; i--) {
var indexStr = worksheet.Cells[i, 2].Value?.ToString();
//找到最后一行非空数据
if (!string.IsNullOrEmpty(indexStr) && int.TryParse(indexStr, out endIndex)) {
endRow = i;
break;
}
}
//第二项指的是第几列,从1开始计数
worksheet.Cells[endRow + 1, 2].Value = endIndex + 1;
worksheet.Cells[endRow + 1, 3].Value = textComponent.text;
languageText.LanguageId = endIndex + 1;
}
package.Save();
}
//重新生成CSharp和JsonData
ExcelToCSharpClass.ExcelToCSharpClassMethod(Application.dataPath + languageExcelPath);
//刷新
languageDict = LanguageLocalization.LoadJson();
AssetDatabase.Refresh();
}
Debug.Log("LanguageId填充成功");
}
/// <summary>
/// 去换行符影响
/// </summary>
private bool IsTwoStrsSame(string str1, string str2) {
str1 = str1.Replace("\r\n", "\n");
str2 = str2.Replace("\r\n", "\n");
return str1 == str2;
}
}
匹配前
匹配后
3.1.3 同步prefab的修改
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using System.Linq;
using System.IO;
using OfficeOpenXml;
[CustomEditor(typeof(LanguageText))]
public class LanguageTextEx : Editor
{
//...
public override void OnInspectorGUI() {
//...
if (GUILayout.Button("匹配中文填充LanguageId")) {
RecalculateLanguageId();
}
if (GUILayout.Button("同步Prefab上的文本到Excel的中文文本")) {
SetTextToExcel(SystemLanguage.Chinese);
}
if (GUILayout.Button("同步Prefab上的文本到Excel的英文文本")) {
SetTextToExcel(SystemLanguage.English);
}
}
//...
/// <summary>
/// 同步文本到Excel表格里
/// </summary>
public void SetTextToExcel(SystemLanguage languageType) {
if (languageType != SystemLanguage.Chinese && languageType != SystemLanguage.English)
{
return;
}
var file = new FileInfo(Application.dataPath + languageExcelPath);
using (ExcelPackage package = new ExcelPackage(file))
{
var worksheet = package.Workbook.Worksheets["Sheet1"];
if (worksheet == null)
{
Debug.Log($"没有找到路径为{languageExcelPath}的工作表");
}
else
{
var endRow = worksheet.Dimension.End.Row;
for(var i = 4; i <= endRow; i++)
{
var id = int.Parse(worksheet.Cells[i, 2].Value.ToString().Trim());
if (id == languageText.LanguageId)
{
switch (languageType) {
case SystemLanguage.Chinese:
worksheet.Cells[i, 3].Value = textComponent.text;
break;
case SystemLanguage.English:
worksheet.Cells[i, 4].Value = textComponent.text;
break;
default:
break;
}
break;
}
}
}
package.Save();
}
//重新生成CSharp和JsonData
ExcelToCSharpClass.ExcelToCSharpClassMethod(Application.dataPath + languageExcelPath);
//刷新
languageDict = LanguageLocalization.LoadJson();
AssetDatabase.Refresh();
}
}
更改Prefab上的文字
点击同步
3.1.4 完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using System.Linq;
using System.IO;
using OfficeOpenXml;
[CustomEditor(typeof(LanguageText))]
public class LanguageTextEx : Editor
{
private string languageExcelPath = "/Excel/LanguageLocalization.xlsx";
private Dictionary<int, LanguageLocalization> languageDict;
private LanguageText languageText;
private Text textComponent;
public override void OnInspectorGUI() {
base.OnInspectorGUI();
if (languageDict == null) {
languageDict = LanguageLocalization.LoadJson();
}
if (textComponent == null) {
languageText = target as LanguageText;
textComponent = languageText.transform.GetComponent<Text>();
}
if (GUILayout.Button("匹配中文填充LanguageId"))
{
RecalculateLanguageId();
}
if (languageText.LanguageId <= -1 || !languageDict.ContainsKey(languageText.LanguageId)) {
GUILayout.Label("当前ID不合法");
return;
}
ShowExcelText();
if (GUILayout.Button("同步Prefab上的文本到Excel的中文文本"))
{
SetTextToExcel(SystemLanguage.Chinese);
}
if (GUILayout.Button("同步Prefab上的文本到Excel的英文文本"))
{
SetTextToExcel(SystemLanguage.English);
}
}
/// <summary>
/// 展示Excel对应ID的文本
/// </summary>
private void ShowExcelText() {
GUILayout.Label($"Prefab上文本:{textComponent.text}");
GUILayout.Label($"Excel上中文:{languageDict[languageText.LanguageId].Text_Cn}");
GUILayout.Label($"Excel上英文:{languageDict[languageText.LanguageId].Text_En}");
}
/// <summary>
/// 填充LanguageID
/// </summary>
private void RecalculateLanguageId() {
var list = languageDict.Where(x => IsTwoStrsSame(textComponent.text, x.Value.Text_Cn)).Select(x => x.Key).ToList();
if (list.Count > 0) {
//有匹配项
languageText.LanguageId = list[0];
}
else {
//无匹配项->写入Excel
var file = new FileInfo(Application.dataPath + languageExcelPath);
using (ExcelPackage package = new ExcelPackage(file)) {
var worksheet = package.Workbook.Worksheets["Sheet1"];
if (worksheet == null) {
Debug.Log($"没有找到路径为{languageExcelPath}的工作表");
}
else {
var endRow = worksheet.Dimension.End.Row;
var endIndex = -1;
for (var i = endRow; i >= 4; i--) {
var indexStr = worksheet.Cells[i, 2].Value?.ToString();
if (!string.IsNullOrEmpty(indexStr) && int.TryParse(indexStr, out endIndex)) {
endRow = i;
break;
}
}
worksheet.Cells[endRow + 1, 2].Value = endIndex + 1;
worksheet.Cells[endRow + 1, 3].Value = textComponent.text;
languageText.LanguageId = endIndex + 1;
}
package.Save();
}
//重新生成CSharp和JsonData
ExcelToCSharpClass.ExcelToCSharpClassMethod(Application.dataPath + languageExcelPath);
//刷新
languageDict = LanguageLocalization.LoadJson();
AssetDatabase.Refresh();
}
Debug.Log("LanguageId填充成功");
}
/// <summary>
/// 同步文本到Excel表格里
/// </summary>
public void SetTextToExcel(SystemLanguage languageType) {
if (languageType != SystemLanguage.Chinese && languageType != SystemLanguage.English)
{
return;
}
var file = new FileInfo(Application.dataPath + languageExcelPath);
using (ExcelPackage package = new ExcelPackage(file))
{
var worksheet = package.Workbook.Worksheets["Sheet1"];
if (worksheet == null)
{
Debug.Log($"没有找到路径为{languageExcelPath}的工作表");
}
else
{
var endRow = worksheet.Dimension.End.Row;
for(var i = 4; i <= endRow; i++)
{
var id = int.Parse(worksheet.Cells[i, 2].Value.ToString().Trim());
if (id == languageText.LanguageId)
{
switch (languageType) {
case SystemLanguage.Chinese:
worksheet.Cells[i, 3].Value = textComponent.text;
break;
case SystemLanguage.English:
worksheet.Cells[i, 4].Value = textComponent.text;
break;
default:
break;
}
break;
}
}
}
package.Save();
}
//重新生成CSharp和JsonData
ExcelToCSharpClass.ExcelToCSharpClassMethod(Application.dataPath + languageExcelPath);
//刷新
languageDict = LanguageLocalization.LoadJson();
AssetDatabase.Refresh();
}
/// <summary>
/// 去换行符影响,因为Excel里的换行符可能是\r\n的形式
/// </summary>
private bool IsTwoStrsSame(string str1, string str2) {
str1 = str1.Replace("\r\n", "\n");
str2 = str2.Replace("\r\n", "\n");
return str1 == str2;
}
}
3.2 自动LanguageText挂载
每次在Text下去挂一个组件有点太麻烦了,需要一个可以遍历选择的Prefab然后自动去挂载的方法。
代码暂略。
4* 内嵌文本图片的本地化
在尽量分离项目里的文本和图片之后,对于剩余的还有内嵌文本的图片(一般都是为了实现美术效果),我采用的方式是:让美术切出不同语言环境下要用的图片,然后以不同后缀,如_cn,_en放在项目里面,然后写一个组件挂在Image下,实现的效果类似:如果当前使用的图片后缀含_cn,则替换为对应语言环境下的图片。
挂载组件也可以参考3.2所说的,写一个遍历挂载的方法。