Как стать автором
Поиск
Написать публикацию
Обновить

Локализация любых текстов

Время на прочтение11 мин
Количество просмотров4.1K
Большинство игр, используют локализацию по принципу ключа, то есть для описания конкретного текста нужен ключ, я же предлагаю вариант получше, хоть и этот вариант не подходит тем у кого есть озвучка в играх, тут уж проще через ключ.

Что такое ключ и зачем он нужен


Ключ, а точнее ключевое слово — это слово по которому будет определено какой именно текст нужно, а потом уже идет поиск по выбранному языку. Пример ключевого слова: scene_Escape_from_jail_Ethan_dialog_with_Mary_3, да, примерно такого вида будет ключ, если ваша игра будет иметь много сцен, большой сюжет. Я же предлагаю прямо сразу писать фразу на одном из языков, чаще всего английский или тот которым свободно владеет программист. Кстати, поскольку все фразы текущего языка и основного языка будут лежать в оперативке, это будет более производительная чем доставать каждый раз из файла, для больших же игр, можно немного модифицировать файл под каждую сцену.

Как же будет все устроено


В ниже описанных действия будет использован класс статический Lang, в котором и будут происходить все поиски слов/фраз. Давайте же объявим класс, подключив нужные нам библиотеки:

using UnityEngine;
using System.Collections.Generic; // list and dictionary
#if UNITY_EDITOR
using UnityEditor;
using System.IO; // created file in editor
#endif

public class Lang {
    private const string Path = "/Resources/"; // path to resources folder
    private const string FileName = "Language"; // file name with phrases
}

И так, без стандартной библиотеки не обойтись, так как мы будем доставать файл из ресурсов, можно же загружать любым удобным способом, но так удобнее и практичнее. Системные библиотеки нужны для подключения списка, словаря и работы с файлами из редактора. Библиотека UnityEditor нужна только для обновления файла во время первой записи фразы, т.к. после быстрого перезапуска не всегда будут загружены все фразы, но с помощью данной библиотеки мы сможем решить данную проблему. Класс хранит два статических поля, это пусть и названия файла, в нашем же случае путь — это папка с ресурсами, а названия файла может быть любое.

Теперь нужно добавить списки для хранения все используемых языком и словарь.

   
    private static int LangIndex; // variable to store the index of the current language 
    private static List<SystemLanguage> languages = new List<SystemLanguage>(); // having languages in game
    private static Dictionary<string, string> Phrases = new Dictionary<string, string>(); // keys and values

Поле LangIndex будет держать индекс текущего языка относительно записи в файле. В списке languages — будут записаны все языки используемые в файле. Словарь же будет хранить все фразы на основном языке и на текущем языке.

Нужно добавить инициализацию выше описанных полей класса.

Код

    public static bool isStarting // bool for check starting
    {
        get; private set;
    }

    public static SystemLanguage language // return current language
    {
        get; private set;
    }

#if UNITY_EDITOR
    public static void Starting(SystemLanguage _language, SystemLanguage default_language = SystemLanguage.English, params SystemLanguage[] _languages) // write languages without main language, it self added
#else
    public static void Starting(SystemLanguage _language = SystemLanguage.English) // main language - only for compilation
#endif
    {
#if UNITY_EDITOR
        if (!File.Exists(Application.dataPath + Path + FileName + ".csv")) // if file wasn't created
        {
            File.Create(Application.dataPath + "/Resources/" + FileName + ".csv").Dispose(); // create and lose link
            File.WriteAllText(Application.dataPath + "/Resources/" + FileName + ".csv", SetLanguage(default_language, _languages)); // write default text with index
        }
#endif
        string[] PhrasesArr = Resources.Load<TextAsset>(FileName).text.Split('\n'); // temp var for write in dicrionary

        string[] string_languages = PhrasesArr[0].Split(';'); // string with using languages
        int _length = string_languages.Length - 1;
        for (int i = 0; i < _length; i++)
        {
            languages.Add(SystemLanguageParse(string_languages[i])); // string language to SystemLanguage
        }

        LangIndex = FindIndexLanguage(_language); // index with current language
        for (int i = 0; i < PhrasesArr.Length; i++) // add keys and value
        {
            string[] temp_string = PhrasesArr[i].Split(';');
            if (temp_string.Length > LangIndex)
                Phrases.Add(temp_string[0], temp_string[LangIndex]);
            else Phrases.Add(temp_string[0], temp_string[0]);
        }
        isStarting = true;
    }

Будет сразу использовать встроенный директивы да бы не делать лишних действий после компиляции приложения. Вызов Lang.Starting(...) должен происходит примерно таким образом:

#if !UNITY_EDITOR
        Lang.Starting(LANGUAGE);
#else
        Lang.Starting(LANGUAGE, SystemLanguage.English, SystemLanguage.Russian, SystemLanguage.Ukrainian);
#endif
    private static int FindIndexLanguage(SystemLanguage _language) // finding index or current language
    {
        int _index = languages.IndexOf(_language); 
        if (_index == -1) // if language not found
            return 0; // return main language
        return _index;
    }

#if UNITY_EDITOR
    private static void Add(string AddString) // add phrases only form editor
    {
        File.AppendAllText(Application.dataPath + "/Resources/" + FileName +".csv", AddString + "\n"); // rewrite text to file
        Phrases.Add(AddString, AddString); // add phrase to dicrionary

        AssetDatabase.Refresh(); // refresh file 
    }
#endif

#if UNITY_EDITOR
    private static string SetLanguage(SystemLanguage default_language, params SystemLanguage[] _languages) // set first string to file
    {
        string ret_string = "";
        ret_string += default_language + ";";
        foreach (SystemLanguage _language in _languages)
        {
            ret_string += _language + ";";
        }
        return ret_string + "!@#$%\n"; // for last index
    }
#endif


Вызов при игре в редакторе должен содержать два основных параметра, это на какой язык должны переводиться фразы сейчас, и какой язык будет в качестве основного, все остальные параметры это параметры языков которые должны будут содержаться в файле, нужны эти параметры только во время первого запуска, когда файл еще не создан (и после удалять не обязательно), в ином случае, если нужно будет добавить какой-то язык, нужно скопировать все из файла, удалить файл, и заново запустить код в редакторе или самостоятельно дописать в файле.

В коде, приведенном выше, используется методSystemLanguageParse(...) который просто переводит названия языка из строкового типа в SystemLanguage (данный метод будет ниже).

Остановимся на методе добавления:

#if UNITY_EDITOR
    private static void Add(string AddString) // add phrases only form editor
    {
        File.AppendAllText(Application.dataPath + "/Resources/" + FileName +".csv", AddString + "\n"); // rewrite text to file
        Phrases.Add(AddString, AddString); // add phrase to dicrionary

        AssetDatabase.Refresh(); // refresh file 
    }
#endif

По скольку этот метод будет использоваться только при запуске из редактора, мы спокойно может использовать системную утилиту для перезаписи файла, а так же обновлять измененные файлы редакторе с помощью метода Refresh(). Между этими действиями просто добавляться в словарь фраза, да бы уберечь себя от повторной записи в этой же сесии.

Кстати, забыл сказать, фразы будут храниться в файле .csv, что позволит нам комфортно делать переводы фраз в Excele. Теперь нужно добавить хороший для нас метод, который позволит сменить язык:

    public static void ChangeLanguage(SystemLanguage _language) // change language
    {
        string[] PhrasesArr = Resources.Load<TextAsset>(FileName).text.Split('\n'); // load all text from file

        LangIndex = FindIndexLanguage(_language);
        Phrases.Clear(); // clear dictionary with phrases
        for (int i = 1; i < PhrasesArr.Length; i++)
        {
            string[] temp_string = PhrasesArr[i].Split(';');
            if (temp_string.Length > LangIndex)
                Phrases.Add(temp_string[0], temp_string[LangIndex]);
            else Phrases.Add(temp_string[0], temp_string[0]);
        }
    }

И так, подошли к самому главному методу, который и будет принимать фразу на основном языке, и выдавать на нужном пользователю:

    public static string Phrase(string DefaultPhrase) // translate phrase, args use to formating string
	{
#if UNITY_EDITOR
        if (!isStarting) // if not starting
        {
            throw new System.Exception("Forgot initialization.Use Lang.Starting(...)"); // throw exception
        }
#endif
        string temp_EnglishPhrase = DefaultPhrase; // temp variable for try get value

        if (Phrases.TryGetValue(DefaultPhrase, out DefaultPhrase)) // if value has been found
        {
            return temp_EnglishPhrase;
        }
#if UNITY_EDITOR
        Add(temp_EnglishPhrase); // add phrase if value hasn't been found
#endif
            return temp_EnglishPhrase;
    }

Этот метод просто принимает фразу на основном языке, потом перебирает все что есть в словаре, находя такую фразу по ключу он выдает нам значения этого ключа, что является фразой на нужном нам языке. Использовать этот метод можно простой строкой кода:

string str = Lang.Phrase("Hello world");

Теперь в строку str попадет фраза на нужном нам языке, если вдруг он она отсутствует то попадет фраза указанная в параметрах, то есть Hello world.

Этот метод можно немного улучшить, да бы можно было принимать аргументы для заполнения:

    public static string Phrase(string DefaultPhrase, params string[] args) // translate phrase, args use to formating string
	{
#if UNITY_EDITOR
        if (!isStarting) // if not starting
        {
            throw new System.Exception("Forgot initialization.Use Lang.Starting(...)"); // throw exception
        }
#endif
        string temp_EnglishPhrase = DefaultPhrase; // temp variable for try get value

        if (Phrases.TryGetValue(DefaultPhrase, out DefaultPhrase)) // if value has been found
        {
            if (args.Length == 0)
                return DefaultPhrase;
            return string.Format(DefaultPhrase, args);
        }
#if UNITY_EDITOR
        Add(temp_EnglishPhrase); // add phrase if value hasn't been found
#endif
        if (args.Length == 0)
            return temp_EnglishPhrase;
        return string.Format(temp_EnglishPhrase, args);
    }

Теперь этот метод можно вызвать так же как и раньше:

string str = Lang.Phrase("Hello world");

Но теперь у нашого метода есть форматированный вывод, указав через запятую параметры:

string str = Lang.Phrase("Hello {0} from {1}", "world", "habr");

Переводы фраз


Как я уже писал выше, файл использует разширения .csv что позволит делать все в екселе, но не все так просто, проблема си-шарпа и екселя в том, что они понимают кириллицу в разных кодировка, ексель понимает только кодировку UTF-8-BOM или же ту, которую не понимает наш ЯП, мы же должны использовать в нем только UTF-8, хоть юнитовский редактор будет понимать UTF-8-BOM, в коде же два одинаковых слова на разных кодировках (UTF-8 и UTF-8-BOM) будут не равны, что приведет к постоянному добавлению одинаковых слов в наш файл.

Кодировать файлы мы можем с помощью бесплатного NotePad++ скачав его с офф. сайта. Редактирования файла не будет приносить вам никаких проблема, для добавления одного слова можно воспользовать даже текстовым редактором, тем же нот-падом или даже нашей средой программирования.

Итоговый код
using UnityEngine;
using System.Collections.Generic; // list and dictionary
#if UNITY_EDITOR
using UnityEditor;
using System.IO; // created file in editor
#endif

public class Lang
{

    private const string Path = "/Resources/"; // path to resources folder
    private const string FileName = "Language"; // file name with phrases

    private static int NumberOfLanguage; // variable to store the index of the current language 
    private static List<SystemLanguage> languages = new List<SystemLanguage>(); // having languages in game
    private static Dictionary<string, string> Phrases = new Dictionary<string, string>(); // keys and values
    private static SystemLanguage language; // current language
#if UNITY_EDITOR
    public static void Starting(SystemLanguage _language, SystemLanguage default_language, params SystemLanguage[] _languages) // write languages without main language, it self added
#else
    public static void Starting(SystemLanguage _language = SystemLanguage.English) // main language - only for compilation
#endif
    {
#if UNITY_EDITOR
        if (!File.Exists(Application.dataPath + Path + FileName + ".csv")) // if file wasn't created
        {
            File.Create(Application.dataPath + "/Resources/" + FileName + ".csv").Dispose(); // create and lose link
            File.WriteAllText(Application.dataPath + "/Resources/" + FileName + ".csv", SetLanguage(default_language, _languages)); // write default text with index
        }
#endif
        string[] PhrasesArr = Resources.Load<TextAsset>(FileName).text.Split('\n'); // temp var for write in dicrionary

        string[] string_languages = PhrasesArr[0].Split(';'); // string with using languages
        int _length = string_languages.Length - 1;
        for (int i = 0; i < _length; i++)
        {
            languages.Add(SystemLanguageParse(string_languages[i])); // string language to SystemLanguage
        }

        NumberOfLanguage = FindIndexLanguage(_language); // index with current language
        for (int i = 0; i < PhrasesArr.Length; i++) // add keys and value
        {
            string[] temp_string = PhrasesArr[i].Split(';');
            if (temp_string.Length > NumberOfLanguage)
                Phrases.Add(temp_string[0], temp_string[NumberOfLanguage]);
            else Phrases.Add(temp_string[0], temp_string[0]);
        }
        isStarting = true;
    }

    public static bool isStarting // bool for check starting
    {
        get; private set;
    }

    public static SystemLanguage Language // return current language
    {
        get { return language; }
    }

    public static string Phrase(string DefaultPhrase, params string[] args) // translate phrase, args use to formating string
    {
#if UNITY_EDITOR
        if (!isStarting) // if not starting
        {
            throw new System.Exception("Forgot initialization.Use Lang.Starting(...)"); // throw exception
        }
#endif
        string temp_EnglishPhrase = DefaultPhrase; // temp variable for try get value

        if (Phrases.TryGetValue(DefaultPhrase, out DefaultPhrase)) // if value has been found
        {
            if (args.Length == 0)
                return DefaultPhrase;
            return string.Format(DefaultPhrase, args);
        }
#if UNITY_EDITOR
        Add(temp_EnglishPhrase); // add phrase if value hasn't been found
#endif
        if (args.Length == 0)
            return temp_EnglishPhrase;
        return string.Format(temp_EnglishPhrase, args);
    }

    public static void ChangeLanguage(SystemLanguage _language) // change language
    {
        string[] PhrasesArr = Resources.Load<TextAsset>(FileName).text.Split('\n'); // load all text from file

        NumberOfLanguage = FindIndexLanguage(_language);
        Phrases.Clear(); // clear dictionary with phrases
        for (int i = 1; i < PhrasesArr.Length; i++)
        {
            string[] temp_string = PhrasesArr[i].Split(';');
            if (temp_string.Length > NumberOfLanguage)
                Phrases.Add(temp_string[0], temp_string[NumberOfLanguage]);
            else Phrases.Add(temp_string[0], temp_string[0]);
        }
    }

    private static int FindIndexLanguage(SystemLanguage _language) // finding index or current language
    {
        int _index = languages.IndexOf(_language);
        if (_index == -1) // if language not found
            return 0; // return main language
        return _index;
    }

#if UNITY_EDITOR
    private static void Add(string AddString) // add phrases only form editor
    {
        File.AppendAllText(Application.dataPath + "/Resources/" + FileName + ".csv", AddString + "\n"); // rewrite text to file
        Phrases.Add(AddString, AddString); // add phrase to dicrionary

        AssetDatabase.Refresh(); // refresh file 
    }
#endif

#if UNITY_EDITOR
    private static string SetLanguage(SystemLanguage default_language, params SystemLanguage[] _languages) // set first string to file
    {
        string ret_string = "";
        ret_string += default_language + ";";
        foreach (SystemLanguage _language in _languages)
        {
            ret_string += _language + ";";
        }
        return ret_string + "!@#$%\n"; // for last index
    }
#endif
    private static SystemLanguage SystemLanguageParse(string _language) // just parse from string to SystemLanguage
    {
        switch (_language)
        {
            case "English": return SystemLanguage.English;
            case "Russian": return SystemLanguage.Russian;
            case "Ukrainian": return SystemLanguage.Ukrainian;
            case "Polish": return SystemLanguage.Polish;
            case "French": return SystemLanguage.French;
            case "Japanese": return SystemLanguage.Japanese;
            case "Chinese": return SystemLanguage.Chinese;
            case "Afrikaans": return SystemLanguage.Afrikaans;
            case "Arabic": return SystemLanguage.Arabic;
            case "Basque": return SystemLanguage.Basque;
            case "Belarusian": return SystemLanguage.Belarusian;
            case "Bulgarian": return SystemLanguage.Bulgarian;
            case "ChineseSimplified": return SystemLanguage.ChineseSimplified;
            case "ChineseTraditional": return SystemLanguage.ChineseTraditional;
            case "Czech": return SystemLanguage.Czech;
            case "Danish": return SystemLanguage.Danish;
            case "Dutch": return SystemLanguage.Dutch;
            case "Estonian": return SystemLanguage.Estonian;
            case "Faroese": return SystemLanguage.Faroese;
            case "Finnish": return SystemLanguage.Finnish;
            case "German": return SystemLanguage.German;
            case "Greek": return SystemLanguage.Greek;
            case "Hebrew": return SystemLanguage.Hebrew;
            case "Hungarian": return SystemLanguage.Hungarian;
            case "Icelandic": return SystemLanguage.Icelandic;
            case "Indonesian": return SystemLanguage.Indonesian;
            case "Italian": return SystemLanguage.Italian;
            case "Korean": return SystemLanguage.Korean;
            case "Latvian": return SystemLanguage.Latvian;
            case "Lithuanian": return SystemLanguage.Lithuanian;
            case "Norwegian": return SystemLanguage.Norwegian;
            case "Portuguese": return SystemLanguage.Portuguese;
            case "Romanian": return SystemLanguage.Romanian;
            case "SerboCroatian": return SystemLanguage.SerboCroatian;
            case "Slovak": return SystemLanguage.Slovak;
            case "Slovenian": return SystemLanguage.Slovenian;
            case "Spanish": return SystemLanguage.Spanish;
            case "Swedish": return SystemLanguage.Swedish;
            case "Thai": return SystemLanguage.Thai;
            case "Turkish": return SystemLanguage.Turkish;
            case "Vietnamese": return SystemLanguage.Vietnamese;
        }
        return SystemLanguage.Unknown;
    }
}

Главное запомните: UTF-8-BOM — для работы в Excel, UTF-8 для работы кодом, не забудьте.
Теги:
Хабы:
Всего голосов 11: ↑2 и ↓9-7
Комментарии19

Публикации

Ближайшие события