VanguardReMaster/client/Assets/Vanguard/Runtime/UI/Localization.cs

555 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}
/// <summary>
/// Небольшой runtime-словарь интерфейса. Русские строки остаются ключами,
/// поэтому существующие экраны не нужно переписывать под отдельные id.
/// </summary>
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<string, string> Exact = new Dictionary<string, string>(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: ",
};
private static readonly KeyValuePair<string, string>[] 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 == '→');
}
}
/// <summary>
/// Переводит uGUI-лейбл при смене языка и подхватывает текст, выставленный кодом.
/// </summary>
[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<Text>();
_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;
}
}
}