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",
["История"] = "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: ",
};
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;
}
}
}