Перейти к основному содержимому

Локализация

OpenMon поставляется с собственной системой локализации на базе Google Sheets. Переводы загружаются в виде TSV-файлов, парсятся в ассеты ScriptableLanguage и обслуживаются во время выполнения через класс Localizer. Система находится в OpenMon.Foundation.Localization с игровыми постпроцессорами в OpenMon.MCE.Localization.PostProcessors.

Обзор архитектуры​

КлассНазначение
LocalizerОсновной сервис времени выполнения. Загружает языковые пакеты, разрешает ключи, применяет постпроцессоры
ILocalizerИнтерфейс для локализатора, внедряемый через Zenject
ScriptableLanguageScriptableObject, содержащий все пары ключ-значение для одного языка
LocalizerSettingsКонфигурация разработчика: каталог языковых пакетов, URL Google Sheets, постпроцессоры
LocalizerConfigurationКонфигурация игрока: сохранённый выбранный язык (по умолчанию: "English")
GoogleSheetLoaderИнструмент редактора для загрузки таблиц и генерации ассетов ScriptableLanguage
LocalizedTextMeshProUI-компонент, автоматически обновляющий TMP_Text при смене языка
LocalizationFontSwitcherПереключает шрифты TMP по языку (для поддержки CJK)
LanguageFontMapScriptableObject, сопоставляющий названия языков с переопределениями TMP_FontAsset
LocalizedTextPostProcessorАбстрактная база для постобработки разрешённого текста
ReplaceSubstringВстроенный постпроцессор: простой поиск и замена
InjectPlayerNameПостпроцессор MCE: заменяет {PlayerName} на имя персонажа игрока
InjectDexValuesПостпроцессор MCE: заменяет {Dex.CaughtSpecies} на количество пойманных

Интеграция с Google Sheets​

Переводы создаются в Google Sheets со следующей структурой:

KeyEnglishSpanishJapanese
Menu/PlayPlayJugar...
Menu/OptionsOptionsOpciones...
Monsters/001/NameSproutylBrotino...

Каждый столбец после столбца ключа представляет язык. Заголовок столбца становится именем ассета ScriptableLanguage.

Импорт в редакторе​

Утилита редактора GoogleSheetLoader загружает каждый URL таблицы из LocalizerSettings.GoogleSheetsDownloadUrls, преобразует его в формат экспорта TSV и парсит строки в ассеты ScriptableLanguage, сохраняемые в Assets/Resources/{LanguagePackDirectory}/.

Процесс:

  1. Для каждого URL преобразовать URL /edit Google Sheets в /export?format=tsv
  2. Загрузить TSV-файл в Temp/
  3. Разобрать заголовки для определения названий языков
  4. Создать или обновить ассет ScriptableLanguage для каждого языка
  5. Заполнить Language[key] = value для каждой строки
  6. Сохранить ассеты и обновить 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}/NameMonsters/001/Name
Описания монстровMonsters/{DexNumber}/DescriptionMonsters/001/Description
ПриёмыMoves/{MoveName}/NameMoves/Tackle/Name
ПредметыItems/{ItemName}/NameItems/Potion/Name
КвестыQuests/{QuestName}Quests/FindTheProfessor
Цели квестаQuests/{QuestName}/Objectives/{N}Quests/FindTheProfessor/Objectives/0
ДиалогиDialog/{SceneName}/{NPC}/{Line}Dialog/Route1/OldMan/0

Постпроцессоры текста​

После разрешения ключа и подстановки модификаторов текст проходит через конвейер экземпляров LocalizedTextPostProcessor, настроенных в LocalizerSettings.TextPostProcessors. Каждый процессор может изменить строку на месте.

Встроенный: ReplaceSubstring​

Простой процессор поиска и замены. Создаётся через OpenMon > Localization > TextPostProcessors > ReplaceSubstring. Настройте поля Original и Replacement в инспекторе. Полезен для глобальных замен, например замены \n на фактические переносы строк.

MCE: InjectPlayerName​

Заменяет {PlayerName} в любой локализованной строке на LocalizableName персонажа игрока. Создаётся через MCE > Localization > TextPostProcessors > InjectPlayerName.

// Значение перевода: "Welcome, {PlayerName}! Ready to explore?"
// После постобработки: "Welcome, Red! Ready to explore?"

MCE: InjectDexValues​

Заменяет {Dex.CaughtSpecies} на количество видов, пойманных игроком. Создаётся через MCE > Localization > TextPostProcessors > InjectDexValues.

Пользовательские постпроцессоры​

Расширьте LocalizedTextPostProcessor для создания собственного:

[CreateAssetMenu(menuName = "MyGame/Localization/MyPostProcessor")]
public class MyPostProcessor : LocalizedTextPostProcessor
{
public override bool PostProcessText(ref string text, params object[] extraParams)
{
text = text.Replace("{MyToken}", GetMyValue());
return true;
}
}

Добавьте ассет в LocalizerSettings.TextPostProcessors, чтобы он был включён в конвейер.

UI: LocalizedTextMeshPro​

Компонент LocalizedTextMeshPro связывает TMP_Text с ключом локализации. Он автоматически обновляется при смене языка.

// Установить из кода
localizedText.SetValue("Menu/Play");

// С модификаторами
localizedText.SetValue("Messages/Caught", true, "Monsters/025/Name");

Добавьте этот компонент на любой GameObject с TMP_Text. Установите поле LocalizationKey в инспекторе. Когда SetOnEnable имеет значение true (по умолчанию), текст разрешается немедленно при включении.

Поддержка шрифтов CJK​

Для языков, требующих другие глифы (например, японский), используйте компонент LocalizationFontSwitcher совместно с ассетом LanguageFontMap.

Настройка​

  1. Создайте ассет LanguageFontMap через OpenMon > Localization > Language Font Map
  2. Добавьте записи, сопоставляющие названия языков со ссылками TMP_FontAsset:
    • "Japanese" -> NotoSansJP-Regular SDF
    • Опционально установите полужирный вариант для каждого языка
  3. Добавьте LocalizationFontSwitcher на корневой Canvas
  4. Назначьте LanguageFontMap в поле FontMap
  5. Установите SearchScope в Children (по умолчанию) или Scene

При смене языка переключатель перебирает все компоненты TMP_Text в заданной области и заменяет их шрифт. Он кеширует исходный шрифт, поэтому при возврате к языку без сопоставления восстанавливается шрифт по умолчанию.

Настройка внедрения зависимостей​

LocalizationInstaller (создаётся через OpenMon > Installers > Localization) связывает:

  • LocalizerSettings как синглтон (внедряется в Localizer)
  • ILocalizer с Localizer как ленивый синглтон
  • Все настроенные TextPostProcessors ставятся в очередь для внедрения
SceneContext
+-- LocalizationInstaller (ссылается на ассет LocalizerSettings)

Добавление нового языка​

  1. Добавьте новый столбец в вашу Google Sheet с названием языка в качестве заголовка (напр., French)
  2. Заполните переводы для всех ключей
  3. Повторно запустите импорт Google Sheet в редакторе
  4. Новый ассет French.asset появится в Resources/Languages/
  5. Если язык требует другого шрифта, добавьте запись в ваш LanguageFontMap

Изменения кода не требуются. Localizer обнаруживает все ассеты ScriptableLanguage в настроенном каталоге при запуске.