Что такое ключ и зачем он нужен
Ключ, а точнее ключевое слово — это слово по которому будет определено какой именно текст нужно, а потом уже идет поиск по выбранному языку. Пример ключевого слова: 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 для работы кодом, не забудьте.
Комментарии (19)
roman-ilyin
05.04.2019 11:45progamedev.net/localization вот тут мы с Сашей описали основные проблемы локализации. не все, но Ваш подход точно соберет бОльшую часть граблей. :)
lair
05.04.2019 12:26в коде же два одинаковых слова на разных кодировках (UTF-8 и UTF-8-BOM) будут не равны
А, что, как? Что такое "слово в кодировке UTF-8-BOM" в коде C#? Вы правда не сумели заставить .net-производное прочитать файл с BOM?
kitsoRik Автор
05.04.2019 21:40Честно говоря, из-за этого были проблемы, почему-то не всегда считывало, иногда при сравнении слова в BOM и константы выдавало false, на всякий что бы, если бы вдруг не работало, не думали что проблема в коде, а наверное в кодировке.
lair
05.04.2019 21:45Что такое "слово в BOM"? Что вообще такое BOM, по вашему?
kitsoRik Автор
05.04.2019 21:49Для меня это как бы та же кодировка что и UTF8, только без маркера.
lair
05.04.2019 21:52Вы не ответили ни на один из двух вопросов.
kitsoRik Автор
05.04.2019 21:54Формально ответ на второй вопрос был, ответ на первый исходил из второго, «слово в ВОМ» для меня это слово в другой кодировке, а точнее слова, т.к. один файл — одна кодировка, ВОМ — это в вашем подтексте было сокращения UTF8-BOM
lair
05.04.2019 22:03Формально ответ на второй вопрос был
Неа, не было. Давайте еще раз. Что такое BOM?
kitsoRik Автор
06.04.2019 14:29Маркер последовательности байтов или метка порядка байтов (англ. Byte Order Mark, BOM)
lair
06.04.2019 14:31Ура. Что такое "слово в BOM"?
kitsoRik Автор
06.04.2019 15:01Слово имеющее кодировку — UTF8-BOM, т.е. — Маркер последовательности байтов или метка порядка байтов (англ. Byte Order Mark, BOM)
lair
06.04.2019 15:01Что такое "кодировка UTF8-BOM"?
kitsoRik Автор
06.04.2019 15:06Это кодировка в которой четвертый байт содержит информацию о следующим
lair
06.04.2019 15:06Нет такой кодировки. Просто нет. Вообще нет кодировки UTF-8-BOM, есть просто UTF-8.
kitsoRik Автор
06.04.2019 15:19Да, вы будете правы, но черт побери, все же ексель не умеет читать кирилицу в UTF-8, а в UTF-8 BOM умеет
lair
06.04.2019 15:21все же ексель не умеет читать кирилицу в UTF-8, а в UTF-8 BOM умеет
Excel не умеет читать файлы в UTF-8, не имеющие BOM. Ну да, бывает. Это не отменяет того, что файлы имеют одну и ту же кодировку вне зависимости от BOM. И, что важнее, .net умеет читать файлы как с BOM, так и без, а в памяти прочитанные строки всегда представляются одинаково — в UTF-8.
adictive_max
Орден почётного члена нашего велоклуба этому господину.
Это, собственно, самый простой и прямолинейный способ локализации, который только бывает. По сути, тот же словарь, только с оригиналом фразы в качестве ключа. Причём в вашей реализации даже множественный формы не поддерживаются, в отличие от gettext, которому уже 30 лет и который портирован, наверное, даже на brainfuck.
Основной недостаток у такого подхода — переводы отваливаются, если поменяется ключевая фраза на дефолтном языке.
kitsoRik Автор
Да, это самый простой, но какой-то новичек которому нужно будет легкий перевод фраз, не откажеться от столь простого решения.
На счет множественных форм, вполне можно обойтись и аргументами вместо {0},{1]..., самому прописать конечно же.
Если же фраза поменяеться, то можно и поменять в файле с языками.
roscomtheend
А мне понравился лёгкий налёт танцев и песен от создателей "Зиты и Гиты" в виде замены TryParse на switch. Есть в этом какая-то незамутнённость и желание побегать по граблям при изменении enum'а (каюсь, остальной код внимательно не осилил, хватило основной идеи, чтобы сразу пролистать до комментариев).
PS. Глянул ещё — постоянное конструирование строк с чередованием константы с её значением тоже выглядит очень мило.
Для автора
Excel не умеет определять кодировку без маркера, а не читать файл и, скорее, некорректно сохраняет, после чего .net читат не то, что нужно. Проверьте файл нормальным редактором, который умеет смотреть что там записано без попыток применения "интеллекта".