Большинство игр, используют локализацию по принципу ключа, то есть для описания конкретного текста нужен ключ, я же предлагаю вариант получше, хоть и этот вариант не подходит тем у кого есть озвучка в играх, тут уж проще через ключ.
Ключ, а точнее ключевое слово — это слово по которому будет определено какой именно текст нужно, а потом уже идет поиск по выбранному языку. Пример ключевого слова: scene_Escape_from_jail_Ethan_dialog_with_Mary_3, да, примерно такого вида будет ключ, если ваша игра будет иметь много сцен, большой сюжет. Я же предлагаю прямо сразу писать фразу на одном из языков, чаще всего английский или тот которым свободно владеет программист. Кстати, поскольку все фразы текущего языка и основного языка будут лежать в оперативке, это будет более производительная чем доставать каждый раз из файла, для больших же игр, можно немного модифицировать файл под каждую сцену.
В ниже описанных действия будет использован класс статический Lang, в котором и будут происходить все поиски слов/фраз. Давайте же объявим класс, подключив нужные нам библиотеки:
И так, без стандартной библиотеки не обойтись, так как мы будем доставать файл из ресурсов, можно же загружать любым удобным способом, но так удобнее и практичнее. Системные библиотеки нужны для подключения списка, словаря и работы с файлами из редактора. Библиотека UnityEditor нужна только для обновления файла во время первой записи фразы, т.к. после быстрого перезапуска не всегда будут загружены все фразы, но с помощью данной библиотеки мы сможем решить данную проблему. Класс хранит два статических поля, это пусть и названия файла, в нашем же случае путь — это папка с ресурсами, а названия файла может быть любое.
Теперь нужно добавить списки для хранения все используемых языком и словарь.
Поле LangIndex будет держать индекс текущего языка относительно записи в файле. В списке languages — будут записаны все языки используемые в файле. Словарь же будет хранить все фразы на основном языке и на текущем языке.
Нужно добавить инициализацию выше описанных полей класса.
Вызов при игре в редакторе должен содержать два основных параметра, это на какой язык должны переводиться фразы сейчас, и какой язык будет в качестве основного, все остальные параметры это параметры языков которые должны будут содержаться в файле, нужны эти параметры только во время первого запуска, когда файл еще не создан (и после удалять не обязательно), в ином случае, если нужно будет добавить какой-то язык, нужно скопировать все из файла, удалить файл, и заново запустить код в редакторе или самостоятельно дописать в файле.
В коде, приведенном выше, используется методSystemLanguageParse(...) который просто переводит названия языка из строкового типа в SystemLanguage (данный метод будет ниже).
Остановимся на методе добавления:
По скольку этот метод будет использоваться только при запуске из редактора, мы спокойно может использовать системную утилиту для перезаписи файла, а так же обновлять измененные файлы редакторе с помощью метода Refresh(). Между этими действиями просто добавляться в словарь фраза, да бы уберечь себя от повторной записи в этой же сесии.
Кстати, забыл сказать, фразы будут храниться в файле .csv, что позволит нам комфортно делать переводы фраз в Excele. Теперь нужно добавить хороший для нас метод, который позволит сменить язык:
И так, подошли к самому главному методу, который и будет принимать фразу на основном языке, и выдавать на нужном пользователю:
Этот метод просто принимает фразу на основном языке, потом перебирает все что есть в словаре, находя такую фразу по ключу он выдает нам значения этого ключа, что является фразой на нужном нам языке. Использовать этот метод можно простой строкой кода:
Теперь в строку str попадет фраза на нужном нам языке, если вдруг он она отсутствует то попадет фраза указанная в параметрах, то есть Hello world.
Этот метод можно немного улучшить, да бы можно было принимать аргументы для заполнения:
Теперь этот метод можно вызвать так же как и раньше:
Но теперь у нашого метода есть форматированный вывод, указав через запятую параметры:
Как я уже писал выше, файл использует разширения .csv что позволит делать все в екселе, но не все так просто, проблема си-шарпа и екселя в том, что они понимают кириллицу в разных кодировка, ексель понимает только кодировку UTF-8-BOM или же ту, которую не понимает наш ЯП, мы же должны использовать в нем только UTF-8, хоть юнитовский редактор будет понимать UTF-8-BOM, в коде же два одинаковых слова на разных кодировках (UTF-8 и UTF-8-BOM) будут не равны, что приведет к постоянному добавлению одинаковых слов в наш файл.
Кодировать файлы мы можем с помощью бесплатного NotePad++ скачав его с офф. сайта. Редактирования файла не будет приносить вам никаких проблема, для добавления одного слова можно воспользовать даже текстовым редактором, тем же нот-падом или даже нашей средой программирования.
Главное запомните: UTF-8-BOM — для работы в Excel, UTF-8 для работы кодом, не забудьте.
Что такое ключ и зачем он нужен
Ключ, а точнее ключевое слово — это слово по которому будет определено какой именно текст нужно, а потом уже идет поиск по выбранному языку. Пример ключевого слова: 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 — будут записаны все языки используемые в файле. Словарь же будет хранить все фразы на основном языке и на текущем языке.
Нужно добавить инициализацию выше описанных полей класса.
Код
Будет сразу использовать встроенный директивы да бы не делать лишних действий после компиляции приложения. Вызов Lang.Starting(...) должен происходит примерно таким образом:
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 для работы кодом, не забудьте.