using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using UnityEngine; using UnityEngine.UI; using Vanguard.Api.Contracts; namespace Vanguard.UI { public enum UiLanguage { Ru, En, } /// /// Небольшой runtime-словарь интерфейса. Русские строки остаются ключами, /// поэтому существующие экраны не нужно переписывать под отдельные id. /// public static class Loc { private const string PreferenceKey = "vanguard.ui.language"; private static bool _initialized; public static UiLanguage Language { get; private set; } = UiLanguage.Ru; public static bool IsEnglish => Language == UiLanguage.En; public static event Action LanguageChanged; private static readonly Dictionary Exact = new Dictionary(StringComparer.Ordinal) { ["ВЫ"] = "YOU", ["Вы"] = "You", ["вы"] = "you", ["да"] = "yes", ["Бой"] = "Battle", ["Все"] = "All", ["нет"] = "no", ["Вы: "] = "You: ", ["ДРОП"] = "DROP", ["Душа"] = "Soul", ["ДУША"] = "SOUL", ["Кадр"] = "Frame", ["Клан"] = "Clan", ["Пыль"] = "Dust", ["Ранг"] = "Grade", ["Рука"] = "Hand", ["УРОН"] = "DAMAGE", [" карт"] = " cards", ["» за "] = "» for ", ["В бой"] = "To battle", ["ВОЙТИ"] = "LOG IN", ["Выйти"] = "Log out", ["Кадр "] = "Frame ", ["Ничья"] = "Draw", ["Пусто"] = "Empty", ["Сброс"] = "Reset", ["Тип: "] = "Type: ", ["Щит: "] = "Shield: ", ["цена "] = "cost ", [" карт."] = " cards.", [" пыли)"] = " dust)", ["Атака "] = "Attack ", ["Атака"] = "Attack", ["Атака с поддержкой: "] = "Boosted attack: ", ["Активировать способность"] = "Activate ability", ["Активировать: "] = "Activate: ", ["Сброс для способности"] = "Discard for ability", ["Сбросить: "] = "Discard: ", ["Без сброса"] = "No discard", ["Сбросить карту в руке"] = "Discard a card from hand", ["Выберите карту в руке для сброса."] = "Choose a card in hand to discard.", ["Perfect Guard: выберите карту в руке для сброса."] = "Perfect Guard: choose a card in hand to discard.", ["Способность"] = "Ability", ["Guard без сброса"] = "Guard without discard", ["Perfect Guard: сбросить "] = "Perfect Guard: discard ", ["Защититься"] = "Guard", ["Защититься: "] = "Guard: ", ["Защита"] = "Guard", ["Не защищаться"] = "Do not guard", ["Скрыть панель"] = "Hide panel", ["Показать панель"] = "Show panel", ["Сменить: "] = "Ride: ", ["Заменить "] = "Replace ", ["Закончить муллиган"] = "Finish mulligan", ["Действия"] = "Actions", ["Переместить"] = "Move", ["Подробнее"] = "Details", ["Нет доступных действий"] = "No available actions", ["Выберите цель атаки."] = "Choose an attack target.", ["Выберите круг для перемещения."] = "Choose a circle to move to.", ["В меню"] = "To menu", ["Готово"] = "Done", ["Журнал"] = "Log", ["КОЛОДА"] = "DECK", ["Кадр ▶"] = "Frame ▶", ["Клан: "] = "Clan: ", ["Колода"] = "Deck", ["Купить"] = "Buy", ["Отмена"] = "Cancel", ["Пароль"] = "Password", ["Победа"] = "Victory", ["Ранг: "] = "Grade: ", ["Реплей"] = "Replay", ["СКРЫТА"] = "HIDDEN", ["Сервер"] = "Server", ["Токены"] = "Tokens", ["Урон: "] = "Damage: ", ["Фаза: "] = "Phase: ", ["◀ Кадр"] = "◀ Frame", [" · ход "] = " · turn ", ["1 токен"] = "1 token", ["\nМатч "] = "\nMatch ", ["АТАКА "] = "ATTACK ", ["Атака: "] = "Attack: ", ["Всего: "] = "Total: ", ["Готово."] = "Done.", ["Забрать"] = "Claim", ["Закрыть"] = "Close", ["Войти"] = "Sign in", ["Подключение…"] = "Connecting…", ["Создать аккаунт"] = "Create account", ["Забыли пароль?"] = "Forgot password?", ["Логин"] = "Login", ["Меню"] = "Menu", ["Адрес сервера"] = "Server address", ["Введите логин и пароль."] = "Enter login and password.", ["Сброс пароля не поддерживается. Обратитесь к администратору."] = "Password reset is not supported. Contact an administrator.", ["История"] = "History", ["Карта: "] = "Card: ", ["Магазин"] = "Store", ["Победа!"] = "Victory!", ["Принт: "] = "Printing: ", ["С ботом"] = "Vs bot", ["Сдаться"] = "Concede", ["События"] = "Events", ["Триггер"] = "Trigger", ["Удалить"] = "Delete", ["Фильтры"] = "Filters", ["← Назад"] = "← Back", [" рука "] = " hand ", [" (всего "] = " (total ", [" · пыль "] = " · dust ", [" · рука "] = " · hand ", [" · урон "] = " · damage ", [" · шаг: "] = " · step: ", [" карт · "] = " cards · ", [" против "] = " vs ", ["Действия"] = "Actions", ["Кадров: "] = "Frames: ", ["Колода: "] = "Deck: ", ["Крафтим…"] = "Crafting…", ["Купить «"] = "Buy «", ["Матчей: "] = "Matches: ", ["Принято."] = "Accepted.", ["Уже есть"] = "Already owned", [" · пыль: "] = " · dust: ", [", эффект "] = ", effect ", ["Все карты"] = "All cards", ["Выбрать: "] = "Choose: ", ["Клан: все"] = "Clan: all", ["Коллекция"] = "Collection", ["Нет колод"] = "No decks", ["ПРОТИВНИК"] = "OPPONENT", ["Поражение"] = "Defeat", ["Противник"] = "Opponent", ["РУКА · "] = "HAND · ", ["Разобрать"] = "Dismantle", ["Ранг: все"] = "Grade: all", ["Реплей · "] = "Replay · ", ["Скрафтить"] = "Craft", ["Сохранить"] = "Save", ["Триггер: "] = "Trigger: ", ["противник"] = "opponent", ["← В лобби"] = "← To lobby", [" · урон "] = " · damage ", [" · колода "] = " · deck ", [" завершён."] = " finished.", ["В фазу боя"] = "To battle phase", ["Имя игрока"] = "Player name", ["Кадров нет"] = "No frames", ["Купить за "] = "Buy for ", ["Маллиган: "] = "Mulligan: ", ["Мои колоды"] = "My decks", ["Нет реплея"] = "No replay", ["Ожидание: "] = "Waiting: ", ["Поражение."] = "Defeat.", ["Проверок: "] = "Checks: ", ["Разбираем…"] = "Dismantling…", ["Рука пуста"] = "Hand is empty", ["Сохраняем…"] = "Saving…", ["Триал-деки"] = "Trial decks", [" · решает: "] = " · deciding: ", [" уникальных"] = " unique", ["Активируем "] = "Activating ", ["Бустер BT01"] = "BT01 Booster", ["ЗОНА ЗАЩИТЫ"] = "GUARD ZONE", ["Отклонено ("] = "Rejected (", ["Поддержка: "] = "Boost: ", ["Противник: "] = "Opponent: ", ["Сохранено: "] = "Saved: ", ["Способности"] = "Abilities", ["без причины"] = "without reason", ["в коллекции"] = "in collection", ["← К истории"] = "← To history", ["← Коллекция"] = "← Collection", [" · Пыль "] = " · Dust ", [" · банк "] = " · bank ", [" · колода "] = " · deck ", [" · в колоде "] = " · in deck ", [" · событий: "] = " · events: ", [" карт · "] = " cards · ", ["3–32 символа"] = "3–32 characters", ["Колода пуста"] = "Deck is empty", ["Колода: 0/50"] = "Deck: 0/50", ["Новая колода"] = "New deck", ["Отправляем: "] = "Sending: ", ["Разобрать (+"] = "Dismantle (+", ["Скрафтить (−"] = "Craft (−", ["Событий нет."] = "No events.", ["Триггер: все"] = "Trigger: all", [" · Редкость: "] = " · Rarity: ", [" · в колоде: "] = " · in deck: ", ["Атака отбита."] = "Attack was guarded.", ["Атака прошла."] = "Attack went through.", ["Бустер открыт"] = "Booster opened", ["Закончить ход"] = "End turn", ["Матч завершён"] = "Match finished", ["Подключаемся…"] = "Connecting…", ["Способность #"] = "Ability #", [" · Токены "] = " · Tokens ", [" · побед: "] = " · wins: ", ["8–128 символов"] = "8–128 characters", ["Бустер открыт!"] = "Booster opened!", ["В коллекции: ×"] = "In collection: ×", ["История матчей"] = "Match history", ["Личный кабинет"] = "Profile", ["Матч завершён."] = "Match finished.", ["Отменить поиск"] = "Cancel search", ["Создать колоду"] = "Create deck", ["Триггер: сила "] = "Trigger: power ", ["Ищем соперника…"] = "Searching for opponent…", ["Колода удалена."] = "Deck deleted.", ["Название колоды"] = "Deck name", ["Повторить поиск"] = "Search again", ["Пропустить Ride"] = "Skip Ride", ["СОЗДАТЬ АККАУНТ"] = "CREATE ACCOUNT", ["Стартовый набор"] = "Starter set", ["Удалить колоду?"] = "Delete deck?", ["Уже в коллекции"] = "Already in collection", ["Бустер открыт · "] = "Booster opened · ", ["Готовим очередь…"] = "Preparing queue…", ["Карта разобрана."] = "Card dismantled.", ["Карта скрафчена."] = "Card crafted.", ["Матчей пока нет."] = "No matches yet.", ["Нельзя разобрать"] = "Cannot dismantle", ["Нельзя скрафтить"] = "Cannot craft", ["Переподключиться"] = "Reconnect", ["Поиск противника"] = "Opponent search", ["Пропустить Guard"] = "Skip Guard", ["Создаём аккаунт…"] = "Creating account…", ["Выберите покупку."] = "Choose a purchase.", ["Выбрать бесплатно"] = "Choose for free", ["Выбрать вариант #"] = "Choose option #", ["Выбрать триггер #"] = "Choose trigger #", ["Выйти из аккаунта"] = "Log out", ["Дека уже получена"] = "Deck already claimed", ["Загружаем реплей…"] = "Loading replay…", ["Купить за 1 токен"] = "Buy for 1 token", ["Очередь закрыта: "] = "Queue closed: ", ["Партия завершена."] = "Game over.", ["Подтвердить сдачу"] = "Confirm concede", ["Профиль обновлён."] = "Profile updated.", ["Фильтры коллекции"] = "Collection filters", ["Выбрать триал-деку"] = "Choose trial deck", ["Выполняем покупку…"] = "Processing purchase…", ["Загружаем историю…"] = "Loading history…", ["Загружаем магазин…"] = "Loading store…", ["Закончить маллиган"] = "Finish mulligan", ["Карт в коллекции: "] = "Cards in collection: ", ["Купить за 100 Gold"] = "Buy for 100 Gold", ["Обновляем профиль…"] = "Updating profile…", ["Повторить загрузку"] = "Retry loading", ["Подтвердите покупку"] = "Confirm purchase", ["Покупка не удалась."] = "Purchase failed.", ["Пропустить анимации"] = "Skip animations", ["Скрыть все действия"] = "Hide all actions", [" с · окно MMR: ±"] = " with · MMR window: ±", ["Онбординг завершён: "] = "Onboarding complete: ", ["Повтор уже применён."] = "Replay already applied.", ["В колоде уже 50 карт."] = "The deck already has 50 cards.", ["Загружаем триал-деки…"] = "Loading trial decks…", ["Подключаемся к матчу…"] = "Connecting to match…", ["Показать все действия"] = "Show all actions", ["Получаем новый тикет…"] = "Getting a new ticket…", ["Применить способность"] = "Activate ability", ["Сервер вернул ошибку "] = "Server returned error ", ["Соединение потеряно: "] = "Connection lost: ", [" · защита между полями"] = " · guard between fields", [". Выберите цель атаки."] = ". Choose an attack target.", ["Выберите цель на поле."] = "Choose a target on the field.", ["Колод пока нет. Пыль: "] = "No decks yet. Dust: ", ["Не удалось связаться: "] = "Could not connect: ", ["Пропустить способность"] = "Skip ability", ["Сервер не выдал тикет."] = "Server did not issue a ticket.", ["Триал-деки недоступны."] = "Trial decks are unavailable.", ["Ничья или матч прерван."] = "Draw or match interrupted.", ["Открыть бустер BT01 за "] = "Open BT01 booster for ", ["» добавлена в коллекцию."] = "» added to collection.", ["Не удалось создать матч."] = "Could not create a match.", ["Сейчас решает противник…"] = "Opponent is deciding…", ["Не удалось подключиться: "] = "Could not connect: ", ["Ошибка отправки интента: "] = "Failed to send intent: ", ["Сначала сохраните колоду."] = "Save the deck first.", ["RE:MASTER · СЕТЕВОЙ КЛИЕНТ"] = "RE:MASTER · NETWORK CLIENT", ["Матч найден! Подключаемся…"] = "Match found! Connecting…", ["Не удалось удалить колоду."] = "Could not delete the deck.", ["Готовим тренировочный матч…"] = "Preparing practice match…", ["Не удалось разобрать карту."] = "Could not dismantle the card.", ["Не удалось скрафтить карту."] = "Could not craft the card.", ["Переподключение не удалось."] = "Reconnect failed.", ["Состояние кадра недоступно."] = "Frame state unavailable.", ["Classic Ranked · скрытый MMR"] = "Classic Ranked · hidden MMR", ["Введите имя игрока и пароль."] = "Enter player name and password.", ["Не удалось загрузить реплей."] = "Could not load replay.", ["Не удалось сохранить колоду."] = "Could not save the deck.", ["Триггер: выберите цель силы."] = "Trigger: choose a power target.", [". ПКМ по карте — подробности."] = ". Right-click a card for details.", ["Загружаем коллекцию и колоды…"] = "Loading collection and decks…", ["Не удалось загрузить историю."] = "Could not load history.", ["Не удалось загрузить магазин."] = "Could not load store.", ["Состав появится после выбора."] = "The lineup appears after selection.", [" · ПКМ по карте — подробности."] = " · Right-click a card for details.", ["Не больше 4 копий одной карты."] = "No more than 4 copies of a card.", ["Не удалось обновить состояние."] = "Could not refresh state.", ["Текст способности отсутствует."] = "Ability text is missing.", ["Не удалось завершить онбординг."] = "Could not complete onboarding.", ["Не удалось загрузить декбилдер."] = "Could not load deck builder.", ["Не удалось начать матч с ботом."] = "Could not start a match against the bot.", ["Триггер: выберите цель эффекта."] = "Trigger: choose an effect target.", [".\nВыберите цель атаки или «Отмена»."] = ".\nChoose an attack target or “Cancel”.", ["Нажмите «Подтвердить сдачу» ещё раз."] = "Press “Confirm concede” again.", ["Очередь недоступна. Проверьте сервер."] = "Queue unavailable. Check the server.", ["Сервер не вернул ни одной триал-деки."] = "Server returned no trial decks.", ["Соединение открыто. Ожидаем состояние…"] = "Connection open. Waiting for state…", ["В коллекции больше нет свободных копий."] = "No free copies left in the collection.", ["Выберите деку — выбор делается один раз."] = "Choose a deck — this can be done only once.", ["\nКолода будет удалена без восстановления."] = "\nThe deck will be deleted permanently.", ["Нажмите на колоду,\nчтобы открыть редактор."] = "Click a deck\nto open the editor.", ["ЛКМ — убрать копию,\nПКМ — посмотреть карту."] = "LMB — remove a copy,\nRMB — view the card.", ["Для этой карты сейчас нет доступного действия."] = "No action is available for this card right now.", ["Только ваши матчи · реплей доступен участникам"] = "Only your matches · replay available to participants", ["Не удалось обновить профиль. Сервер недоступен?"] = "Could not update profile. Is the server down?", ["Не удалось загрузить триал-деки. Сервер недоступен?"] = "Could not load trial decks. Is the server down?", ["Онбординг не завершён: выберите стартовую триал-деку."] = "Onboarding incomplete: choose a starter trial deck.", [" · стенд 192.168.31.158:18080 · localhost:5080"] = " · stand 192.168.31.158:18080 · localhost:5080", ["Нет связи с сервером. Проверьте адрес и запущен ли сервер."] = "No connection to the server. Check the address and that the server is running.", ["Выберите цель на поле.\nНажмите «Отмена», чтобы снять выбор."] = "Choose a target on the field.\nPress “Cancel” to clear the selection.", ["5 карт: 4 C + 1 R+ · duplicate protection · 100 Gold или 1 токен."] = "5 cards: 4 C + 1 R+ · duplicate protection · 100 Gold or 1 token.", ["Нажмите на карту в руке или на поле.\nДопустимые цели подсветятся."] = "Click a card in hand or on the field.\nValid targets will be highlighted.", ["Выберите одну триал-деку. 50 карт попадут в коллекцию, плюс 5 токенов на бустеры."] = "Choose one trial deck. 50 cards will be added to your collection, plus 5 tokens for boosters.", ["Unity блокирует HTTP. Player Settings → Allow downloads over HTTP = Always, затем перезапусти Unity."] = "Unity blocks HTTP. Player Settings → Allow downloads over HTTP = Always, then restart Unity.", ["VanguardApp не создан. Запусти игру из Boot или через Play в любой сцене клиента."] = "VanguardApp was not created. Start the game from Boot or via Play in any client scene.", ["Кликните карты, которые вернутся в колоду."] = "Click the cards you want to return to the deck.", ["К обмену выбрано карт: "] = "Cards selected for mulligan: ", ["Закрытая карта"] = "Face-down card", ["Выберите закрытую карту урона для Counter Blast (необязательно)."] = "Choose a face-down damage card for Counter Blast (optional).", ["Для Counter Blast выбрана карта урона "] = "Damage card selected for Counter Blast: ", ["УРОН · "] = "DAMAGE · ", ["Нужно ровно 50 карт и 16 триггеров."] = "The deck needs exactly 50 cards and 16 triggers.", ["В колоде уже 16 триггеров."] = "The deck already has 16 triggers.", [" · триггеры: "] = " · triggers: ", ["Рейтинг"] = "Rating", ["Сообщество"] = "Community", ["Найдите соперника и сразитесь"] = "Find an opponent and fight", ["Соберите и настройте колоду"] = "Build and customize your deck", ["Бустеры, триал-деки и токены"] = "Boosters, trial decks and tokens", ["Сезон — скоро"] = "Season — coming soon", ["Ежедневные / еженедельные задания"] = "Daily / weekly missions", ["Задание появится позже"] = "Missions will appear later", ["Награды и прогресс — после появления серверных заданий."] = "Rewards and progress will arrive with server missions.", ["Золото"] = "Gold", ["Чат"] = "Chat", ["Чат появится позже"] = "Chat will appear later", ["Здесь будет общий чат лобби и личные сообщения. Пока серверного канала нет."] = "A lobby chat and direct messages will live here. There is no server channel yet.", ["Введите сообщение…"] = "Type a message…", ["Друзья онлайн"] = "Friends online", ["Список друзей появится вместе с сервером."] = "The friends list will arrive with the server.", ["скоро"] = "soon", ["матчей"] = "matches", ["побед"] = "wins", ["Рейтинговый"] = "Ranked", ["Выберите колоду и режим"] = "Choose a deck and mode", ["Загружаем колоды…"] = "Loading decks…", ["Нет колод. Соберите колоду."] = "No decks. Build a deck.", ["Не удалось загрузить колоды."] = "Could not load decks.", ["Проверяем колоду…"] = "Checking the deck…", ["Колода готова."] = "Deck is ready.", ["Колода не прошла проверку."] = "Deck failed validation.", ["Не удалось проверить колоду."] = "Could not validate the deck.", ["Собрать колоду"] = "Build a deck", ["Гайды"] = "Guides", ["Колоды"] = "Decks", ["Тикеты"] = "Tickets", ["Гайды, колоды и обратная связь"] = "Guides, decks and feedback", ["Разборы карт, колод и механик появятся позже. Материалы будут открываться прямо здесь."] = "Card, deck and mechanics guides will appear later and open right here.", ["Публичные колоды появятся позже. Сейчас колодой можно поделиться по коду в редакторе."] = "Public decks will appear later. For now a deck can be shared by code in the editor.", ["Отправка сообщений о багах и улучшениях появится позже: нужен серверный раздел тикетов."] = "Bug and improvement reports will arrive later: the server needs a ticket section.", ["Топ игроков по рейтингу появится позже: нужен серверный лидерборд."] = "The rating leaderboard will appear later: the server needs a leaderboard endpoint.", ["Создать"] = "Create", ["← К списку колод"] = "← Back to decks", ["Готова"] = "Ready", ["Не готова"] = "Not ready", ["Без триггера"] = "No trigger", ["Критический"] = "Critical", ["Добор"] = "Draw", ["Стойка"] = "Stand", ["Лечение"] = "Heal", ["Сбросить"] = "Reset", ["Применить"] = "Apply", ["Удалить колоду"] = "Delete deck", ["не сохранено"] = "not saved", ["Сохранено"] = "Saved", ["Кланы появятся после загрузки коллекции."] = "Clans will appear after the collection loads.", ["Нужно ровно 50 карт, 16 триггеров и не больше 4 копий."] = "Exactly 50 cards, 16 triggers and no more than 4 copies are required.", ["В лобби"] = "To lobby", ["Новый набор"] = "New deck", ["Поиск по названию"] = "Search by name", ["Редкость"] = "Rarity", ["есть"] = "owned", ["в колоде"] = "in deck", ["Открыть"] = "Open", }; private static readonly KeyValuePair[] Phrases = Exact .Where(pair => IsPhraseKey(pair.Key)) .OrderByDescending(pair => pair.Key.Length) .ToArray(); private static readonly Regex YesWord = new Regex( @"\bда\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex NoWord = new Regex( @"\bнет\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static void Initialize() { if (_initialized) return; _initialized = true; Language = PlayerPrefs.GetString(PreferenceKey, "ru") == "en" ? UiLanguage.En : UiLanguage.Ru; } public static void SetLanguage(UiLanguage language) { Initialize(); if (Language == language) return; Language = language; PlayerPrefs.SetString(PreferenceKey, language == UiLanguage.En ? "en" : "ru"); PlayerPrefs.Save(); LanguageChanged?.Invoke(); } public static void Toggle() => SetLanguage(IsEnglish ? UiLanguage.Ru : UiLanguage.En); public static string Translate(string source) { if (string.IsNullOrEmpty(source) || !IsEnglish) return source; if (Exact.TryGetValue(source, out var exact)) return exact; var result = source; foreach (var pair in Phrases) { if (result.Contains(pair.Key, StringComparison.Ordinal)) result = result.Replace(pair.Key, pair.Value, StringComparison.Ordinal); } result = YesWord.Replace(result, "yes"); result = NoWord.Replace(result, "no"); return result; } public static string CardName(CardCatalogItemDto card) { if (card == null) return string.Empty; var localized = IsEnglish ? card.NameEn : card.NameRu; return string.IsNullOrWhiteSpace(localized) ? card.NameEn : localized; } public static string OtherCardName(CardCatalogItemDto card) { if (card == null) return string.Empty; var other = IsEnglish ? card.NameRu : card.NameEn; return string.IsNullOrWhiteSpace(other) ? string.Empty : other; } public static string CardText(CardCatalogItemDto card) { if (card == null) return string.Empty; var localized = IsEnglish ? card.TextEn : card.TextRu; if (!string.IsNullOrWhiteSpace(localized)) return localized; return card.TextEn ?? string.Empty; } private static bool IsPhraseKey(string key) { if (key.Length >= 6) return true; return key.Any(character => character == ' ' || character == ':' || character == '.' || character == ',' || character == '!' || character == '?' || character == '«' || character == '»' || character == '·' || character == '—' || character == '–' || character == '\n' || character == '(' || character == ')' || character == '±' || character == '←' || character == '→'); } } /// /// Переводит uGUI-лейбл при смене языка и подхватывает текст, выставленный кодом. /// [DisallowMultipleComponent] public sealed class LocalizedText : MonoBehaviour { private Text _text; private string _source = string.Empty; private string _applied = string.Empty; private bool _literal; private bool _subscribed; private void Awake() => Subscribe(); private void OnDestroy() => Unsubscribe(); public void Initialize(string source) { _text = GetComponent(); _source = source ?? string.Empty; _literal = false; Subscribe(); ApplyCurrent(); } private void Subscribe() { if (_subscribed) return; Loc.LanguageChanged += ApplyCurrent; _subscribed = true; } private void Unsubscribe() { if (!_subscribed) return; Loc.LanguageChanged -= ApplyCurrent; _subscribed = false; } public void SetLiteral(string value) { _literal = true; _source = value ?? string.Empty; _applied = _source; if (_text != null) _text.text = _source; } private void LateUpdate() => ApplyNow(); public void ApplyNow() { if (_literal || _text == null) return; if (!string.Equals(_text.text, _applied, StringComparison.Ordinal)) _source = _text.text; ApplyCurrent(); } private void ApplyCurrent() { if (_literal || _text == null) return; _applied = Loc.Translate(_source); if (!string.Equals(_text.text, _applied, StringComparison.Ordinal)) _text.text = _applied; } } }