使用Unity Localization插件进行项目本地化实战详解

在使用Unity开发游戏的过程中,本地化是必不可少的。网络上也有很多的本地化工具,本次我介绍的是Unity官方提供的Localization插件,大家可以在Package Manager进行安装

 一、语言配置,本地化表创建

在Project Setting中找到Localization,(需要先创建这个Localization Setting文件)点击Locale Generator选择需要本地化的语言。创建好后会得到这些文件,这些文件可以用于切换语言(后面做切换语言界面时会用),先把英语拖入作为默认语言。

 打开本地化表工具,创建本地化表

 创建UILocalization和ScriptLocalization两个本地化表,分别用作UI和代码本地化。会得到以下文件

 可以在表格中添加一些需要本地化的文本(这里只是先试试,先不要添加太多,后面会用Excel进行管理),注意最左侧的Key值,这个key值后续用来确定文本

 

二、使用Localize String Event进行本地化

 给UI上的文本挂载LocalizeStringEvent脚本,点击其中最上面的String Reference搜索之前表格中创建的本地化文本。搜索Key值或者本地化的文本都可以搜得到。

注:如果显示不完整,可以调节右下角的小球

然后将LocalizeStringEvent的Update String调整为Text.text,就是要刷新的脚本。任何的string都可以刷新,Text Mesh Pro也是可以的

 运行游戏查看效果,可以临时先使用右上角的下拉框调节本地化语言,界面上的文本会实时刷新。

 

三、UI本地化

 我先将本地化的需求暂时分成两类

1,UI本地化:UI界面上面固定不变的文本

2,Script本地化:代码中实时更改的文本,包括(String.Format+数值),动态弹出的提示语,NPC对话,物品名字等

为了后续教程,我们暂时约定在制作界面时,UI本地化的内容正常命名,Script本地化的对象以$开头命名,例如:

首先,需要知道有哪些文本需要本地化,并给它们自动挂载上LocalizeStringEvent脚本。可以在Editor目录下创建一个扩展脚本实现此功能。

using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Localization;
using UnityEngine.Localization.Components;
using UnityEngine.UI;

public class LocalizeTextEditor : EditorWindow
{
    private string outputFilePath = "LocalizedText.txt"; // 指定的txt文件路径
    private List<string> localizedTextEntries = new List<string>();

    [MenuItem("Custom/Localize Text")]
    private static void ShowWindow()
    {
        GetWindow<LocalizeTextEditor>("Localize Text");
    }

    private void OnGUI()
    {
        GUILayout.Label("Localize Text Editor", EditorStyles.boldLabel);

        if (GUILayout.Button("Localize Selected GameObjects"))
        {
            LocalizeSelectedGameObjects();
        }

        if (GUILayout.Button("Save Localized Text to File"))
        {
            SaveLocalizedTextToFile();
        }

        if (GUILayout.Button("Print Script Localize Selected GameObjects"))
        {
            PrintScriptLocalize();
        }
    }

    private void LocalizeSelectedGameObjects()
    {
        localizedTextEntries.Clear();

        GameObject[] selectedObjects = Selection.gameObjects;
        foreach (GameObject selectedObject in selectedObjects)
        {
            Text[] textComponents = selectedObject.GetComponentsInChildren<Text>(true);
            foreach (Text textComponent in textComponents)
            {
                if (!textComponent.name.StartsWith("$"))
                {
                    // 需要本地化的Text
                    if(textComponent.gameObject.GetComponent<LocalizeStringEvent>() == null)
                    {
                        LocalizeStringEvent localizeEvent = textComponent.gameObject.AddComponent<LocalizeStringEvent>();
                        // 标记对象为“已修改”
                        EditorUtility.SetDirty(selectedObject);
                    }
                    // 添加到列表
                    string entry = $"{selectedObject.name}\t{textComponent.name}\t{textComponent.text}";
                    localizedTextEntries.Add(entry);
                }
            }
        }
    }

    private void SaveLocalizedTextToFile()
    {
        if(!File.Exists(outputFilePath))
        {
            File.Create(outputFilePath).Dispose();
        }
        using (StreamWriter writer = new StreamWriter(outputFilePath, true))
        {
            foreach (string entry in localizedTextEntries)
            {
                writer.WriteLine(entry);
            }
        }

        Debug.Log($"Localized text entries saved to {outputFilePath}");
    }

    private void PrintScriptLocalize()
    {
        GameObject[] selectedObjects = Selection.gameObjects;
        foreach (GameObject selectedObject in selectedObjects)
        {
            Text[] textComponents = selectedObject.GetComponentsInChildren<Text>(true);
            foreach (Text textComponent in textComponents)
            {
                if (textComponent.name.StartsWith("$"))
                {
                    Debug.Log($"{selectedObject.name}\t{textComponent.name}\t{textComponent.text}");
                }
            }
        }
    }
}

我本来还想自动给LocalizeStringEvent的Update String赋值,但是未能实现。如果哪位高人有办法可以在评论区指出

 全选需要本地化的UI预制体,依次点击扩展窗口的第1和第2个按钮,可以给非$开头的Text(你们如果命名规则不一样,请自行修改脚本)自动挂载LocalizeStringEvent脚本

这些Text中的内容会输出到txt中,可以查看(注:txt默认应该在项目根路径)

 

 四、使用Excel表格管理本地化文本

 右键点击本地化表的选项卡,选择导出CSV

 如果使用Office,导出时选择UTF-8即可。如果和我一样使用WPS,请另存为xlsx格式,不然部分语言会乱码。

将之前txt中的内容复制到表格中,由于我输出的是\t,内容会自动分布到表格的不同列,大家可以把根据预制体名字来给Key起名。

丢给AI或者翻译软件翻译之后,对于没有空格的语言,可以使用alt+回车在适当位置输入换行,例如:中文,日语等。Unity的自适应换行只会在有空格的地方换行

对于WPS用户,我们需要将xlsx转回CSV才能在Unity中读取,这里使用python的pandas库进行处理。

import pandas as pd
 
# 读取 Excel 文件
df = pd.read_excel("UILocalization.xlsx", sheet_name="UILocalization")
 
# 将 DataFrame 写入 CSV 文件
df.to_csv('dataUI.csv', encoding='utf-8', index=False)

df2 = pd.read_excel("ScriptLocalization.xlsx", sheet_name="ScriptLocalization")
df2.to_csv('dataScript.csv', encoding='utf-8', index=False)

这里还处理了后面的Script本地化文件,大家还没看完后续教程的,可以先注释后两行代码

在本地化表导出CSV的上面还有导入CSV,这里就不截图了,导入之后就可以得到我们刚才在Office或WPS中编辑的表格

 

五、Script本地化

还是全选所有UI预制体,这次点击第三个按钮,会在控制台中输出所有$开头的Text。这些Text都是在代码中更改内容的,只能对其查找所有引用,然后逐个更改。

那么,如何在代码中获取本地化的内容呢,我们需要创建一个单例的LocalizationManager,提供一个获取本地化内容的函数。

 以防止有编程小白,这里介绍一下单例模式,单例模式就是类最多只有一个对象,并且有一个指向该对象的静态指针。在Unity中可以用以下代码实现一个简单的单例模式

using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : Singleton<T> {
    protected static T _instance;

    public static T Ins {
        get {
            return _instance;
        }
    }

    protected virtual void Awake() {
        _instance = (T)this;
    }

    protected virtual void OnDestroy() {
        if (_instance == this) {
            _instance = null;
        }
    }
}

在场景中创建一个(DontDestroyOnLoad)不会销毁的对象,并挂载LocalizationManager脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
using UnityEngine.ResourceManagement.AsyncOperations;

namespace UI
{
    public class LocalizationManager : Singleton<LocalizationManager>
    {
        private StringTable ScriptStringTable;          //代码本地化表

        void Start()
        {
            if (LocalizationSettings.AvailableLocales.Locales.Count > 0)
            {
                GetLocalizationTable();
            }
            else
            {
                LocalizationSettings.InitializationOperation.Completed += OnLocalizationInitialized;
            }
            LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;
        }

        /// <summary>
        /// 本地化初始化完成
        /// </summary>
        private void OnLocalizationInitialized(AsyncOperationHandle<LocalizationSettings> handle)
        {
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                Debug.Log("Localization initialized successfully!");
                GetLocalizationTable();
            }
            else
            {
                Debug.LogError("Localization initialization failed.");
            }
        }

        /// <summary>
        /// 切换语言
        /// </summary>
        private void OnLocaleChanged(Locale newLocale)
        {
            GetLocalizationTable();
        }

        /// <summary>
        /// 获取本地化表
        /// </summary>
        public void GetLocalizationTable()
        {
            ScriptStringTable = LocalizationSettings.StringDatabase.GetTable("ScriptLocalization");
            //Debug.LogWarning(ScriptStringTable.GetEntry("CommonTip_NoItem").GetLocalizedString());
        }

        /// <summary>
        /// 获取本地化文本
        /// </summary>
        public string GetLocalizedString(string key)
        {
            return ScriptStringTable.GetEntry(key).GetLocalizedString();
        }
    }
}

这里我们注册了两个事件,一个是LocalizationSettings.InitializationOperation.Completed,这个是LocalizationSettings初始化完成时调用。由于本地化插件是异步初始化,代码运行到start时不一定初始化完成,此处通过判断可用语言是否大于0来判断有没有初始化完成。

另一个事件是LocalizationSettings.SelectedLocaleChanged,这个是LocalizationSettings切换语言是调用。这两个事件都会执行获取本地化表的操作。对于之前在代码中更改的Text,可用调用GetLocalizedString来获取本地化文本。

例如:

物品名字要进行本地化,key值是GameItem_ID,每个物品ID不同

和之前使用表格管理UILocalization表类似,我们也使用表格管理ScriptLocalization表。

 

六、语言切换界面

创建一个这样的UI界面,我设计的是每个按钮都能点。当前使用的语言是绿的。

给每个按钮拖上一个Locale,就是之前最开始创建的用于切换语言的

        /// <summary>
        /// 刷新按钮状态
        /// </summary>
        public void RefreshItemChooseState()
        {
            Locale currentLocale = LocalizationSettings.SelectedLocale;
            foreach (var item in languageItems)
            {
                item.SetChooseState(currentLocale == item.locale);
            }
        }

        /// <summary>
        /// 设置语言
        /// </summary>
        public void SetLanguage(Locale locale)
        {
            if(locale == LocalizationSettings.SelectedLocale)
            {
                return;
            }
            LocalizationSettings.Instance.SetSelectedLocale(locale);
            Client.Ins.Player.PlayerLanguage = locale.LocaleName;
            RefreshItemChooseState();
        }

获取当前语言:LocalizationSettings.SelectedLocale,获取之后和每个按钮上面拖入的Locale比对,一样的就是当前语言

设置语言:LocalizationSettings.Instance.SetSelectedLocale(locale);把拖入的Locale设置进去,就可以切换语言。

 

以下为拓展内容:

设置完语言之后,最好能够保存下来,用户下次启动自动使用。这里把locale.LocaleName,也就是语言的名字保存下来。

大家可以用任意方式保存一个string,我这边用的json,这里就涉及到游戏存档设计了,超出了本文的讨论范围。大家可以自行设计游戏存档,反正能够存取当前语言名字(public string PlayerLanguage)就行了

 这里仍然使用了之前提到的LocalizationSettings.InitializationOperation.Completed事件,在LocalizationSettings初始化完成之后设置为保存的语言。

        /// <summary>
        /// 本地化初始化完成
        /// </summary>
        private void OnLocalizationInitialized(AsyncOperationHandle<LocalizationSettings> handle)
        {
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                Debug.Log("Localization initialized successfully!");
                UseSaveLanguage();
            }
            else
            {
                Debug.LogError("Localization initialization failed.");
            }
        }

        /// <summary>
        /// 使用保存的语言
        /// </summary>
        private void UseSaveLanguage()
        {
            var locales = LocalizationSettings.AvailableLocales.Locales;
            foreach (var locale in locales)
            {
                if (locale.LocaleName == PlayerLanguage)
                {
                    LocalizationSettings.Instance.SetSelectedLocale(locale);
                    break;
                }
            }
        }

 

热门相关:惊世第一妃   全系灵师:魔帝嗜宠兽神妃   百炼成仙   傲娇总裁:蜜宠小甜妻!   仙碎虚空