Локализация
OpenMon поставляется с собственной системой локализации на базе Google Sheets. Переводы загружаются в виде TSV-файлов, парсятся в ассеты ScriptableLanguage и обслуживаются во время выполнения через класс Localizer. Система находится в OpenMon.Foundation.Localization с игровыми постпроцессорами в OpenMon.MCE.Localization.PostProcessors.
Обзор архитектуры
| Класс | Назначение |
|---|---|
Localizer | Основной сервис времени выполнения. Загружает языковые пакеты, разрешает ключи, применяет постпроцессоры |
ILocalizer | Интерфейс для локализатора, внедряемый через Zenject |
ScriptableLanguage | ScriptableObject, содержащий все пары ключ-значение для одного языка |
LocalizerSettings | Конфигурация разработчика: каталог языковых пакетов, URL Google Sheets, постпроцессоры |
LocalizerConfiguration | Конфигурация игрока: сохранённый выбранный язык (по умолчанию: "English") |
GoogleSheetLoader | Инструмент редактора для загрузки таблиц и генерации ассетов ScriptableLanguage |
LocalizedTextMeshPro | UI-компонент, автоматически обновляющий TMP_Text при смене языка |
LocalizationFontSwitcher | Переключает шрифты TMP по языку (для поддержки CJK) |
LanguageFontMap | ScriptableObject, сопоставляющий названия языков с переопределениями TMP_FontAsset |
LocalizedTextPostProcessor | Абстрактная база для постобработки разрешённого текста |
ReplaceSubstring | Встроенный постпроцессор: простой поиск и замена |
InjectPlayerName | Постпроцессор MCE: заменяет {PlayerName} на имя персонажа игрока |
InjectDexValues | Постпроцессор MCE: заменяет {Dex.CaughtSpecies} на количество пойманных |
Интеграция с Google Sheets
Переводы создаются в Google Sheets со следующей структурой:
| Key | English | Spanish | Japanese |
|---|---|---|---|
Menu/Play | Play | Jugar | ... |
Menu/Options | Options | Opciones | ... |
Monsters/001/Name | Sproutyl | Brotino | ... |
Каждый столбец после столбца ключа представляет язык. Заголовок столбца становится именем ассета ScriptableLanguage.
Импорт в редакторе
Утилита редактора GoogleSheetLoader загружает каждый URL таблицы из LocalizerSettings.GoogleSheetsDownloadUrls, преобразует его в формат экспорта TSV и парсит строки в ассеты ScriptableLanguage, сохраняемые в Assets/Resources/{LanguagePackDirectory}/.
Процесс:
- Для каждого URL преобразовать URL
/editGoogle Sheets в/export?format=tsv - Загрузить TSV-файл в
Temp/ - Разобрать заголовки для определения названий языков
- Создать или обновить ассет
ScriptableLanguageдля каждого языка - Заполнить
Language[key] = valueдля каждой строки - Сохранить ассеты и обновить AssetDatabase
Поддерживается несколько таблиц. Если две таблицы определяют один и тот же ключ, побеждает более поздний URL (нижние URL в списке переопределяют верхние).
Повторная загрузка во время выполнения
Localizer может повторно загрузить переводы во время выполнения через ReDownloadLanguagesFromGoogleSheet(). Это создаёт временные экземпляры ScriptableLanguage в памяти (не сохраняя на диск) и перезагружает языковую систему. Полезно для тестирования переводов без повторного импорта в редакторе.
ScriptableLanguage
Каждый язык — это ScriptableLanguage ScriptableObject, содержащий SerializableDictionary<string, string> всех пар ключ-значение. Ассет хранится в Resources/Languages/ (настраивается через LocalizerSettings.LanguagePackDirectory).
// ScriptableLanguage разрешает ключи с запасным вариантом — исходным ключом
string value = englishPack["Menu/Play"]; // "Play"
string missing = englishPack["Nonexistent"]; // "Nonexistent" (исходный ключ как запасной вариант)
Когда ключ не найден, ассет возвращает сам ключ. Это делает отсутствующие переводы немедленно види мыми в игре в виде исходных строк ключей.
Localizer
Localizer — основной сервис времени выполнения, связанный как ILocalizer через Zenject. Он загружает все ассеты ScriptableLanguage из каталога Resources при инициализации.
Разрешение текста
// Синтаксис индексатора (наиболее распространённый)
string text = localizer["Menu/Play"]; // "Play"
// С параметрами подстановки
// Если значение "You caught {0}!" и "Monsters/025/Name" разрешается в "Thundermane"
string text = localizer["Messages/Caught", true, "Monsters/025/Name"];
// Результат: "You caught Thundermane!"
// Параметры как литеральные строки (не ключи локализации)
string text = localizer.GetText("Messages/Caught", false, "Thundermane");
Заполнители {0}, {1} и т.д. в значениях перевода заменяются соответствующей строкой-модификатором. Когда modifiersAreLocalizableKeys имеет значение true (по умолчанию), каждый модификатор сначала разрешается через локализатор перед подстановкой.
Переключение языка
// Получить доступные языки
List<string> languages = localizer.GetAllLanguageIds(); // ["English", "Spanish", "Japanese"]
// Получить текущий язык
string current = localizer.GetCurrentLanguage(); // "English"
// Переключить язык (автоматически сохраняется в конфигурации)
localizer.SetLanguage("Spanish");
// Переключить по индексу
localizer.SetLanguage(1);
При смене языка выбор сохраняется через IConfigurationManager, что позволяет пережить перезапуск приложения. Все подписчики на событие смены языка уведомляются.
События смены языка
// Подписаться с названием языка
localizer.SubscribeToLanguageChange((string newLang) =>
{
Debug.Log("Language changed to: " + newLang);
});
// Подписаться без параметра (просто уведомление)
localizer.SubscribeToLanguageChange(() => RefreshUI());
// Отписаться
localizer.UnsubscribeFromLanguageChange(myCallback);
Соглашения о ключах локализации
Ключи следуют иерархическому формату пути:
| Домен | Шаблон | Пример |
|---|---|---|
| UI / Меню | Menu/{Screen}/{Element} | Menu/Battle/Run |
| Монстры | Monsters/{DexNumber}/Name | Monsters/001/Name |
| Описания монстров | Monsters/{DexNumber}/Description | Monsters/001/Description |
| Приёмы | Moves/{MoveName}/Name | Moves/Tackle/Name |
| Предметы | Items/{ItemName}/Name | Items/Potion/Name |
| Квесты | Quests/{QuestName} | Quests/FindTheProfessor |
| Цели квеста | Quests/{QuestName}/Objectives/{N} | Quests/FindTheProfessor/Objectives/0 |
| Диалоги | Dialog/{SceneName}/{NPC}/{Line} | Dialog/Route1/OldMan/0 |