add SREBOT, SHARED, TSSBOT contents (fixup for #1223)

PR #1223 only staged the deletions of the old paths because the new
top-level directories were still untracked when the commit was authored.
This commit adds the actual restructured tree: SREBOT/ (existing bot),
SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES,
update_game_files), and TSSBOT/ (skeleton).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
FURRO404
2026-05-13 23:17:02 -07:00
commit 2b399fdb81
186 changed files with 96596 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
[
{
"title": "DISCORD OUTAGE - SEVERE",
"body": "Starting around <t:1778259412:F> (<t:1778259412:R>), Discord experienced a severe outage. The bot was unable to log in or post messages during this window, and a number of games were almost certainly missed. Service will resume automatically once Discord is fully back. Sorry for the inconvenience.",
"expires": 1778443012
},
{
"title": "Language Update",
"body": "We've updated our translations across the bot and website, and Chinese is now supported too. Use `/language` to select your preferred language.",
"expires": 1778529412
},
{
"title": "/comp Usage Limits",
"body": "Free servers now get **15 /comp lookups per timeslot** during SQB hours. Outside SQB hours, /comp remains unlimited for everyone. Premium subscribers get **unlimited /comp lookups** at all times — run `/unlock` to subscribe.",
"expires": 1778356612
},
{
"title": "Updated Terms of Service",
"body": "We've updated our [Terms of Service](https://srebot-meow.ing/terms) with new sections covering **Premium Subscription Terms** (billing, cancellation, refunds, price changes) and **Service Availability & Liability** (warranty, liability limits, service credits for outages). Please review the updated terms at your convenience.",
"expires": 1778716800
}
]
+65
View File
@@ -0,0 +1,65 @@
[
{
"max_br": 14.3,
"start": 1777618800,
"end": 1778137200,
"start_discord": "<t:1777618800:D>",
"end_discord": "<t:1778137200:D>"
},
{
"max_br": 12.0,
"start": 1778137200,
"end": 1778742000,
"start_discord": "<t:1778137200:D>",
"end_discord": "<t:1778742000:D>"
},
{
"max_br": 11.0,
"start": 1778742000,
"end": 1779346800,
"start_discord": "<t:1778742000:D>",
"end_discord": "<t:1779346800:D>"
},
{
"max_br": 10.0,
"start": 1779346800,
"end": 1779951600,
"start_discord": "<t:1779346800:D>",
"end_discord": "<t:1779951600:D>"
},
{
"max_br": 9.0,
"start": 1779951600,
"end": 1780556400,
"start_discord": "<t:1779951600:D>",
"end_discord": "<t:1780556400:D>"
},
{
"max_br": 8.0,
"start": 1780556400,
"end": 1781161200,
"start_discord": "<t:1780556400:D>",
"end_discord": "<t:1781161200:D>"
},
{
"max_br": 7.0,
"start": 1781161200,
"end": 1781766000,
"start_discord": "<t:1781161200:D>",
"end_discord": "<t:1781766000:D>"
},
{
"max_br": 6.0,
"start": 1781766000,
"end": 1782370800,
"start_discord": "<t:1781766000:D>",
"end_discord": "<t:1782370800:D>"
},
{
"max_br": 5.0,
"start": 1782370800,
"end": 1782802800,
"start_discord": "<t:1782370800:D>",
"end_discord": "<t:1782802800:D>"
}
]
+273
View File
@@ -0,0 +1,273 @@
"""
analytics.py
SQB session analytics: map win rates, team composition analysis,
player consistency, time-of-day performance, and opponent difficulty.
"""
# Standard Library Imports
import json
import logging
import math
import re
from collections import Counter, defaultdict
from datetime import datetime, timezone
# Third-Party Library Imports
import aiosqlite
# Local Module Imports
from .data_parser import get_unit_type_abbrev
from .utils import SQ_BATTLES_DB_PATH, decompress_json
async def get_map_stats(squadron_short: str, start_ts: int = 0) -> list[dict]:
"""Win rate by map for a squadron.
Returns list of dicts sorted by total games desc:
[{map_name, wins, losses, total, win_rate}, ...]
"""
async with aiosqlite.connect(SQ_BATTLES_DB_PATH, timeout=30.0) as db:
await db.execute("PRAGMA busy_timeout=30000;")
rows = await db.execute_fetchall(
"""SELECT map_name, winning_sq, losing_sq
FROM match_summary
WHERE (winning_sq = ? OR losing_sq = ?)
AND endtime_unix >= ?""",
(squadron_short, squadron_short, start_ts),
)
seen: dict[str, str] = {} # lowercase key -> first clean spelling
def _normalize_map(raw: str) -> tuple[str, str]:
clean = re.sub(r"^\s*\[[^\]]+\]\s*", "", raw).strip() or "Unknown"
key = clean.lower()
if key not in seen:
seen[key] = clean
return key, seen[key]
stats: dict[str, dict] = {}
for raw_name, winning_sq, losing_sq in rows:
key, name = _normalize_map(raw_name or "Unknown")
entry = stats.setdefault(key, {"map_name": name, "wins": 0, "losses": 0})
if winning_sq == squadron_short:
entry["wins"] += 1
else:
entry["losses"] += 1
result = list(stats.values())
for r in result:
r["total"] = r["wins"] + r["losses"]
r["win_rate"] = round(r["wins"] / r["total"] * 100, 1) if r["total"] else 0
result.sort(key=lambda x: x["total"], reverse=True)
return result
async def get_comp_analysis(squadron_short: str, start_ts: int = 0) -> list[dict]:
"""Vehicle type composition vs win rate.
Parses winning_team_json/losing_team_json, classifies vehicles,
builds comp signatures (e.g. "3T 1H 1F"), correlates with outcomes.
Returns list sorted by usage:
[{comp_signature, wins, losses, total, win_rate}, ...]
"""
async with aiosqlite.connect(SQ_BATTLES_DB_PATH, timeout=30.0) as db:
await db.execute("PRAGMA busy_timeout=30000;")
rows = await db.execute_fetchall(
"""SELECT winning_sq, losing_sq, winning_team_json, losing_team_json
FROM match_summary
WHERE (winning_sq = ? OR losing_sq = ?)
AND endtime_unix >= ?""",
(squadron_short, squadron_short, start_ts),
)
stats: dict[str, dict] = {}
for winning_sq, losing_sq, w_json, l_json in rows:
is_win = winning_sq == squadron_short
team_json = w_json if is_win else l_json
if not team_json:
continue
try:
team = decompress_json(team_json)
except (json.JSONDecodeError, TypeError, OSError):
continue
# Classify vehicles
type_counts: Counter = Counter()
players = team.get("players", [])
for p in players:
veh_internal = p.get("vehicle", "")
if not veh_internal or veh_internal == "DISCONNECTED":
continue
vtype = get_unit_type_abbrev(veh_internal)
type_counts[vtype] += 1
if not type_counts:
continue
# Build signature: sorted by count desc, then alpha
sig_parts = sorted(type_counts.items(), key=lambda x: (-x[1], x[0]))
signature = " ".join(f"{count}{abbr}" for abbr, count in sig_parts)
entry = stats.setdefault(signature, {"comp_signature": signature, "wins": 0, "losses": 0})
if is_win:
entry["wins"] += 1
else:
entry["losses"] += 1
result = list(stats.values())
for r in result:
r["total"] = r["wins"] + r["losses"]
r["win_rate"] = round(r["wins"] / r["total"] * 100, 1) if r["total"] else 0
result.sort(key=lambda x: x["total"], reverse=True)
return result[:20]
async def get_player_consistency(squadron_short: str, min_games: int = 50) -> list[dict]:
"""Per-player performance variance.
Returns list sorted by most recently played:
[{uid, nick, avg_kills, std_kills, avg_deaths, std_deaths, games, consistency_score, last_played}, ...]
"""
async with aiosqlite.connect(SQ_BATTLES_DB_PATH, timeout=30.0) as db:
await db.execute("PRAGMA busy_timeout=30000;")
# squadron_name in player_games_hist stores the short name (e.g. "DSPL")
rows = await db.execute_fetchall(
"""SELECT UID, nick,
SUM(ground_kills + air_kills) as total_kills,
SUM(deaths) as total_deaths,
session_id,
MAX(endtime_unix) as last_played
FROM player_games_hist
WHERE squadron_name = ?
GROUP BY UID, session_id""",
(squadron_short,),
)
# Aggregate per-player
player_data: dict[str, dict] = {}
for uid, nick, kills, deaths, session_id, last_played in rows:
entry = player_data.setdefault(uid, {"uid": uid, "nick": nick, "kills": [], "deaths": [], "last_played": 0})
entry["nick"] = nick # keep latest nick
entry["kills"].append(kills or 0)
entry["deaths"].append(deaths or 0)
entry["last_played"] = max(entry["last_played"], last_played or 0)
result = []
for uid, data in player_data.items():
games = len(data["kills"])
if games < min_games:
continue
avg_kills = sum(data["kills"]) / games
avg_deaths = sum(data["deaths"]) / games
std_kills = math.sqrt(sum((k - avg_kills) ** 2 for k in data["kills"]) / games)
std_deaths = math.sqrt(sum((d - avg_deaths) ** 2 for d in data["deaths"]) / games)
# Consistency score: lower is more consistent (normalized std dev)
consistency = (std_kills + std_deaths) / max(avg_kills + avg_deaths, 1)
result.append({
"uid": uid,
"nick": data["nick"],
"avg_kills": round(avg_kills, 2),
"std_kills": round(std_kills, 2),
"avg_deaths": round(avg_deaths, 2),
"std_deaths": round(std_deaths, 2),
"games": games,
"consistency_score": round(consistency, 3),
"last_played": data["last_played"],
})
result.sort(key=lambda x: x["avg_kills"] / max(x["avg_deaths"], 0.01), reverse=True)
return result[:128]
async def get_matchup_history(squadron_short: str, start_ts: int = 0) -> dict:
"""Top opponents this squadron has won against and lost to.
Returns:
{
"won_against": [{opponent, wins, losses, total, win_rate}, ...up to 10],
"lost_against": [{opponent, wins, losses, total, win_rate}, ...up to 10],
"total_opponents": int,
}
"""
async with aiosqlite.connect(SQ_BATTLES_DB_PATH, timeout=30.0) as db:
await db.execute("PRAGMA busy_timeout=30000;")
rows = await db.execute_fetchall(
"""SELECT winning_sq, losing_sq
FROM match_summary
WHERE (winning_sq = ? OR losing_sq = ?)
AND endtime_unix >= ?""",
(squadron_short, squadron_short, start_ts),
)
stats: dict[str, dict] = {}
for winning_sq, losing_sq in rows:
if not winning_sq or not losing_sq or winning_sq == losing_sq:
continue
opponent = losing_sq if winning_sq == squadron_short else winning_sq
if not opponent:
continue
entry = stats.setdefault(opponent, {"opponent": opponent, "wins": 0, "losses": 0})
if winning_sq == squadron_short:
entry["wins"] += 1
else:
entry["losses"] += 1
enriched = []
for r in stats.values():
r["total"] = r["wins"] + r["losses"]
r["win_rate"] = round(r["wins"] / r["total"] * 100, 1) if r["total"] else 0
enriched.append(r)
won_against = sorted(enriched, key=lambda x: (x["wins"], x["total"]), reverse=True)[:10]
lost_against = sorted(enriched, key=lambda x: (x["losses"], x["total"]), reverse=True)[:10]
return {
"won_against": won_against,
"lost_against": lost_against,
"total_opponents": len(enriched),
}
async def get_time_performance(squadron_short: str) -> dict:
"""Win rate bucketed by hour of day (UTC).
Returns dict keyed by hour (0-23):
{hour: {wins, losses, total, win_rate}, ...}
"""
async with aiosqlite.connect(SQ_BATTLES_DB_PATH, timeout=30.0) as db:
await db.execute("PRAGMA busy_timeout=30000;")
rows = await db.execute_fetchall(
"""SELECT endtime_unix, winning_sq, losing_sq
FROM match_summary
WHERE (winning_sq = ? OR losing_sq = ?)
AND endtime_unix > 0""",
(squadron_short, squadron_short),
)
stats: dict[int, dict] = {}
for endtime_unix, winning_sq, losing_sq in rows:
hour = datetime.fromtimestamp(endtime_unix, tz=timezone.utc).hour
entry = stats.setdefault(hour, {"wins": 0, "losses": 0})
if winning_sq == squadron_short:
entry["wins"] += 1
else:
entry["losses"] += 1
result = {}
for hour in sorted(stats.keys()):
e = stats[hour]
total = e["wins"] + e["losses"]
result[hour] = {
"wins": e["wins"],
"losses": e["losses"],
"total": total,
"win_rate": round(e["wins"] / total * 100, 1) if total else 0,
}
return result
+2164
View File
File diff suppressed because it is too large Load Diff
+9402
View File
File diff suppressed because it is too large Load Diff
+1101
View File
File diff suppressed because it is too large Load Diff
+2486
View File
File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
"""
health.py
Bot health monitoring. Tracks task execution status, WebSocket connectivity,
and game processing metrics. Writes periodic heartbeat to STORAGE/bot_health.json.
"""
# Standard Library Imports
import json
import logging
import time
from collections import deque
from pathlib import Path
# Third-Party Library Imports
import aiofiles
# Local Module Imports
from .utils import STORAGE_DIR, get_bot
HEALTH_PATH = STORAGE_DIR / "bot_health.json"
# Rolling window for games-processed counters
_games_timestamps: deque[float] = deque()
_health_state: dict = {
"bot_started_at": None,
"guild_count": 0,
"last_heartbeat": 0,
"tasks": {},
"websocket": {},
"games_processed_1h": 0,
"games_processed_24h": 0,
}
def init_health(started_at: float, guild_count: int) -> None:
"""Initialize health state on bot startup."""
_health_state["bot_started_at"] = started_at
_health_state["guild_count"] = guild_count
async def record_task_run(task_name: str, success: bool, error: str = "") -> None:
"""Record a task execution result."""
entry = _health_state["tasks"].setdefault(task_name, {
"status": "unknown",
"last_run": 0,
"run_count": 0,
"error_count": 0,
"last_error": "",
})
entry["last_run"] = time.time()
entry["run_count"] += 1
if success:
entry["status"] = "ok"
else:
entry["status"] = "error"
entry["error_count"] += 1
entry["last_error"] = str(error)[:200]
async def record_ws_message(ws_name: str) -> None:
"""Record a WebSocket message receipt."""
entry = _health_state["websocket"].setdefault(ws_name, {
"connected": True,
"last_message_at": 0,
"messages_processed": 0,
})
entry["connected"] = True
entry["last_message_at"] = time.time()
entry["messages_processed"] += 1
def record_ws_disconnect(ws_name: str) -> None:
"""Mark a WebSocket as disconnected."""
entry = _health_state["websocket"].get(ws_name)
if entry:
entry["connected"] = False
def record_game_processed() -> None:
"""Record that a game was processed (for hourly/daily counters)."""
_games_timestamps.append(time.time())
def _prune_games_window() -> tuple[int, int]:
"""Count games in the last 1h and 24h, pruning old entries."""
now = time.time()
cutoff_24h = now - 86400
while _games_timestamps and _games_timestamps[0] < cutoff_24h:
_games_timestamps.popleft()
cutoff_1h = now - 3600
count_1h = sum(1 for ts in _games_timestamps if ts >= cutoff_1h)
return count_1h, len(_games_timestamps)
async def write_heartbeat() -> None:
"""Dump current health state to HEALTH_PATH as JSON."""
try:
bot = get_bot()
_health_state["guild_count"] = len(bot.guilds)
except Exception:
pass
_health_state["last_heartbeat"] = time.time()
games_1h, games_24h = _prune_games_window()
_health_state["games_processed_1h"] = games_1h
_health_state["games_processed_24h"] = games_24h
try:
HEALTH_PATH.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(HEALTH_PATH, "w", encoding="utf-8") as f:
await f.write(json.dumps(_health_state, indent=2, default=str))
except Exception as e:
logging.error(f"[HEALTH] Failed to write heartbeat: {e}")
async def get_health_snapshot() -> dict:
"""Return current health state dict (live, not from file)."""
try:
bot = get_bot()
_health_state["guild_count"] = len(bot.guilds)
except Exception:
pass
_health_state["last_heartbeat"] = time.time()
games_1h, games_24h = _prune_games_window()
_health_state["games_processed_1h"] = games_1h
_health_state["games_processed_24h"] = games_24h
return dict(_health_state)
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Chyba",
"no_data_title": "Žádná data",
"access_denied_title": "Přístup zamítnut",
"access_denied_desc": "Tento server byl zablokován.",
"no_players_selected": "Žádní hráči nevybráni. Vyberte prosím alespoň jednoho hráče.",
"must_use_in_server": "Tento příkaz lze použít pouze na serveru.",
"could_not_resolve_channel": "Nelze najít vybraný kanál.",
"failed_update_setting": "❌ Nastavení se nepodařilo aktualizovat.",
"configuration_not_found": "Konfigurace nenalezena.",
"no_channel_selected": "Žádný kanál nevybrán.",
"no_selection_received": "Nebyla přijata žádná volba.",
"database_error": "❌ Chyba databáze: {error}",
"enabled": "Zapnuto",
"disabled": "Vypnuto",
"not_configured": "Nenastaveno",
"unknown": "Neznámý",
"rating_field": "Hodnocení",
"battles_field": "Bitvy",
"wins_field": "Výhry",
"losses_field": "Prohry",
"win_rate_field": "Úspěšnost",
"kills_field": "Zabití",
"deaths_field": "Úmrtí",
"kd_field": "K/D",
"members_field": "Členové",
"placement_field": "Umístění",
"points_field": "Body",
"ground_kills_field": "Pozemní zabití",
"air_kills_field": "Vzdušná zabití",
"total_kills_field": "Celková zabití",
"assists_field": "Asistence",
"captures_field": "Obsazení",
"none_option": "Žádný"
},
"buttons": {
"skip": "Přeskočit",
"previous": "Předchozí",
"next": "Další",
"prev": "Zpět",
"prev_arrow": "◀ Předchozí",
"next_arrow": "Další ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Generovat graf",
"show_graph": "Zobrazit graf",
"view_player_stats": "📊 Zobrazit statistiky hráčů",
"compare_nearby": "📈 Porovnat okolní svazy",
"confirm_swap": "Ano, přepnout",
"cancel_swap": "Ne, ponechat stávající",
"set_squadron": "Nastavit svaz",
"same_as_logs": "Stejný jako logy",
"require_password": "🔒 Vyžadovat heslo",
"password_required": "🔒 Heslo vyžadováno",
"lock_data": "🔐 Připojit data svazu",
"data_locked": "🔐 Data připojena k serveru",
"allow_public": "👥 Povolit veřejná meta data",
"public_enabled": "👥 Veřejná meta data povolena",
"update_accounts": "📋 Aktualizovat meta účty",
"change_password": "🔑 Změnit heslo",
"help": "❓ Nápověda",
"add_player": " Přidat hráče",
"update_all": "🔄 Aktualizovat všechny členy",
"back_to_settings": "⬅ Zpět na nastavení",
"manage_notifications": "Správa oznámení",
"diagnose_permissions": "Diagnostika oprávnění",
"enable": "Zapnout",
"disable": "Vypnout",
"change_channel": "Změnit kanál",
"view_replay": "Zobrazit záznam",
"view_website": "Zobrazit na webu",
"view_video": "Zobrazit video",
"view_log": "Zobrazit log",
"view_chat": "Zobrazit chat",
"subscribe_website": "Přihlásit přes web",
"yes_disband": "Ano, rozpustit",
"cancel": "Zrušit",
"transfer_leave": "Předat a odejít",
"accept_selected": "Přijmout vybrané",
"accept_all": "Přijmout všechny",
"decline_selected": "Odmítnout vybrané",
"back": "Zpět",
"remove_all": "Odebrat všechny",
"remove_active": "Odebrat aktivní",
"remove_queued": "Odebrat ve frontě",
"remove_selected": "Odebrat vybrané",
"ping_all": "Pingnout všechny",
"ping_active": "Pingnout aktivní",
"ping_queued": "Pingnout ve frontě",
"ping_selected": "Pingnout vybrané",
"accept_members": "Přijmout členy",
"remove_members": "Odebrat členy",
"ping_members": "Pingnout členy",
"rename_stack": "Přejmenovat stack",
"request_to_join": "Požádat o vstup",
"leave_withdraw": "Odejít / Stáhnout",
"manage_stack": "Spravovat stack ⚙️",
"disband_stack": "Rozpustit stack",
"force_disband_create": "Vynutit rozpuštění a vytvořit nový"
},
"events": {
"guild_join_title": "Díky za přidání!",
"guild_join_desc": "Spusťte `/setup` pro nastavení bota na tomto serveru."
},
"comp": {
"not_found_title": "Sestavy nenalezeny",
"not_found_desc": "Žádná data pro **{squadron}**, zkuste to znovu později.",
"error_loading_title": "Chyba při načítání sestav",
"error_loading_desc": "Nepodařilo se načíst data sestav: {error}",
"title": "Sestavy pro {squadron}",
"desc": "Sestavy viděné za posledních {minutes} minut",
"no_recent_title": "Žádné nedávné sestavy",
"no_recent_desc": "Žádné sestavy za posledních {minutes} minut.",
"comp_title": "SESTAVA {index}",
"last_seen_label": "**Naposledy viděna** : {timestamp}{warning}",
"comp_label": "**Sestava**: {notation}",
"no_players_recorded": "Žádní hráči nezaznamenáni.",
"limit_reached_title": "Limit sestav dosažen",
"limit_reached_desc": "Tento server vyčerpal všech {limit} vyhledávání sestav pro tento časový slot. Předplaťte si (pomocí /unlock) neomezený přístup nebo počkejte na další časový slot.",
"remaining_footer": "{remaining}/{limit} vyhledávání sestav zbývá v tomto časovém slotu"
},
"quick_log": {
"invalid_type": "Typ lze nastavit pouze na Logy, Body, Žebříček, Týdenní BR nebo Oba.",
"squadron_required": "Pro alarmy Logy, Body nebo Obojí musíte zadat název svazu.",
"wildcard_logs_only": "Pouze Logy lze nastavit na zástupný svaz.",
"squadron_not_resolved": "Svaz `{squadron}` se nepodařilo najít.",
"save_failed": "Nepodařilo se uložit předvolby. Zkuste to prosím znovu.",
"premium_warning": "\n\n> ⚠️ **Herní logy vyžadují Premium.** Spusťte `/unlock` pro přihlášení k odběru (2,99 $/měs.) — logy se nebudou odesílat, dokud tak neučiníte.",
"leaderboard_set": "Globální alarm žebříčku nastaven na tento kanál.",
"both_set": "Alarmy logů a bodů pro {squadron} nastaveny na tento kanál.{premium_note}",
"alarm_set": "Alarm {alarm_type} pro {squadron} nastaven na tento kanál.{premium_note}",
"weekly_br_wildcard_set": "Týdenní zpráva BR (top 20 letek) nastavena pro tento kanál. Odesílá se na konci každé rotace BR.",
"weekly_br_squadron_set": "Týdenní zpráva BR pro {squadron} (top 15 hráčů) nastavena pro tento kanál. Odesílá se na konci každé rotace BR."
},
"diagnostics": {
"title": "Diagnostika autologu",
"channel_permissions_header": "**Oprávnění kanálu** (<#{channel_id}>)",
"perms_needed": " ^ Autologování potřebuje vše výše uvedené pro odesílání výsledkových tabulek.",
"server_squadron_header": "**Svaz serveru** (`/set-squadron`)",
"server_squadron_short": " Zkratka: `{short}`",
"server_squadron_long": " Celý název: `{long}`",
"server_squadron_not_set": " Nenastaveno (barva pruhu výsledkové tabulky bude zobrazena jako 'not_set')",
"autolog_prefs_header": "**Předvolby autologu** (`/quick-log`)",
"autolog_none_configured": " ❌ ŽÁDNÉ nenastaveno — autologování NEBUDE odesílat nic na tento server.",
"autolog_setup_hint": " Použijte `/quick-log <squadron_short> Logs` v cílovém kanálu pro nastavení.",
"autolog_no_logs_channels": " ❌ Žádné kanály pro Logy nenakonfigurovány. Nalezeny pouze Body/Žebříček.",
"autolog_enable_hint": " Použijte `/quick-log <squadron_short> Logs` pro zapnutí autologování.",
"selected_channel_tag": " **(vybraný kanál)**",
"missing_send_attach": " (chybí oprávnění k odesílání/přílohám)",
"channel_not_found": " (kanál nenalezen)",
"invalid_channel_id": " (neplatné ID kanálu)",
"premium_status_header": "**Stav Premium** (`/unlock`)",
"premium_active": " ✅ Tento server má aktivní Premium předplatné.",
"premium_not_subscribed": " ❌ Tento server **nemá** Premium předplatné.",
"premium_autolog_required": " Autologování vyžaduje Premium. Použijte `/unlock` pro přihlášení k odběru.",
"premium_not_subscribed_free": " ⚪ Nepřihlášeno k odběru — použijte `/unlock` pro přihlášení (2,99 $/měs.).",
"premium_free_note": " *(Autology jsou nyní zdarma pro všechny servery.)*"
},
"sq_info": {
"title": "Informace o svazu: {squadron}",
"placement_field": "Umístění",
"total_points_field": "Celkové body",
"total_members_field": "Celkem členů",
"members_field": "Členové",
"fetch_failed": "Nepodařilo se načíst informace o svazu."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Sezóna {season})",
"embed_title": "{squadron} — Složení svazu",
"embed_desc": "Sezóna **{season}** · Medián zápasů: **{median}** · Jádro: **{core}** · Aktivní: **{active}** · Slabí: **{weak}**\nSloupce řazeny podle zápasů sestupně; výška = procento výher. Jádro = horních 30 % WR a zápasů ≥ medián. Aktivní = horních 3045 % WR a zápasů ≈ medián. Slabí = všichni ostatní.",
"core_threshold_line": "JÁDRO ≥ {wr} %",
"weak_threshold_line": "SLABÍ < {wr} %",
"y_label": "Procento výher",
"core_header": "JÁDRO — {count} · WR {avg}%",
"active_header": "AKTIVNÍ — {count} · WR {avg}%",
"weak_header": "SLABÍ — {count} · WR {avg}%",
"no_active_season": "Nebyla nalezena žádná aktivní sezóna. Zkuste to znovu, až začne další.",
"no_members": "Nebyli nalezeni žádní aktuální členové pro {squadron}."
},
"recap_card": {
"unknown_season": "Neznámá sezóna: `{season}`.",
"no_clan_id": "Nepodařilo se určit ID svazu `{squadron}`.",
"render_failed": "Nepodařilo se vygenerovat kartu sezónního přehledu. Zkuste to znovu později."
},
"sq_stats": {
"no_data_title": "Žádná data",
"no_data_desc": "Pro svaz nenalezena žádná historická data: {squadron}",
"title": "{squadron} // SVAZ",
"desc": "Trend celkového skóre (posledních {count} datových bodů)",
"previous_score_field": "Předchozí skóre",
"current_score_field": "Aktuální skóre",
"change_field": "Změna",
"player_title": "{squadron} // HRÁČI",
"player_desc": "Trendy bodů jednotlivých hráčů",
"comparison_title": "{squadron} // POROVNÁNÍ ŽEBŘÍČKU",
"comparison_desc": "Porovnání se svazy na pozicích {range}",
"current_position_field": "Aktuální pozice",
"squadrons_shown_field": "Zobrazené svazy",
"squadron_not_found_error": "Svaz nenalezen v žebříčku",
"no_nearby_error": "Žádné okolní svazy nenalezeny",
"no_historical_error": "Nenalezena žádná historická data pro okolní svazy",
"comparison_chart_failed": "Nepodařilo se vygenerovat srovnávací graf",
"select_players_placeholder": "Vyberte hráče (strana {page})"
},
"loss_calc": {
"title": "Ztráta bodů — {squadron}",
"players_leaving_field": "Odcházející hráči",
"share_of_total_field": "% podíl z celku",
"points_lost_real_field": "Ztracené body (skutečné)",
"points_lost_raw_field": "Ztracené body (hrubé)",
"squadron_rating_field": "Hodnocení svazu",
"squadron_position_field": "Pozice svazu",
"positions_lost_field": "Ztracené pozice",
"not_found_footer": "Nenalezeno ve svazu: {players}",
"fetch_failed": "Nepodařilo se načíst data svazu: {error}",
"no_point_data": "Pro tento svaz nejsou dostupná žádná bodová data.",
"no_matching_players": "Ve **{squadron}** nebyli nalezeni žádní shodující se hráči."
},
"player": {
"select_player_placeholder": "Vyberte hráče",
"no_stats_found": "❌ Nenalezeny žádné statistiky pro UID: {uid}",
"no_vehicle_stats": "❌ Pro tohoto hráče nebyla nalezena žádná statistika vozidel.",
"vehicles_found": "Nalezeno **{count}** vozidel pro **{nick}**\nVyberte vozidlo pro zobrazení podrobných statistik:",
"vehicle_select_placeholder": "Vyberte vozidlo (strana {page}/{total})",
"combat_stats_header": "**__BOJOVÉ STATISTIKY__**",
"ground_kills_label": "**Pozemní zabití:** {value}",
"air_kills_label": "**Vzdušná zabití:** {value}",
"total_kills_label": "**Celková zabití:** {value}",
"assists_label": "**Asistence:** {value}",
"deaths_label": "**Úmrtí:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Obsazení:** {value}",
"battle_record_header": "**__HERNÍ ZÁZNAM__**",
"total_battles_label": "**Celkové bitvy:** {value}",
"wins_label": "**Výhry:** {value}",
"losses_label": "**Prohry:** {value}",
"win_rate_label": "**Úspěšnost:** {value}%",
"stats_desc": "Statistiky pro **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Hráč nenalezen",
"not_found_desc": "Pro `{player}` nebyla nalezena žádná herní historie.",
"no_players_found": "Nebyli nalezeni žádní hráči odpovídající **{username}**\nZkuste použít `/website` pro vyhledávání na webu.",
"multiple_matches": "Nalezeno více shod, vyberte správnou níže:",
"must_provide_input": "Musíte zadat alespoň UID nebo uživatelské jméno."
},
"player_games": {
"no_recent_title": "Žádné nedávné hry",
"no_recent_desc": "Pro hráče **{player}** nebyly nalezeny žádné hry za posledních 8 hodin.",
"squadron_label": "**Svaz:** {squadron}",
"record_label": "**V:** {wins} **P:** {losses} **Ú:** {wr}%",
"comps_played_header": "\n\n**Odehrané sestavy**"
},
"match": {
"missing_input_title": "Chybějící vstup",
"missing_input_desc": "Zadejte `match_id` nebo `player_name`.",
"not_found_title": "Zápas nenalezen",
"not_found_desc": "Nebyl nalezen zápas s ID `{match_id}`.",
"invalid_data_title": "Neplatná data zápasu",
"invalid_data_desc": "Data záznamu nebylo možné zpracovat.",
"scoreboard_error_title": "Chyba výsledkové tabulky",
"scoreboard_error_desc": "Nepodařilo se vygenerovat obrázek výsledkové tabulky.",
"no_games_title": "Žádné hry nenalezeny",
"no_games_desc": "Pro hráče **{player}** nebyla nalezena žádná herní historie.",
"recent_matches_title": "Nedávné zápasy pro {player}",
"recent_matches_desc": "Zobrazeno až {count} nedávných her. Vyberte jednu pro zobrazení celé výsledkové tabulky.",
"select_match_placeholder": "Vyberte zápas k zobrazení..."
},
"compare": {
"no_players_found": "Nebyli nalezeni žádní hráči odpovídající **{name}**.",
"multiple_matches": "Více shod pro **{name}**: {matches}\nPoužijte prosím přesnější jméno (návrhy automatického doplňování jsou přesné).",
"could_not_resolve": "Hráče nebylo možné najít.",
"could_not_fetch": "❌ Nelze načíst statistiky pro **{name}**.",
"no_graph_data": "Žádná data dostupná za posledních 90 dní.",
"no_squadron_points_data": "Žádná data bodů svazu pro {names} (hráč nenalezen v historii sledovaného svazu).",
"graph_title": "Body hráče — posledních 90 dní",
"battles_label": "Bitvy",
"wins_label": "Výhry",
"losses_label": "Prohry",
"win_rate_label": "Úspěšnost",
"ground_kills_label": "Pozemní zabití",
"air_kills_label": "Vzdušná zabití",
"total_kills_label": "Celková zabití",
"assists_label": "Asistence",
"deaths_label": "Úmrtí",
"kd_label": "K/D",
"captures_label": "Obsazení"
},
"squadron": {
"not_found_desc": "Svaz `{squadron}` nenalezen.",
"set_title": "✅ Svaz nastaven",
"set_desc": "Svaz **{squadron}** byl nastaven pro tento server.",
"short_name_field": "Zkratka",
"long_name_field": "Celý název",
"swap_title": "✅ Svaz přepnut",
"swap_desc": "Svaz **{old}** nahrazen svazem **{new}** na tomto serveru.",
"already_set_title": "⚠️ Svaz již nastaven",
"already_set_desc": "Tento server je momentálně nastaven na **{old}**.\nPřepnout na **{new}**?",
"swap_cancelled": "❌ Změna svazu zrušena."
},
"setup": {
"step1_title": "Nastavení serveru — krok 1 ze 3",
"step1_desc": "Tento průvodce vás provede nastavením bota pro váš server.\n\n**Krok 1** — Nastavte svaz\n**Krok 2** — Zvolte kanál pro logy\n**Krok 3** — Zvolte kanál pro body\n",
"step1_current_sq": "\nAktuálně nastavený svaz: **[{short}] {long}**",
"step2_title": "Nastavení serveru — krok 2 ze 3",
"step2_desc": "Svaz nastaven na **[{short}] {long}**.\n\nKam mají být odesílány **bojové logy**?\nVyberte textový kanál níže nebo tento krok přeskočte.",
"step3_title": "Nastavení serveru — krok 3 ze 3",
"step3_desc": "Kam mají být odesílána **bodová oznámení**?\nVyberte textový kanál níže nebo tento krok přeskočte.",
"step3_same_as_logs": "\n\nMůžete také kliknout na „Stejna jako Logy“ a znovu použít kanál logů.",
"summary_title": "Nastavení dokončeno",
"summary_desc": "Tato nastavení lze později změnit pomocí `/autolog-management`.",
"squadron_field": "Svaz",
"logs_channel_field": "Kanál logů",
"points_channel_field": "Kanál bodů",
"premium_required_field": "⚠️ Herní logy vyžadují Premium",
"premium_required_value": "Automatické herní výsledkové tabulky se nebudou odesílat, dokud tento server nemá aktivní předplatné. Spusťte `/unlock` pro přihlášení k odběru (2,99 $/měs.).",
"modal_title": "Nastavit svaz",
"modal_label": "Zkratka svazu",
"modal_placeholder": "např. AXYS",
"squadron_not_found": "Svaz `{squadron}` nenalezen. Zkuste to prosím znovu.",
"logs_channel_placeholder": "Vyberte kanál logů...",
"points_channel_placeholder": "Vyberte kanál bodů..."
},
"meta_management": {
"squadron_not_found_title": "❌ Svaz nenalezen",
"squadron_not_found_desc": "Nelze najít ID klanu pro svaz: **{squadron}**",
"access_denied_title": "❌ Přístup zamítnut",
"access_denied_desc": "Nesprávné heslo. Meta data tohoto svazu jsou chráněna.",
"data_locked_title": "🔐 Data svazu připojena",
"data_locked_desc": "**{squadron}** má zapnuté připojení dat a nemůže být přenesen na jiný server.\n\nVlastník svazu musí vypnout **Připojit data svazu** před přenosem.",
"error_retrieving_settings": "❌ Chyba při získávání nastavení serveru po přenosu. Zkuste to prosím znovu.",
"error_retrieving_settings_retry": "❌ Chyba při získávání nastavení serveru. Zkuste prosím příkaz spustit znovu.",
"authenticated_title": "✅ Ověřeno",
"authenticated_desc": "Heslo ověřeno. Spravuji nastavení pro **{squadron}**.",
"claimed_title": "✅ Svaz přijat",
"claimed_desc": "**{squadron}** byl úspěšně přijat pro tento server!",
"password_requirement_field": "🔒 Požadavek na heslo",
"data_lock_field": "🔐 Připojení dat svazu",
"public_meta_field": "👥 Veřejný přístup k meta datům",
"access_password_field": "🔑 Přístupové heslo",
"enabled_value": "✅ Zapnuto",
"disabled_value": "❌ Vypnuto",
"settings_title": "🔐 Nastavení správy meta dat",
"settings_desc": "**Svaz:** {squadron}\n**ID klanu:** {clan_id}",
"first_time_title": "🔐 Správa meta dat — první nastavení",
"first_time_owner_desc": "**Svaz:** {squadron}\n**ID klanu:** {clan_id}\n\n🔑 Vaše přístupové heslo bylo vygenerováno. **Uložte si toto heslo** — budete ho potřebovat pro ověření přístupu k meta datům v budoucnu.\n\n**Heslo:** `{password}`",
"first_time_non_owner_desc": "**Svaz:** {squadron}\n**ID klanu:** {clan_id}\n\nSvaz byl nastaven. Požádejte vlastníka serveru o přístupové heslo.",
"settings_field": "Nastavení",
"settings_hint": "Pro konfiguraci nastavení přístupu použijte tlačítka níže.",
"password_toggled": "✅ Požadavek na heslo: **{state}**",
"lock_toggled": "✅ Připojení dat svazu: **{state}**",
"public_meta_toggled": "✅ Veřejný přístup k meta datům: **{state}**\n{detail}",
"public_meta_enabled_detail": "Nesprávcové nyní mohou používat příkaz `/meta`.",
"public_meta_disabled_detail": "Pouze správci mohou používat příkaz `/meta`.",
"owner_only_password": "❌ Heslo svazu může změnit pouze vlastník serveru.",
"help_title": "📖 Nápověda ke správě meta dat",
"help_desc": "Vysvětlení každého nastavení a funkce:",
"help_password_field": "🔑 Přístupové heslo",
"help_password_value": "Přístupové heslo vašeho svazu. Heslo v panelu nastavení vidí pouze **vlastník serveru**. Kdokoli s heslem může přijmout meta data vašeho svazu na svůj server, proto ho chraňte.",
"help_require_field": "🔒 Vyžadovat heslo",
"help_require_value": "Pokud je zapnuto, i správci na tomto serveru musí zadat heslo svazu pro přístup k `/meta-management`. Přidává další vrstvu zabezpečení proti nechtěným změnám.",
"help_lock_field": "🔐 Připojit data svazu",
"help_lock_value": "Pokud je zapnuto, připojí data svazu k tomuto serveru a zabrání přenosu i se správným heslem. Musí být vypnuto před přenosem svazu.",
"help_public_field": "👥 Povolit veřejná meta data",
"help_public_value": "Pokud je zapnuto, umožňuje nesprávcům používat příkaz `/meta` pro vyhledávání vozidel svazu. Pokud je vypnuto, příkaz `/meta` mohou používat pouze správci serveru.",
"help_accounts_field": "📋 Aktualizovat meta účty",
"help_accounts_value": "Otevře správce seznamu hráčů, kde můžete přidávat nebo odebírat hráče z meta seznamu svazu. Použijte **Aktualizovat všechny členy** pro synchronizaci celého svazu najednou.",
"help_change_pw_field": "🔑 Změnit heslo",
"help_change_pw_value": "**Pouze vlastník serveru.** Změňte přístupové heslo svazu a nastavte volitelnou nápovědu. Nápověda se zobrazí ve výzvě k zadání hesla, aby si ho bylo snazší zapamatovat.",
"password_modal_title": "Přístupové heslo svazu",
"password_modal_label": "Zadejte heslo svazu",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Změnit heslo svazu",
"current_password_label": "Aktuální heslo",
"current_password_placeholder": "Zadejte aktuální heslo",
"new_password_label": "Nové heslo",
"new_password_placeholder": "Zadejte nové heslo",
"confirm_password_label": "Potvrďte nové heslo",
"confirm_password_placeholder": "Zadejte nové heslo znovu",
"hint_label": "Nápověda k heslu (volitelné)",
"hint_placeholder": "Nápověda pro zapamatování hesla",
"pw_incorrect": "❌ Aktuální heslo je nesprávné.",
"pw_mismatch": "❌ Nová hesla se neshodují. Zkuste to prosím znovu.",
"pw_empty": "❌ Nové heslo nemůže být prázdné.",
"pw_changed": "✅ Heslo pro **{squadron}** bylo úspěšně aktualizováno.\n**Nové heslo:** `{password}`",
"pw_changed_hint": "\n**Nápověda:** {hint}",
"player_add_modal_title": "Přidat hráče do meta seznamu",
"player_add_label": "UID nebo přezdívka hráče",
"player_add_placeholder": "Zadejte UID hráče (např. 12345678) nebo přezdívku",
"player_not_found": "❌ Hráč `{player}` nebyl nalezen v databázi Players_Global.\n",
"roster_title": "📋 Správa meta seznamu — {squadron}",
"roster_desc": "**ID klanu svazu:** {clan_id}\n**Celkem hráčů:** {count}",
"roster_page_field": "Hráči (strana {page}/{total})",
"no_players_field": "Žádní hráči",
"no_players_hint": "Do meta seznamu zatím nebyli přidáni žádní hráči. Klikněte na **Přidat hráče** pro začátek.",
"remove_player_placeholder": "Vyberte hráče k odebrání...",
"fetch_members_failed": "❌ Nepodařilo se načíst členy svazu: {error}",
"no_members_found": "❌ Ve svazu nebyli nalezeni žádní členové nebo volání API selhalo.",
"roster_synced": "✅ Seznam byl synchronizován se svazem.",
"roster_added": "**+{count}** přidáno",
"roster_removed": "**-{count}** odebráno (opustilo svaz)",
"roster_up_to_date": "**{count}** již aktuální",
"refreshing_vehicles": "Aktualizuji data vozidel na pozadí..."
},
"meta": {
"not_configured": "❌ Meta data nejsou nastavena pro tento server. Nejprve spusťte `/meta-management`.",
"no_permission": "❌ Pro použití tohoto příkazu potřebujete oprávnění správce.\nSprávcové mohou veřejný přístup povolit přes `/meta-management`.",
"no_results": "❌ Žádný hráč ve vašem meta seznamu svazu nemá **{vehicle}**.",
"no_results_admin_hint": "\n*Čekáte, že to někdo má? Klikněte na tlačítko aktualizace členů v `/meta-management` a zkontrolujte to.*",
"search_title": "🔍 Výsledky hledání — {vehicle}",
"matches_found": "**Nalezené shody:** {count} hráč(ů)",
"spawns_label": "Nasazení",
"deaths_label": "Úmrtí",
"gk_label": "PZ",
"ak_label": "VZ",
"points_label": "Body",
"kdr_label": "KDR",
"games_label": "Hry",
"no_points": "—"
},
"top": {
"title": "**Top 20 svazů**",
"rating_label": "**Hodnocení:** {value}",
"air_kills_label": "**Vzdušná zabití:** {value}",
"ground_kills_label": "**Pozemní zabití:** {value}",
"deaths_label": "**Úmrtí:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Úspěšnost:** {value}",
"playtime_label": "**Herní čas:** {value}",
"fetch_failed": "Nepodařilo se načíst data svazu."
},
"analytics": {
"no_data_title": "Žádná data",
"no_matches_desc": "Žádné zápasy nenalezeny.",
"no_comp_desc": "Nenalezena žádná data o sestavách.",
"no_consistency_desc": "Nedostatek dat o hráčích (minimum 50 zápasů).",
"no_time_desc": "Nenalezena žádná časová data.",
"unknown_view": "Neznámý pohled.",
"map_title": "Úspěšnost na mapách: {squadron}",
"comp_title": "Týmové sestavy: {squadron}",
"consistency_title": "Konzistentnost hráčů: {squadron}",
"consistency_desc": "Seřazeno podle K/D",
"time_title": "Výkonnost podle denní doby: {squadron}",
"eu_timeslot": "\n**EU časový úsek**",
"na_timeslot": "\n**NA časový úsek**",
"off_peak": "\n**Mimo špičku**",
"matchups_title": "📜 {squadron} — Historie Soubojů",
"matchups_won_field": "🏆 Nejvíce Vítězství Proti",
"matchups_lost_field": "💀 Nejvíce Proher Od",
"no_matchups_desc": "Žádné zaznamenané zápasy proti jiným klanům."
},
"recent": {
"title": "Nedávné zápasy: {squadron}",
"no_matches_desc": "Pro tento svaz nebyly nalezeny žádné zápasy."
},
"h2h": {
"two_required_title": "Vyžadovány dva svazy",
"two_required_desc": "Zadejte alespoň jeden svaz nebo použijte `/set-squadron` a zadejte soupeře.",
"provide_a_desc": "Zadejte `squadron_a` nebo nejprve použijte `/set-squadron`.",
"provide_b_desc": "Zadejte `squadron_b` nebo nejprve použijte `/set-squadron`.",
"squadron_not_found_title": "Svaz nenalezen",
"same_squadron_title": "Stejný svaz",
"same_squadron_desc": "Nemůžete kontrolovat head-to-head sami proti sobě.",
"record_desc": "**Záznam:** {a_wins}V - {b_wins}P ({total} her)",
"no_matches_desc": "Žádné zaznamenané zápasy mezi **{a}** a **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Aktivní — autologování je na tomto serveru povoleno.",
"premium_not_subscribed_line": "❌ **Premium:** Nepřihlášeno k odběru — použijte `/unlock` pro zapnutí autologování.",
"premium_free_line": "⚪ **Premium:** Nepřihlášeno k odběru — použijte `/unlock` pro přihlášení (2,99 $/měs.). *(Autology jsou nyní zdarma pro všechny servery.)*",
"what_to_do": "\n\nCo byste chtěli udělat?",
"select_notif_type": "Vyberte typ oznámení ke správě:",
"select_notif_placeholder": "Vyberte typ oznámení",
"logs_option": "Logy",
"logs_option_desc": "Správa oznámení logů",
"points_option": "Body",
"points_option_desc": "Správa oznámení bodů",
"leaderboard_option": "Žebříček",
"leaderboard_option_desc": "Správa oznámení žebříčku",
"selected_type": "Vybráno **{type}**. Nyní zvolte svaz ke správě:",
"select_squadron_placeholder": "Vyberte svaz",
"select_squadron_page_placeholder": "Vyberte svaz (strana {page})",
"no_squadrons_available": "Pro tento typ oznámení není dostupná žádný svaz.",
"managing_global": "Spravuji **{type}** (globální) v kanálu **{channel}**.",
"managing_squadron": "Spravuji **{type}** pro svaz **{squadron}** v kanálu **{channel}**.",
"select_channel": "Vyberte nový kanál:",
"select_channel_placeholder": "Vyberte kanál",
"select_channel_page_placeholder": "Vyberte kanál (strana {page})",
"global_toggled": "{type} (globální) je nyní {state}.",
"squadron_toggled": "{type} pro **{squadron}** je nyní {state}.",
"channel_updated_global": "Aktualizováno {type} (globální) na {channel}",
"channel_updated_squadron": "Aktualizováno {type} pro **{squadron}** na {channel}",
"diagnose_channel_placeholder": "Vyberte kanál k diagnostice...",
"select_channel_diagnose": "Vyberte kanál k diagnostice:",
"game_not_logged_title": "Hra nezaznamenána",
"game_not_logged_desc": "Použijte `/unlock` pro předplatné tarifu **Standard** (nebo vyšší) a získejte automatické výsledkové tabulky.",
"server_not_upgraded_title": "⚠️ Server není upgradován",
"server_not_upgraded_autolog_desc": "Tento server nemá aktivní Premium předplatné.\n\n**Automatické herní výsledkové tabulky přestanou být odesílány na neupgradované servery po <t:{deadline}:D>.**\n\nPoužijte `/unlock` pro přihlášení k odběru a zachování automatických herních logů.",
"replay_not_available": "Data záznamu ještě nejsou k dispozici — chvíli počkejte a zkuste to znovu!",
"too_many_videos": "Právě se renderuje příliš mnoho videí — zkuste to prosím za chvíli.",
"video_gen_failed": "Chyba při generování videa: `{error}`",
"video_missing": "Nepodařilo se vygenerovat video záznamu — výstupní soubor chybí nebo je prázdný.",
"video_too_large": "Video záznamu je příliš velké pro nahrání ({file_mb:.1f} MB). Limit serveru je {limit_mb:.0f} MB.",
"video_web_fallback": "Tento zápas si také můžete prohlédnout na {url}",
"video_upload_failed": "Video je příliš velké pro nahrání — prohlédněte si ho na webu:\n{url}",
"video_unexpected_error": "Neočekávaná chyba při generování videa záznamu: `{error}`",
"replay_not_found": "Data záznamu nebyla nalezena pro sezení `{session_id}` na disku.",
"chat_log_title": "**Chat log hry [{session_id}]({url})**",
"chat_log_part_title": "**Chat log hry [{session_id}]({url}) (část {part}/{total})**",
"chat_log_part_only": "**Chat log (část {part}/{total})**",
"no_chat_log": "Pro sezení `{session_id}` nebyl nalezen žádný chat log.",
"chat_log_error": "Neočekávaná chyba při načítání chat logu: `{error}`",
"battle_log_title": "**Bojový log hry [{session_id}]({url})**",
"battle_log_part_title": "**Bojový log hry [{session_id}]({url}) (část {part}/{total})**",
"battle_log_part_only": "**Bojový log (část {part}/{total})**",
"no_battle_log": "Pro sezení `{session_id}` nebyly nalezeny žádné bojové události.",
"battle_log_error": "Neočekávaná chyba při načítání bojového logu: `{error}`",
"points_update_title": "**{squadron} {region} aktualizace bodů**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Změny hráčů:**",
"points_table_header": "Jméno Změna Nyní\n",
"wl_line": "\n**{squadron}** šla **{wins}V-{losses}P** v tomto sezení",
"placement_rose": "\n**{squadron}** stoupla na **{new_place}** z **{old_place}**",
"placement_fell": "\n**{squadron}** klesla na **{new_place}** z **{old_place}**",
"points_not_logged_title": "Body nezaznamenány",
"points_not_logged_desc": "Použijte `/unlock` pro předplatné tarifu **Standard** (nebo vyšší) a získejte automatické aktualizace bodů.",
"server_not_upgraded_points_desc": "Tento server nemá aktivní Premium předplatné.\n\n**Automatické aktualizace přestanou být odesílány na neupgradované servery po <t:{deadline}:D>.**\n\nPoužijte `/unlock` pro přihlášení k odběru a zachování automatických aktualizací.",
"leave_title": "⚠️ Hráč opustil {squadron}",
"leave_desc": "**{nick}** ({uid}) opustil svaz.\n\nPosledních zaznamenaných bodů: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Squadrona nad limitem vašeho tarifu",
"over_cap_desc": "Váš server má tarif **{tier}**, který umožňuje **{cap} {notif}** squadron. Squadrona **{squadron}** je nad limitem a nezaznamenává se. Upgradujte pro obnovení.",
"over_cap_footer": "Upgrade na srebot-meow.ing/premium nebo /unlock",
"wildcard_blocked_title": "Wildcard vyžaduje vyšší tarif",
"wildcard_blocked_desc": "Wildcard záznamy (*, all, everything) jsou dostupné pouze v Pro nebo Max. Váš server je na **{tier}** pro {notif}. Upgradujte pro povolení.",
"cap_header": "{used}/{cap} {notif} aktivních — tarif {tier}"
},
"track": {
"squadron_not_found": "Svaz nenalezen.",
"fetch_failed": "Nepodařilo se načíst informace o svazu."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Odemkněte prémiové funkce pro tento server.**\n\nPremium zahrnuje:\n> • Automatické odesílání výsledkových tabulek\n> • Chat a bojové logy\n> • Vyhledávání záznamů\n> • Neomezené vyhledávání /comp\n> • Prioritní podpora\n\n**2,99 $ / měsíc · za server · zrušení kdykoli**\n\n⚠️ Discord platby jsou dostupné pouze ve vybraných zemích. Pokud tlačítko níže zobrazuje **„Produkt není dostupný“**, může to být způsobeno nepodporovanou zemí nebo použitím **mobilního zařízení**. Použijte místo toho tlačítko **Přihlásit přes web**.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Tento server je již přihlášen k odběru!**",
"manage_discord_field": "Spravovat předplatné",
"manage_discord_value": "Vaše předplatné je přes **Discord**.\nPro zrušení přejděte do **Nastavení uživatele → Předplatná** v Discord.",
"manage_website_field": "Spravovat předplatné",
"manage_website_value": "Vaše předplatné je přes **web**.\nSpravujte ho na [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Brzy k dispozici",
"coming_soon_value": "Premium předplatná zatím nejsou dostupná. Sledujte nás!",
"current_tier": "Máte tarif **{tier}**.",
"upgrade_to": "Upgrade na {tier}",
"upgrade_to_value": "Více squadron a funkcí s tarifem **{tier}**."
},
"language": {
"prompt": "Vyberte prosím jazyk serveru:",
"select_placeholder": "Zvolte jazyk serveru",
"language_set": "Jazyk nastaven na {language}.",
"translate_prompt": "Níže vyber cílový jazyk 👇",
"translate_placeholder": "Zvolte cílový jazyk…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Překlad není k dispozici (DeepL není nakonfigurován)",
"translation_failed": "Překlad selhal"
},
"misc": {
"credits_title": "Poděkování",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Vedoucí vývojář, správce bota, správce komunity\n> **Z3R0** - Vývojář, optimalizační vývojář, databázový inženýr\n> **Clippii (Heidi)** - Vývojář, webový vývojář, správce komunity\n> **LivingTheDagor** - Vývojář, vývojář parseru, konzultant\n> **Lux_** - API inženýr, vývojář Spectry\n> **Konigallerwaffen** - Konzultant pro zpětnou vazbu a funkce\n> **Žralok Tonda** - Český překladatel\n> **Styevy**, **Lopais** - Němečtí překladatelé\n> **Susogus**, **playforfun698** - Polští překladatelé\n> **Bobr** - Ruský překladatel\n\n\n[Chcete se přidat?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "ROZVRH SEZÓNY",
"schedule_not_found_title": "Rozvrh nenalezen",
"schedule_not_found_desc": "Žádná data rozvrhu zatím nejsou k dispozici.",
"news_no_news_title": "Žádné novinky",
"news_no_news_desc": "Momentálně nejsou žádná oznámení. Sledujte nás!",
"news_footer": "Díky za vaši podporu! ᖙᘘᗢ",
"help_title": "Průvodce botem",
"donate_title": "Podpořte SRE Bot",
"donate_desc": "Pokud vám SRE Bot přináší radost a chcete podpořit jeho vývoj, zvažte koupi kávy!\n\n**[Přispějte na Ko-fi](https://ko-fi.com/notsotoothless)**\n\nKaždý příspěvek pomáhá udržovat bota v provozu a podporuje nové funkce. Díky!",
"status_title": "Stav bota",
"status_last_received": "Poslední přijatá hra",
"status_avg_ttl": "Průměrné TTL (posledních 30)",
"status_no_data": "Zatím žádná data",
"status_gaijin_slow": "⚠️ Gaijin servery jsou pomalé",
"help_commands_header": "**Přehled příkazů**",
"help_links": "Podrobnosti najdeš v dokumentaci [zde]({docs}) nebo na podpoře [zde]({support}).",
"help_terms": "[Podmínky služby]({terms}) • [Zásady ochrany soukromí]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Žebříček svazu",
"top15_desc": "Top 15 svazů se statistikami, odesíláno 35 minut po zavření časového úseku.\nToto odesláno <t:{timestamp}:R>.",
"top30_desc": "Svazy 1630 se statistikami.",
"not_logged_title": "Žebříček nezaznamenán",
"not_logged_desc": "Použijte `/unlock` pro předplatné tarifu **Standard** (nebo vyšší) a získejte automatické aktualizace žebříčku.",
"server_not_upgraded_title": "⚠️ Server není upgradován",
"server_not_upgraded_desc": "Tento server nemá aktivní Premium předplatné.\n\n**Automatické aktualizace přestanou být odesílány na neupgradované servery po <t:{deadline}:D>.**\n\nPoužijte `/unlock` pro přihlášení k odběru a zachování automatických aktualizací."
},
"stacks": {
"stack_title": "Stack hráče {leader}",
"stack_named_title": "{name}",
"no_members": "Zatím žádní členové.",
"members_field": "Členové ({count}/{max})",
"queue_field": "Fronta ({count}/{max})",
"manage_title": "Správa stacku",
"no_pending_requests": "Žádné nevyřízené žádosti.",
"disbanded_title": "Stack [Rozpuštěn]",
"disbanded_desc": "Tento stack byl rozpuštěn vedoucím.",
"expired_title": "Stack [Vypršel]",
"expired_desc": "Tento stack vypršel.",
"join_modal_title": "Žádost o vstup do stacku",
"join_vehicle_label": "S čím budeš hrát?",
"join_vehicle_placeholder": "např. F-16C, WZ305...",
"ping_modal_title": "Zpráva pingu",
"ping_message_label": "Vlastní zpráva (nepovinné)",
"ping_message_placeholder": "např. Pojďte! Stack začíná!",
"rename_modal_title": "Přejmenovat stack",
"rename_label": "Název stacku",
"rename_placeholder": "např. Noční sovy, Alfa tým...",
"select_new_leader": "Vyberte nového vedoucího…",
"select_applicants": "Vyberte žadatele…",
"no_pending_applications": "Žádné nevyřízené žádosti.",
"select_to_remove": "Vyberte osoby k odebrání…",
"no_members_or_applicants": "Žádní členové ani žadatelé.",
"select_to_ping": "Vyberte osoby k individuálnímu pingu…",
"stack_not_found": "❌ Stack nenalezen.",
"no_longer_exists": "❌ Tento stack již neexistuje.",
"member_not_exists": "❌ Tento člen již neexistuje.",
"already_has_stack": "❌ Tento hráč již má aktivní stack.",
"already_member": "❌ Již jste členem tohoto stacku.",
"already_applied": "❌ Již máte nevyřízenou žádost o tento stack.",
"queue_full": "❌ Fronta je plná ({max}/{max}). Zkuste to později.",
"application_sent": "✅ Žádost odeslána! Vedoucí stacku ji posoudí.",
"stack_disbanded": "✅ Stack rozpuštěn.",
"cancelled": "Zrušeno.",
"select_member_transfer": "❌ Vyberte člena, na kterého chcete převést vedení.",
"ownership_transferred": "✅ Vedení převedeno na {nick}. Opustili jste stack.",
"select_applicant_first": "❌ Nejprve vyberte alespoň jednoho žadatele.",
"stack_full": "❌ Stack je již plný ({max}/{max} členů).",
"select_person_first": "❌ Nejprve vyberte alespoň jednu osobu.",
"no_one_to_ping": "❌ Není koho pingnout.",
"ping_footer": "Pingnul {leader} pro {stack}.",
"pinged": "✅ Pingnuto!",
"select_from_dropdown": "❌ Nejprve vyberte alespoň jednu osobu z rozbalovací nabídky.",
"stack_renamed": "✅ Stack přejmenován na **{name}**.",
"only_member_use_disband": "❌ Jste jediný člen. Použijte **Rozpustit stack** k ukončení.",
"select_transfer_prompt": "Vyberte člena, na kterého chcete převést vedení před odchodem:",
"left_stack": "✅ Opustili jste stack.",
"application_withdrawn": "✅ Vaše žádost byla stažena.",
"not_member_or_applicant": "❌ Nejste členem ani žadatelem tohoto stacku.",
"leader_only_manage": "❌ Pouze vedoucí stacku může spravovat tento stack.",
"leader_only_disband": "❌ Pouze vedoucí stacku může rozpustit tento stack.",
"confirm_disband": "Opravdu chcete rozpustit tento stack? Tuto akci nelze vrátit zpět.",
"already_active_stack": "⚠️ Již máte aktivní stack. Pokud původní zpráva zmizela (např. po restartu bota), můžete vynutit rozpuštění a začít znovu.",
"force_created": "✅ Předchozí stack rozpuštěn. Nový stack vytvořen.",
"no_active_stack": "❌ Nemáte aktivní stack. Použijte `/stack-create` k vytvoření.",
"could_not_parse_channel": "⚠️ Nelze zpracovat uložené ID kanálu."
},
"commands": {
"common": {
"season": "Sezóna pro vygenerování karty",
"theme": "Barevné téma karty",
"squadron_short": "Krátký název svazu",
"player_username": "Jméno hráče",
"choice_dark": "Tmavé",
"choice_light": "Světlé"
},
"comp": {
"description": "Najít poslední známé sestavy týmu",
"squadron_short": "Krátký název nepřátelského týmu"
},
"quick_log": {
"description": "Nastavit upozornění pro tento svaz v tomto kanálu",
"squadron_name": "KRÁTKÝ název svazu ke sledování",
"type": "Vyberte Logy, Body, Žebříček, Týdenní BR nebo Oba",
"choice_logs": "Logy",
"choice_points": "Body",
"choice_leaderboard": "Žebříček",
"choice_both": "Obojí (Logy + Body)",
"choice_weekly_br": "Týdenní BR"
},
"sq_info": {
"description": "Získat informace o svazu"
},
"sq_info_graph": {
"description": "Zobrazit graf složení svazu podle aktivity a procenta výher (aktuální sezóna)"
},
"sq_card": {
"description": "Vygenerovat sezónní kartu pro svaz",
"squadron": "Krátký název svazu"
},
"sq_stats": {
"description": "Zobrazit body svazu v čase"
},
"loss_calculator": {
"description": "Spočítat ztrátu bodů při odchodu hráčů ze svazu",
"player1": "Odcházející hráč",
"player_optional": "Odcházející hráč (volitelné)"
},
"website": {
"description": "Získat odkaz na web SRE Bot"
},
"card": {
"description": "Vygenerovat sezónní kartu pro hráče"
},
"player_stats": {
"description": "Zobrazit podrobné statistiky vozidel hráče",
"username": "WT jméno pro statistiky",
"uid": "WT UID pro statistiky"
},
"view_player_games": {
"description": "Zobrazit posledních 20 her hráče"
},
"view_match": {
"description": "Zobrazit skóre zápasu podle ID nebo hráče",
"match_id": "Hex ID session zápasu",
"player_name": "Hráč pro procházení posledních zápasů"
},
"compare": {
"description": "Porovnat souhrnné SQB statistiky hráčů",
"player1": "První hráč",
"player2": "Druhý hráč",
"player_optional": "Další hráč (volitelné)"
},
"leaderboard": {
"description": "Získat globální žebříček SRE Bot"
},
"set_squadron": {
"description": "Nastavit tag svazu pro tento server",
"abbreviated_name": "Krátký název svazu k nastavení"
},
"setup": {
"description": "Nastavit bota pro tento server"
},
"meta_management": {
"description": "Spravovat přístup k meta datům tohoto serveru"
},
"meta": {
"description": "Hledat v meta soupisce podle názvu vozidla",
"vehicle": "Název vozidla k hledání"
},
"top": {
"description": "Zobrazit top 20 svazů s detailními statistikami"
},
"language": {
"description": "Změnit jazyk bota."
},
"translate_message": {
"name": "Přeložit zprávu"
},
"sq_track": {
"description": "Sledovat svaz a porovnat s poslední kontrolou",
"squadron_short_name": "Krátký název sledovaného svazu"
},
"analytics": {
"description": "Zobrazit pokročilé SQB analýzy svazu",
"view": "Který analytický pohled zobrazit",
"choice_maps": "Win rate map",
"choice_comps": "Týmové sestavy",
"choice_consistency": "Stálost hráčů",
"choice_time": "Denní doba",
"choice_matchups": "Historie soubojů"
},
"recent": {
"description": "Zobrazit nedávné svazové bitvy",
"length": "Počet zápasů k zobrazení"
},
"vs": {
"description": "Vzájemná bilance dvou svazů",
"squadron_a": "První svaz",
"squadron_b": "Druhý svaz"
},
"autolog_management": {
"description": "Spravovat autolog upozornění a diagnostiku oprávnění"
},
"diagnose_perms": {
"description": "Diagnostikovat autolog oprávnění v tomto kanálu"
},
"unlock": {
"description": "Odemknout Premium funkce pro tento server"
},
"credits": {
"description": "Zobrazit tým stojící za tímto projektem"
},
"schedule": {
"description": "Zobrazit aktuální sezónní BR plán"
},
"news": {
"description": "Zobrazit nejnovější zprávy a oznámení SRE Bot"
},
"help": {
"description": "Zobrazit průvodce, ToS a odkazy podpory"
},
"donate": {
"description": "Podpořit vývoj SRE Bot"
},
"stack_create": {
"description": "Vytvořit hráčský stack",
"vehicle": "S jakým vozidlem začneš?"
},
"stack_manage": {
"description": "Znovu poslat aktivní stack do tohoto kanálu"
},
"bot_status": {
"description": "Zobrazit stav bota: poslední přijatá hra a průměrné TTL"
}
},
"permission": {
"blacklisted_title": "❌ Blokováno",
"blacklisted_desc": "Tento příkaz nemůžeš použít, protože jsi na blacklistu.",
"reason_line": "**Důvod:** {reason}",
"access_denied_title": "⛔ Přístup odepřen",
"no_permission_desc": "Nemáš oprávnění použít tento příkaz.",
"unexpected_error_title": "❗ Chyba, nahlas ji...."
},
"weekly_br": {
"title_wildcard": "Týdenní zpráva BR — {br} BR",
"title_squadron": "Týdenní zpráva BR — [{tag}] {long} • {br} BR",
"window_label": "Období: {start} → {end}",
"wildcard_desc_first": "Top {count} letek podle ELO • Pozice {low}{high}",
"wildcard_desc_second": "Top {count} letek podle ELO • Pozice {low}{high}",
"squadron_stats_line": "- {games} bitev • K/D {kdr} • Vítězství {wr}%",
"top_players_inline_header": "🥇 Nejlepší hráči:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}b)",
"top_players_header": "**Top {count} hráčů podle ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} bitev • K/D {kdr}",
"squadron_header_line": "ELO letky: {score} • {games} bitev • Vítězství {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO letky: tento týden nedostatek týmové aktivity.",
"no_data": "Žádné zápasy pro [{tag}] v této rotaci BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Fehler",
"no_data_title": "Keine Daten",
"access_denied_title": "Zugriff verweigert",
"access_denied_desc": "Dieser Server wurde gesperrt.",
"no_players_selected": "Keine Spieler ausgewählt. Bitte mindestens einen Spieler auswählen.",
"must_use_in_server": "Dieser Befehl muss auf einem Server verwendet werden.",
"could_not_resolve_channel": "Der ausgewählte Kanal konnte nicht gefunden werden.",
"failed_update_setting": "❌ Einstellung konnte nicht aktualisiert werden.",
"configuration_not_found": "Konfiguration nicht gefunden.",
"no_channel_selected": "Kein Kanal ausgewählt.",
"no_selection_received": "Keine Auswahl erhalten.",
"database_error": "❌ Datenbankfehler: {error}",
"enabled": "Aktiviert",
"disabled": "Deaktiviert",
"not_configured": "Nicht konfiguriert",
"unknown": "Unbekannt",
"rating_field": "Wertung",
"battles_field": "Gefechte",
"wins_field": "Siege",
"losses_field": "Niederlagen",
"win_rate_field": "Siegrate",
"kills_field": "Abschüsse",
"deaths_field": "Tode",
"kd_field": "K/D",
"members_field": "Mitglieder",
"placement_field": "Platzierung",
"points_field": "Punkte",
"ground_kills_field": "Bodenabschüsse",
"air_kills_field": "Luftabschüsse",
"total_kills_field": "Abschüsse gesamt",
"assists_field": "Unterstützungen",
"captures_field": "Eroberungen",
"none_option": "Keine"
},
"buttons": {
"skip": "Überspringen",
"previous": "Zurück",
"next": "Weiter",
"prev": "Zurück",
"prev_arrow": "◀ Zurück",
"next_arrow": "Weiter ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Diagramm erstellen",
"show_graph": "Grafik anzeigen",
"view_player_stats": "📊 Spielerstatistiken anzeigen",
"compare_nearby": "📈 mit umliegenden Kampfgruppen vergleichen",
"confirm_swap": "Ja, wechseln",
"cancel_swap": "Nein, behalten",
"set_squadron": "Kampfgruppe festlegen",
"same_as_logs": "Selber Channel wie die Logs",
"require_password": "🔒 Passwort erforderlich",
"password_required": "🔒 Passwort erforderlich",
"lock_data": "🔐 Kampfgruppendaten an diesen Server binden",
"data_locked": "🔐 Daten an diesen Server binden",
"allow_public": "👥 Öffentliche Metalisten-Daten erlauben",
"public_enabled": "👥 Öffentliche Metalisten-Daten aktiv",
"update_accounts": "📋 Konten für Meta-Liste aktualisieren",
"change_password": "🔑 Passwort ändern",
"help": "❓ Hilfe",
"add_player": " Spieler hinzufügen",
"update_all": "🔄 Alle Mitglieder aktualisieren",
"back_to_settings": "⬅ Zurück zu den Einstellungen",
"manage_notifications": "Benachrichtigungen verwalten",
"diagnose_permissions": "Berechtigungen ansehen",
"enable": "Aktivieren",
"disable": "Deaktivieren",
"change_channel": "Kanal ändern",
"view_replay": "Replay anzeigen",
"view_website": "Auf Website anzeigen",
"view_video": "Video anzeigen",
"view_log": "Protokoll anzeigen",
"view_chat": "Chat anzeigen",
"subscribe_website": "Über Website abonnieren",
"yes_disband": "Ja, bitte auflösen",
"cancel": "Abbrechen",
"transfer_leave": "Übertragen & Verlassen",
"accept_selected": "Ausgewählte annehmen",
"accept_all": "Alle annehmen",
"decline_selected": "Ausgewählte ablehnen",
"back": "Zurück",
"remove_all": "Alle entfernen",
"remove_active": "Aktive entfernen",
"remove_queued": "Wartende entfernen",
"remove_selected": "Ausgewählte entfernen",
"ping_all": "Alle anpingen",
"ping_active": "Aktive anpingen",
"ping_queued": "Wartende anpingen",
"ping_selected": "Ausgewählte anpingen",
"accept_members": "Mitglieder annehmen",
"remove_members": "Mitglieder entfernen",
"ping_members": "Mitglieder anpingen",
"rename_stack": "Staffel umbenennen",
"request_to_join": "Beitritt anfragen",
"leave_withdraw": "Verlassen / Zurückziehen",
"manage_stack": "Staffel verwalten ⚙️",
"disband_stack": "Staffel auflösen",
"force_disband_create": "Auflösung erzwingen & Neu erstellen"
},
"events": {
"guild_join_title": "Danke, dass du mich hinzugefügt hast!",
"guild_join_desc": "Führe `/setup` aus, um den Bot für diesen Server zu konfigurieren."
},
"comp": {
"not_found_title": "Aufstellung nicht gefunden",
"not_found_desc": "Keine Daten für **{squadron}**, versuche es später erneut.",
"error_loading_title": "Fehler beim Laden der Aufstellungen",
"error_loading_desc": "Aufstellungs-Daten konnten nicht geladen werden: {error}",
"title": "Aufstellungen für {squadron}",
"desc": "Aufstellungen der letzten {minutes} Minuten",
"no_recent_title": "Keine aktuellen Aufstellungen",
"no_recent_desc": "Keine Aufstellungen in den letzten {minutes} Minuten.",
"comp_title": "Aufstellung {index}",
"last_seen_label": "**Zuletzt gesehen** : {timestamp}{warning}",
"comp_label": "**Aufstellung**: {notation}",
"no_players_recorded": "Keine Spieler erfasst.",
"limit_reached_title": "Aufstellungslimit erreicht",
"limit_reached_desc": "Dieser Server hat alle {limit} Aufstellungsabfragen für diesen Zeitslot verbraucht. Abonniere (mit /unlock) für unbegrenzten Zugang oder warte auf den nächsten Zeitslot.",
"remaining_footer": "{remaining}/{limit} Aufstellungsabfragen übrig in diesem Zeitslot"
},
"quick_log": {
"invalid_type": "Typ kann nur auf Logs, Punkte, Leaderboard, Wöchentlicher BR oder Beide gesetzt werden.",
"squadron_required": "Du musst einen Kampfgruppennamen für Logs-, Punkte- oder Beide-Alarme angeben.",
"wildcard_logs_only": "Nur Logs können auf Platzhalter-Kampfgruppe gesetzt werden.",
"squadron_not_resolved": "Kampfgruppe `{squadron}` konnte nicht gefunden werden.",
"save_failed": "Einstellungen konnten nicht gespeichert werden. Bitte versuche es später erneut.",
"premium_warning": "\n\n> ⚠️ **Spielprotokolle erfordern Premium.** Führe `/unlock` aus, um zu abonnieren ($2.99/Monat) — Protokolle werden erst dann gepostet.",
"leaderboard_set": "Globaler Ranglisten-Alarm wurde auf diesen Kanal gesetzt.",
"both_set": "Logs- und Punkte-Alarme für {squadron} wurden auf diesen Kanal gesetzt.{premium_note}",
"alarm_set": "{alarm_type}-Alarm für {squadron} wurde auf diesen Kanal gesetzt.{premium_note}",
"weekly_br_wildcard_set": "Wöchentlicher BR-Bericht (Top 20 Geschwader) für diesen Kanal aktiviert. Wird am Ende jeder BR-Rotation gesendet.",
"weekly_br_squadron_set": "Wöchentlicher BR-Bericht für {squadron} (Top 15 Spieler) für diesen Kanal aktiviert. Wird am Ende jeder BR-Rotation gesendet."
},
"diagnostics": {
"title": "Autolog-Diagnose",
"channel_permissions_header": "**Kanalberechtigungen** (<#{channel_id}>)",
"perms_needed": " ^ Autologging benötigt alle oben genannten Berechtigungen zum Senden von Ergebnistabellen.",
"server_squadron_header": "**Server-Kampfgruppe** (`/set-squadron`)",
"server_squadron_short": " Kurz: `{short}`",
"server_squadron_long": " Lang: `{long}`",
"server_squadron_not_set": " Nicht gesetzt (Ergebnistabellenfarbe wird als 'nicht gesetzt' angezeigt)",
"autolog_prefs_header": "**Autolog-Einstellungen** (`/quick-log`)",
"autolog_none_configured": " ❌ NICHTS konfiguriert - Autologging sendet NICHTS an diesen Server.",
"autolog_setup_hint": " Verwende `/quick-log <squadron_short> Logs` im Zielkanal zum Einrichten.",
"autolog_no_logs_channels": " ❌ Keine Logs-Kanäle konfiguriert. Nur Punkte/Rangliste gefunden.",
"autolog_enable_hint": " Verwende `/quick-log <squadron_short> Logs` um Autologging zu aktivieren.",
"selected_channel_tag": " **(ausgewählter Kanal)**",
"missing_send_attach": " (Senden/Anhängen fehlt)",
"channel_not_found": " (Kanal nicht gefunden)",
"invalid_channel_id": " (ungültige Kanal-ID)",
"premium_status_header": "**Premium-Status** (`/unlock`)",
"premium_active": " ✅ Dieser Server hat ein aktives Premium-Abonnement.",
"premium_not_subscribed": " ❌ Dieser Server hat **kein** Premium-Abonnement.",
"premium_autolog_required": " Autologging erfordert Premium. Verwende `/unlock` zum Abonnieren ($2.99/Monat).",
"premium_not_subscribed_free": " ⚪ Nicht abonniert — verwende `/unlock` zum Abonnieren ($2.99/Monat).",
"premium_free_note": " *(Autologs sind derzeit für alle Server kostenlos.)*"
},
"sq_info": {
"title": "Kampfgruppe-Info: {squadron}",
"placement_field": "Platzierung",
"total_points_field": "Gesamtpunkte",
"total_members_field": "Mitglieder gesamt",
"members_field": "Mitglieder",
"fetch_failed": "Kampfgruppeninformationen konnten nicht abgerufen werden."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Saison {season})",
"embed_title": "{squadron} — Aufstellung",
"embed_desc": "Saison **{season}** · Median Spiele: **{median}** · Kern: **{core}** · Aktiv: **{active}** · Schwach: **{weak}**\nSäulen absteigend nach Spielen sortiert; Höhe = Siegrate. Kern = obere 30 % SR und Spiele ≥ Median. Aktiv = obere 3045 % SR und Spiele ≈ Median. Schwach = alle anderen.",
"core_threshold_line": "KERN ≥ {wr} %",
"weak_threshold_line": "SCHWACH < {wr} %",
"y_label": "Siegrate",
"core_header": "KERN — {count} · SR {avg}%",
"active_header": "AKTIV — {count} · SR {avg}%",
"weak_header": "SCHWACH — {count} · SR {avg}%",
"no_active_season": "Keine aktive Saison gefunden. Bitte erneut versuchen, sobald die nächste beginnt.",
"no_members": "Keine aktuellen Mitglieder für {squadron} gefunden."
},
"recap_card": {
"unknown_season": "Unbekannte Saison: `{season}`.",
"no_clan_id": "Kampfgruppen-ID für `{squadron}` konnte nicht ermittelt werden.",
"render_failed": "Die Saison-Rückblick-Karte konnte nicht erstellt werden. Bitte später erneut versuchen."
},
"sq_stats": {
"no_data_title": "Keine Daten",
"no_data_desc": "Keine historischen Daten für Kampfgruppe: {squadron} gefunden",
"title": "{squadron} // Kampfgruppe",
"desc": "Gesamtpunktverlauf (Letzte {count} Datenpunkte)",
"previous_score_field": "Vorheriger Punktestand",
"current_score_field": "Aktueller Punktestand",
"change_field": "Änderung",
"player_title": "{squadron} // SPIELER",
"player_desc": "Individuelle Spielerpunktetrends",
"comparison_title": "{squadron} // RANGLISTEN-VERGLEICH",
"comparison_desc": "Vergleich mit Kampfgruppen auf den Plätzen {range}",
"current_position_field": "Aktuelle Position",
"squadrons_shown_field": "Angezeigte Kampfgruppe",
"squadron_not_found_error": "Kampfgruppe nicht in der Rangliste gefunden",
"no_nearby_error": "Keine umliegenden Kampfgruppen gefunden",
"no_historical_error": "Keine historischen Daten für nahe Kampfgruppen gefunden",
"comparison_chart_failed": "Vergleichsdiagramm konnte nicht erstellt werden",
"select_players_placeholder": "Spieler auswählen (Seite {page})"
},
"loss_calc": {
"title": "Punktverlust — {squadron}",
"players_leaving_field": "Austretende Spieler",
"share_of_total_field": "% Anteil am Gesamt",
"points_lost_real_field": "Verlorene Punkte (Real)",
"points_lost_raw_field": "Verlorene Punkte (Roh)",
"squadron_rating_field": "Kampfgruppenwertung",
"squadron_position_field": "Kampfgruppenposition",
"positions_lost_field": "Verlorene Plätze",
"not_found_footer": "Nicht in Kampfgruppe gefunden: {players}",
"fetch_failed": "Kampfgruppendaten konnten nicht abgerufen werden: {error}",
"no_point_data": "Keine Punktedaten für diese Kampfgruppe verfügbar.",
"no_matching_players": "Keine passenden Spieler in **{squadron}** gefunden."
},
"player": {
"select_player_placeholder": "Spieler auswählen",
"no_stats_found": "❌ Keine Statistiken für UID: {uid} gefunden",
"no_vehicle_stats": "❌ Keine Fahrzeugstatistiken für diesen Spieler gefunden.",
"vehicles_found": "**{count}** Fahrzeuge für **{nick}** gefunden\nFahrzeug auswählen um detaillierte Statistiken anzuzeigen:",
"vehicle_select_placeholder": "Fahrzeug auswählen (Seite {page}/{total})",
"combat_stats_header": "**__KAMPFSTATISTIKEN__**",
"ground_kills_label": "**Bodenabschüsse:** {value}",
"air_kills_label": "**Luftabschüsse:** {value}",
"total_kills_label": "**Abschüsse gesamt:** {value}",
"assists_label": "**Unterstützungen:** {value}",
"deaths_label": "**Tode:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Eroberungen:** {value}",
"battle_record_header": "**__GEFECHTSREKORD__**",
"total_battles_label": "**Gefechte gesamt:** {value}",
"wins_label": "**Siege:** {value}",
"losses_label": "**Niederlagen:** {value}",
"win_rate_label": "**Siegrate:** {value}%",
"stats_desc": "Statistiken für **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Spieler nicht gefunden",
"not_found_desc": "Kein Spielverlauf für `{player}` gefunden.",
"no_players_found": "Keine Spieler gefunden, die **{username}** entsprechen\nVersuche `/website` zur Suche auf der Website.",
"multiple_matches": "Mehrere Treffer gefunden, wähle den richtigen unten aus:",
"must_provide_input": "Du musst mindestens eine UID oder einen Benutzernamen angeben."
},
"player_games": {
"no_recent_title": "Keine aktuellen Spiele",
"no_recent_desc": "Keine Spiele für **{player}** in den letzten 8 Stunden gefunden.",
"squadron_label": "**Kampfgruppe:** {squadron}",
"record_label": "**S:** {wins} **N:** {losses} **SR:** {wr}%",
"comps_played_header": "\n\n**Gespielte Comps**"
},
"match": {
"missing_input_title": "Fehlende Eingabe",
"missing_input_desc": "Gib entweder eine `match_id` oder einen `player_name` an.",
"not_found_title": "Gefecht nicht gefunden",
"not_found_desc": "Kein Gefecht mit ID `{match_id}` gefunden.",
"invalid_data_title": "Ungültige Gefechts-Daten",
"invalid_data_desc": "Die Replay-Daten konnten nicht verarbeitet werden.",
"scoreboard_error_title": "Ergebnistabellen-Fehler",
"scoreboard_error_desc": "Die Ergebnistabelle konnte nicht erstellt werden.",
"no_games_title": "Keine Spiele gefunden",
"no_games_desc": "Kein Spielverlauf für **{player}** gefunden.",
"recent_matches_title": "Aktuelle Gefechte für {player}",
"recent_matches_desc": "Zeige bis zu {count} aktuelle Spiele. Eines auswählen, um die vollständige Ergebnistabelle anzuzeigen.",
"select_match_placeholder": "Gefecht zum Anzeigen auswählen..."
},
"compare": {
"no_players_found": "Keine Spieler gefunden, die **{name}** entsprechen.",
"multiple_matches": "Mehrere Treffer für **{name}**: {matches}\nBitte einen spezifischeren Namen verwenden (die Autovervollständigungs-Vorschläge sind exakt).",
"could_not_resolve": "Spieler konnten nicht aufgelöst werden.",
"could_not_fetch": "❌ Statistiken für **{name}** konnten nicht abgerufen werden.",
"no_graph_data": "Keine Daten für die letzten 90 Tage verfügbar.",
"no_squadron_points_data": "Keine Kampfgruppenpunkte-Daten für {names} (Spieler nicht im verfolgten Kampfgruppenverlauf gefunden).",
"graph_title": "Spielerpunkte — Letzte 90 Tage",
"battles_label": "Gefechte",
"wins_label": "Siege",
"losses_label": "Niederlagen",
"win_rate_label": "Siegrate",
"ground_kills_label": "Bodenabschüsse",
"air_kills_label": "Luftabschüsse",
"total_kills_label": "Abschüsse gesamt",
"assists_label": "Unterstützungen",
"deaths_label": "Tode",
"kd_label": "K/D",
"captures_label": "Eroberungen"
},
"squadron": {
"not_found_desc": "Kampfgruppe `{squadron}` nicht gefunden.",
"set_title": "✅ Kampfgruppe gesetzt",
"set_desc": "Kampfgruppe **{squadron}** wurde für diesen Server gesetzt.",
"short_name_field": "Kurzname",
"long_name_field": "Langname",
"swap_title": "✅ Kampfgruppe gewechselt",
"swap_desc": "**{old}** wurde durch **{new}** für diesen Server ersetzt.",
"already_set_title": "⚠️ Kampfgruppe bereits gesetzt",
"already_set_desc": "Dieser Server ist derzeit auf **{old}** eingestellt.\nAuf **{new}** wechseln?",
"swap_cancelled": "❌ Kampfgruppenwechsel abgebrochen."
},
"setup": {
"step1_title": "Server-Einrichtung — Schritt 1 von 3",
"step1_desc": "Dieser Assistent führt dich durch die Konfiguration des Bots für deinen Server.\n\n**Schritt 1** — Kampfgruppe festlegen\n**Schritt 2** — Logs-Kanal auswählen\n**Schritt 3** — Punkte-Kanal auswählen\n",
"step1_current_sq": "\nAktuell konfigurierte Kampfgruppe: **[{short}] {long}**",
"step2_title": "Server-Einrichtung — Schritt 2 von 3",
"step2_desc": "Kampfgruppe auf **[{short}] {long}** gesetzt.\n\nWo sollen **Gefechtsprotokolle** gepostet werden?\nUnten einen Textkanal auswählen oder diesen Schritt überspringen.",
"step3_title": "Server-Einrichtung — Schritt 3 von 3",
"step3_desc": "Wo sollen **Punkte-Benachrichtigungen** gepostet werden?\nUnten einen Textkanal auswählen oder diesen Schritt überspringen.",
"step3_same_as_logs": "\n\nDu kannst auch auf \"Wie Logs\" klicken, um den Logs-Kanal wiederzuverwenden.",
"summary_title": "Einrichtung abgeschlossen",
"summary_desc": "Du kannst `/autolog-management` verwenden, um diese Einstellungen später zu ändern.",
"squadron_field": "Kampfgruppe",
"logs_channel_field": "Logs-Kanal",
"points_channel_field": "Punkte-Kanal",
"premium_required_field": "⚠️ Spielprotokolle erfordern Premium",
"premium_required_value": "Automatische Spielergebnistabellen werden erst gepostet, wenn dieser Server ein aktives Abonnement hat. Führe `/unlock` aus, um zu abonnieren ($2.99/Monat).",
"modal_title": "Kampfgruppe festlegen",
"modal_label": "Kampfgruppe-Kurzname",
"modal_placeholder": "z.B. AXYS",
"squadron_not_found": "Kampfgruppe `{squadron}` nicht gefunden. Bitte erneut versuchen.",
"logs_channel_placeholder": "Logs-Kanal auswählen...",
"points_channel_placeholder": "Punkte-Kanal auswählen..."
},
"meta_management": {
"squadron_not_found_title": "❌ Kampfgruppe nicht gefunden",
"squadron_not_found_desc": "Clan-ID für Kampfgruppe **{squadron}** konnte nicht gefunden werden",
"access_denied_title": "❌ Zugriff verweigert",
"access_denied_desc": "Falsches Passwort. Die Meta-Daten dieser Kampfgruppe sind geschützt.",
"data_locked_title": "🔐 Kampfgruppendaten gebunden",
"data_locked_desc": "**{squadron}** hat die Datenbindung aktiviert und kann nicht auf einen anderen Server übertragen werden.\n\nDer Kampfgruppeninhaber muss **Kampfgruppendaten binden** deaktivieren, bevor es übertragen werden kann.",
"error_retrieving_settings": "❌ Fehler beim Abrufen der Server-Einstellungen nach der Übertragung. Bitte erneut versuchen.",
"error_retrieving_settings_retry": "❌ Fehler beim Abrufen der Server-Einstellungen. Bitte den Befehl erneut ausführen.",
"authenticated_title": "✅ Authentifiziert",
"authenticated_desc": "Passwort bestätigt. Einstellungen für **{squadron}** werden verwaltet.",
"claimed_title": "✅ Kampfgruppe beansprucht",
"claimed_desc": "**{squadron}** wurde erfolgreich für diesen Server beansprucht!",
"password_requirement_field": "🔒 Passwortpflicht",
"data_lock_field": "🔐 Kampfgruppendatenbindung",
"public_meta_field": "👥 Öffentlicher Meta-Zugriff",
"access_password_field": "🔑 Zugriffspasswort",
"enabled_value": "✅ Aktiviert",
"disabled_value": "❌ Deaktiviert",
"settings_title": "🔐 Meta-Verwaltungs-Einstellungen",
"settings_desc": "**Kampfgruppe:** {squadron}\n**Clan-ID:** {clan_id}",
"first_time_title": "🔐 Meta-Verwaltung - Ersteinrichtung",
"first_time_owner_desc": "**Kampfgruppe:** {squadron}\n**Clan-ID:** {clan_id}\n\n🔑 Dein Zugriffspasswort wurde generiert. **Speichere dieses Passwort** — du wirst es benötigen, um in Zukunft auf die Meta-Daten zuzugreifen.\n\n**Passwort:** `{password}`",
"first_time_non_owner_desc": "**Kampfgruppe:** {squadron}\n**Clan-ID:** {clan_id}\n\nKampfgruppe wurde eingerichtet. Frage den Server-Inhaber nach dem Zugriffspasswort.",
"settings_field": "Einstellungen",
"settings_hint": "Verwende die Schaltflächen unten, um Zugriffseinstellungen zu konfigurieren.",
"password_toggled": "✅ Passwortpflicht: **{state}**",
"lock_toggled": "✅ Kampfgruppendatenbindung: **{state}**",
"public_meta_toggled": "✅ Öffentlicher Meta-Zugriff: **{state}**\n{detail}",
"public_meta_enabled_detail": "Nicht-Admins können jetzt den `/meta`-Befehl verwenden.",
"public_meta_disabled_detail": "Nur Admins können den `/meta`-Befehl verwenden.",
"owner_only_password": "❌ Nur der Server-Inhaber kann das Kampfgruppenpasswort ändern.",
"help_title": "📖 Meta-Verwaltungs-Hilfe",
"help_desc": "Erklärung jeder Einstellung und Funktion:",
"help_password_field": "🔑 Zugriffspasswort",
"help_password_value": "Das Zugriffspasswort deiner Kampfgruppe. Nur der **Server-Inhaber** kann das Passwort im Einstellungsbereich sehen. Jeder mit dem Passwort kann die Meta-Daten deiner Kampfgruppe auf seinem Server beanspruchen, also halte es sicher.",
"help_require_field": "🔒 Passwort erforderlich",
"help_require_value": "Wenn aktiviert, müssen auch Admins auf diesem Server das Kampfgruppenpasswort eingeben, um auf `/meta-management` zuzugreifen. Bietet eine zusätzliche Sicherheitsschicht gegen versehentliche Änderungen.",
"help_lock_field": "🔐 Kampfgruppendaten binden",
"help_lock_value": "Wenn aktiviert, verhindert die Übertragung der Kampfgruppe auf andere Server, auch mit dem richtigen Passwort. Muss deaktiviert werden, bevor die Kampfgruppe übertragen werden kann.",
"help_public_field": "👥 Öffentliches Meta erlauben",
"help_public_value": "Wenn aktiviert, können Nicht-Admin-Mitglieder den `/meta`-Befehl zur Suche nach Kampfgruppenfahrzeugen verwenden. Wenn deaktiviert, können nur Server-Administratoren `/meta` verwenden.",
"help_accounts_field": "📋 Meta-Konten aktualisieren",
"help_accounts_value": "Öffnet den Spieler-Roster-Manager, mit dem du Spieler aus dem Meta-Roster deiner Kampfgruppe hinzufügen oder entfernen kannst. Verwende **Alle Mitglieder aktualisieren**, um dein gesamtes Kampfgruppe auf einmal zu synchronisieren.",
"help_change_pw_field": "🔑 Passwort ändern",
"help_change_pw_value": "**Nur Server-Inhaber.** Ändere das Zugriffspasswort der Kampfgruppe und setze optional einen Hinweis. Der Hinweis wird in der Passwortabfrage angezeigt, um daran zu erinnern.",
"password_modal_title": "Kampfgruppe-Zugriffspasswort",
"password_modal_label": "Kampfgruppenpasswort eingeben",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Kampfgruppenpasswort ändern",
"current_password_label": "Aktuelles Passwort",
"current_password_placeholder": "Aktuelles Passwort eingeben",
"new_password_label": "Neues Passwort",
"new_password_placeholder": "Neues Passwort eingeben",
"confirm_password_label": "Neues Passwort bestätigen",
"confirm_password_placeholder": "Neues Passwort erneut eingeben",
"hint_label": "Passwort-Hinweis (Optional)",
"hint_placeholder": "Ein Hinweis zum Merken des Passworts",
"pw_incorrect": "❌ Aktuelles Passwort ist falsch.",
"pw_mismatch": "❌ Neue Passwörter stimmen nicht überein. Bitte erneut versuchen.",
"pw_empty": "❌ Neues Passwort darf nicht leer sein.",
"pw_changed": "✅ Passwort für **{squadron}** erfolgreich aktualisiert.\n**Neues Passwort:** `{password}`",
"pw_changed_hint": "\n**Hinweis:** {hint}",
"player_add_modal_title": "Spieler zum Meta-Roster hinzufügen",
"player_add_label": "Spieler-UID oder Spitzname",
"player_add_placeholder": "Spieler-UID eingeben (z.B. 12345678) oder Spitzname",
"player_not_found": "❌ Spieler `{player}` nicht in der Players_Global-Datenbank gefunden.\n",
"roster_title": "📋 Meta-Roster-Verwaltung - {squadron}",
"roster_desc": "**Kampfgruppe-Clan-ID:** {clan_id}\n**Spieler gesamt:** {count}",
"roster_page_field": "Spieler (Seite {page}/{total})",
"no_players_field": "Keine Spieler",
"no_players_hint": "Noch keine Spieler zum Meta-Roster hinzugefügt. Klicke auf **Spieler hinzufügen**, um zu beginnen.",
"remove_player_placeholder": "Spieler zum Entfernen auswählen...",
"fetch_members_failed": "❌ Kampfgruppenmitglieder konnten nicht abgerufen werden: {error}",
"no_members_found": "❌ Keine Mitglieder in Kampfgruppe gefunden oder API-Aufruf fehlgeschlagen.",
"roster_synced": "✅ Roster mit Kampfgruppe synchronisiert.",
"roster_added": "**+{count}** hinzugefügt",
"roster_removed": "**-{count}** entfernt (Kampfgruppe verlassen)",
"roster_up_to_date": "**{count}** bereits aktuell",
"refreshing_vehicles": "Fahrzeugdaten werden im Hintergrund aktualisiert..."
},
"meta": {
"not_configured": "❌ Meta-Daten für diesen Server nicht konfiguriert. Führe zuerst `/meta-management` aus.",
"no_permission": "❌ Du benötigst Administratorrechte, um diesen Befehl zu verwenden.\nAdmins können den öffentlichen Zugriff über `/meta-management` aktivieren.",
"no_results": "❌ Kein Spieler in deiner Kampfgruppe-Roster hat **{vehicle}**.",
"no_results_admin_hint": "\n*Erwartest du, dass jemand dieses Fahrzeug hat? Klicke auf den Mitglieder-aktualisieren-Button in `/meta-management` und überprüfe es.*",
"search_title": "🔍 Suchergebnisse - {vehicle}",
"matches_found": "**Treffer gefunden:** {count} Spieler",
"spawns_label": "Spawns",
"deaths_label": "Tode",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "Punkte",
"kdr_label": "KDR",
"games_label": "Spiele",
"no_points": "—"
},
"top": {
"title": "**Top 20 Kampfgruppen**",
"rating_label": "**Wertung:** {value}",
"air_kills_label": "**Luftabschüsse:** {value}",
"ground_kills_label": "**Bodenabschüsse:** {value}",
"deaths_label": "**Tode:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Siegrate:** {value}",
"playtime_label": "**Spielzeit:** {value}",
"fetch_failed": "Kampfgruppendaten konnten nicht abgerufen werden."
},
"analytics": {
"no_data_title": "Keine Daten",
"no_matches_desc": "Keine Gefechte gefunden.",
"no_comp_desc": "Keine Kompositionsdaten gefunden.",
"no_consistency_desc": "Nicht genug Spielerdaten (mindestens 50 Gefechte).",
"no_time_desc": "Keine Zeitdaten gefunden.",
"unknown_view": "Unbekannte Ansicht.",
"map_title": "Karten-Siegraten: {squadron}",
"comp_title": "Teamzusammensetzungen: {squadron}",
"consistency_title": "Spielerkonstanz: {squadron}",
"consistency_desc": "Sortiert nach K/D-Verhältnis",
"time_title": "Tageszeit-Performance: {squadron}",
"eu_timeslot": "\n**EU-Zeitfenster**",
"na_timeslot": "\n**NA-Zeitfenster**",
"off_peak": "\n**Nebenstoßzeit**",
"matchups_title": "📜 {squadron} — Begegnungsverlauf",
"matchups_won_field": "🏆 Meiste Siege gegen",
"matchups_lost_field": "💀 Meiste Niederlagen gegen",
"no_matchups_desc": "Keine aufgezeichneten Spiele gegen andere Squadrons."
},
"recent": {
"title": "Aktuelle Gefechte: {squadron}",
"no_matches_desc": "Keine Gefechte für diese Kampfgruppe gefunden."
},
"h2h": {
"two_required_title": "Zwei Kampfgruppen erforderlich",
"two_required_desc": "Mindestens eine Kampfgruppe angeben oder `/set-squadron` verwenden und den Gegner angeben.",
"provide_a_desc": "`squadron_a` angeben oder zuerst `/set-squadron` verwenden.",
"provide_b_desc": "`squadron_b` angeben oder zuerst `/set-squadron` verwenden.",
"squadron_not_found_title": "Kampfgruppe nicht gefunden",
"same_squadron_title": "Gleiche Kampfgruppe",
"same_squadron_desc": "Du kannst kein direktes Duell gegen dich selbst prüfen.",
"record_desc": "**Bilanz:** {a_wins}S - {b_wins}N ({total} Spiele)",
"no_matches_desc": "Keine aufgezeichneten Gefechte zwischen **{a}** und **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Aktiv — Autologging ist für diesen Server aktiviert.",
"premium_not_subscribed_line": "❌ **Premium:** Nicht abonniert — verwende `/unlock` um Autologging zu aktivieren.",
"premium_free_line": "⚪ **Premium:** Nicht abonniert — verwende `/unlock` zum Abonnieren ($2.99/Monat). *(Autologs sind derzeit für alle Server kostenlos.)*",
"what_to_do": "\n\nWas möchtest du tun?",
"select_notif_type": "Benachrichtigungstyp auswählen:",
"select_notif_placeholder": "Benachrichtigungstyp auswählen",
"logs_option": "Logs",
"logs_option_desc": "Logs-Benachrichtigungen verwalten",
"points_option": "Punkte",
"points_option_desc": "Punkte-Benachrichtigungen verwalten",
"leaderboard_option": "Rangliste",
"leaderboard_option_desc": "Ranglisten-Benachrichtigungen verwalten",
"selected_type": "**{type}** ausgewählt. Jetzt die zu verwaltende Kampfgruppe auswählen:",
"select_squadron_placeholder": "Kampfgruppe auswählen",
"select_squadron_page_placeholder": "Kampfgruppe auswählen (Seite {page})",
"no_squadrons_available": "Keine Kampfgruppe für diesen Benachrichtigungstyp verfügbar.",
"managing_global": "**{type}** (global) in Kanal **{channel}** wird verwaltet.",
"managing_squadron": "**{type}** für Kampfgruppe **{squadron}** in Kanal **{channel}** wird verwaltet.",
"select_channel": "Neuen Kanal auswählen:",
"select_channel_placeholder": "Kanal auswählen",
"select_channel_page_placeholder": "Kanal auswählen (Seite {page})",
"global_toggled": "{type} (global) ist jetzt {state}.",
"squadron_toggled": "{type} für **{squadron}** ist jetzt {state}.",
"channel_updated_global": "{type} (global) auf {channel} aktualisiert",
"channel_updated_squadron": "{type} für **{squadron}** auf {channel} aktualisiert",
"diagnose_channel_placeholder": "Zu diagnostizierenden Kanal auswählen...",
"select_channel_diagnose": "Zu diagnostizierenden Kanal auswählen:",
"game_not_logged_title": "Spiel nicht protokolliert",
"game_not_logged_desc": "Nutze `/unlock`, um den **Standard**-Tarif (oder höher) zu abonnieren und automatische Spielergebnistabellen zu erhalten.",
"server_not_upgraded_title": "⚠️ Server nicht geupgradet",
"server_not_upgraded_autolog_desc": "Dieser Server hat kein aktives Premium-Abonnement.\n\n**Automatische Spielergebnistabellen werden nach <t:{deadline}:D> nicht mehr an nicht-upgegradete Server gesendet.**\n\nVerwende `/unlock` zum Abonnieren und weiterhin automatische Spielprotokolle zu erhalten.",
"replay_not_available": "Replay-Daten sind noch nicht verfügbar — kurz warten und erneut versuchen!",
"too_many_videos": "Zu viele Videos werden gerade gerendert — bitte in einem Moment erneut versuchen.",
"video_gen_failed": "Fehler beim Erstellen des Videos: `{error}`",
"video_missing": "Replay-Video konnte nicht erstellt werden - Ausgabedatei fehlt oder ist leer.",
"video_too_large": "Replay-Video zu groß zum Hochladen ({file_mb:.1f} MB). Serverlimit ist {limit_mb:.0f} MB.",
"video_web_fallback": "Du kannst dieses Gefecht auch unter {url} ansehen",
"video_upload_failed": "Video zu groß zum Hochladen — auf der Website ansehen:\n{url}",
"video_unexpected_error": "Unerwarteter Fehler beim Erstellen des Replay-Videos: `{error}`",
"replay_not_found": "Replay-Daten für Session `{session_id}` nicht auf Disk gefunden.",
"chat_log_title": "**Chat-Protokoll für Spiel [{session_id}]({url})**",
"chat_log_part_title": "**Chat-Protokoll für Spiel [{session_id}]({url}) (Teil {part}/{total})**",
"chat_log_part_only": "**Chat-Protokoll (Teil {part}/{total})**",
"no_chat_log": "Kein Chat-Protokoll für Session `{session_id}` gefunden.",
"chat_log_error": "Unerwarteter Fehler beim Laden des Chat-Protokolls: `{error}`",
"battle_log_title": "**Gefechtsbericht für Spiel [{session_id}]({url})**",
"battle_log_part_title": "**Gefechtsbericht für Spiel [{session_id}]({url}) (Teil {part}/{total})**",
"battle_log_part_only": "**Gefechtsbericht (Teil {part}/{total})**",
"no_battle_log": "Keine Kampfereignisse für Session `{session_id}` gefunden.",
"battle_log_error": "Unerwarteter Fehler beim Laden des Gefechtsberichts: `{error}`",
"points_update_title": "**{squadron} {region} Punkte-Update**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Spieleränderungen:**",
"points_table_header": "Name Änderung Jetzt\n",
"wl_line": "\n**{squadron}** hat in dieser Session **{wins}S-{losses}N** gespielt",
"placement_rose": "\n**{squadron}** stieg auf **{new_place}** von **{old_place}**",
"placement_fell": "\n**{squadron}** fiel auf **{new_place}** von **{old_place}**",
"points_not_logged_title": "Punkte nicht protokolliert",
"points_not_logged_desc": "Nutze `/unlock`, um den **Standard**-Tarif (oder höher) zu abonnieren und automatische Punkte-Updates zu erhalten.",
"server_not_upgraded_points_desc": "Dieser Server hat kein aktives Premium-Abonnement.\n\n**Automatische Updates werden nach <t:{deadline}:D> nicht mehr an nicht-upgegradete Server gesendet.**\n\nVerwende `/unlock` zum Abonnieren und weiterhin automatische Updates zu erhalten.",
"leave_title": "⚠️ Spieler hat {squadron} verlassen",
"leave_desc": "**{nick}** ({uid}) hat die Kampfgruppe verlassen.\n\nZuletzt erfasste Punkte: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Geschwader über dem Tarif-Limit",
"over_cap_desc": "Dein Server nutzt den **{tier}**-Tarif, der **{cap}** Geschwader für **{notif}** erlaubt. Das Geschwader **{squadron}** liegt aktuell über dem Limit und wird nicht geloggt. Upgrade für mehr Kapazität.",
"over_cap_footer": "Upgrade unter srebot-meow.ing/premium oder /unlock",
"wildcard_blocked_title": "Wildcard-Logging erfordert einen höheren Tarif",
"wildcard_blocked_desc": "Wildcard-Einträge (*, all, everything) sind nur in Pro/Max verfügbar. Dein Server ist auf **{tier}** für {notif}. Upgrade zum Aktivieren.",
"cap_header": "{used}/{cap} {notif} aktiviert — {tier}-Tarif"
},
"track": {
"squadron_not_found": "Kampfgruppe nicht gefunden.",
"fetch_failed": "Kampfgruppen-Informationen konnten nicht abgerufen werden."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Premium-Funktionen für diesen Server freischalten.**\n\nPremium beinhaltet:\n> • Automatische Ergebnistabellenpostings\n> • Chat- & Gefechtsberichte\n> • Replay-Suche\n> • Unbegrenzte /comp-Abfragen\n> • Prioritätssupport\n\n**$2.99 / Monat · pro Server · jederzeit kündbar**\n\n⚠️ Discord-Abrechnung ist nur in ausgewählten Ländern verfügbar. Wenn die Schaltfläche unten **\"Produkt nicht verfügbar\"** anzeigt, kann dies an einem nicht unterstützten Land oder einem **Mobilgerät** liegen. Verwende stattdessen die Schaltfläche **Über Website abonnieren**.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Dieser Server ist bereits abonniert!**",
"manage_discord_field": "Abonnement verwalten",
"manage_discord_value": "Dein Abonnement läuft über **Discord**.\nZum Kündigen gehe in Discord zu **Benutzereinstellungen → Abonnements**.",
"manage_website_field": "Abonnement verwalten",
"manage_website_value": "Dein Abonnement läuft über die **Website**.\nVerwalte es unter [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Demnächst verfügbar",
"coming_soon_value": "Premium-Abonnements sind noch nicht verfügbar. Schau bald wieder vorbei!",
"current_tier": "Du nutzt den **{tier}**-Tarif.",
"upgrade_to": "Upgrade auf {tier}",
"upgrade_to_value": "Mehr Geschwader und Features durch Upgrade auf **{tier}**."
},
"language": {
"prompt": "Bitte wähle deine Server-Sprache:",
"select_placeholder": "Server-Sprache auswählen",
"language_set": "Sprache auf {language} gesetzt.",
"translate_prompt": "Wähle unten eine Zielsprache aus 👇",
"translate_placeholder": "Zielsprache auswählen…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Übersetzung nicht verfügbar (DeepL nicht konfiguriert)",
"translation_failed": "Übersetzung fehlgeschlagen"
},
"misc": {
"credits_title": "Credits",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Leitentwickler, Bot-Manager, Community-Manager\n> **Z3R0** - Entwickler, Optimierungsentwickler, Datenbankingenieur\n> **Clippii (Heidi)** - Entwickler, Website-Entwickler, Community-Manager\n> **LivingTheDagor** - Entwickler, Parser-Entwickler, Berater\n> **Lux_** - API-Ingenieur, Spectra-Entwickler\n> **Konigallerwaffen** - Berater für Feedback und Funktionen\n> **Žralok Tonda** - Tschechischer Übersetzer\n> **Styevy**, **Lopais** - Deutsche Übersetzer\n> **Susogus**, **playforfun698** - Polnische Übersetzer\n> **Bobr** - Russischer Übersetzer\n\n\n[Lust auf Mitmachen?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "SAISONKALENDER",
"schedule_not_found_title": "Kalender nicht gefunden",
"schedule_not_found_desc": "Es sind noch keine Kalenderdaten verfügbar.",
"news_no_news_title": "Keine Neuigkeiten",
"news_no_news_desc": "Es gibt derzeit keine Ankündigungen. Schau später wieder vorbei!",
"news_footer": "Danke für deine Unterstützung! ᕙᘘᗢ",
"help_title": "Bot-Anleitung",
"donate_title": "SRE Bot unterstützen",
"donate_desc": "Wenn du SRE Bot gerne nutzt und seine Entwicklung unterstützen möchtest, erwäge mir einen Kaffee zu spendieren!\n\n**[Auf Ko-fi spenden](https://ko-fi.com/notsotoothless)**\n\nJeder Beitrag hilft, den Bot am Laufen zu halten und neue Funktionen zu unterstützen. Danke!",
"status_title": "Bot-Status",
"status_last_received": "Letztes empfangenes Spiel",
"status_avg_ttl": "Durchschn. TTL (letzte 30)",
"status_no_data": "Noch keine Daten",
"status_gaijin_slow": "⚠️ Gaijin-Server sind langsam",
"help_commands_header": "**Command-Übersicht**",
"help_links": "Details findest du in der Dokumentation [hier]({docs}) oder beim Support [hier]({support}).",
"help_terms": "[Nutzungsbedingungen]({terms}) • [Datenschutzrichtlinie]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Kampfgruppen-Rangliste",
"top15_desc": "Top-15-Kampfgruppen mit Statistiken, gesendet 35 Minuten nach Zeitfenster-Schluss.\nDieses wurde gesendet <t:{timestamp}:R>.",
"top30_desc": "Kampfgruppen 16-30 mit Statistiken.",
"not_logged_title": "Rangliste nicht protokolliert",
"not_logged_desc": "Nutze `/unlock`, um den **Standard**-Tarif (oder höher) zu abonnieren und automatische Ranglisten-Updates zu erhalten.",
"server_not_upgraded_title": "⚠️ Server nicht geupgradet",
"server_not_upgraded_desc": "Dieser Server hat kein aktives Premium-Abonnement.\n\n**Automatische Updates werden nach <t:{deadline}:D> nicht mehr an nicht-upgegradete Server gesendet.**\n\nVerwende `/unlock` zum Abonnieren und weiterhin automatische Updates zu erhalten."
},
"stacks": {
"stack_title": "{leader}s Staffel",
"stack_named_title": "{name}",
"no_members": "Noch keine Mitglieder.",
"members_field": "Mitglieder ({count}/{max})",
"queue_field": "Warteschlange ({count}/{max})",
"manage_title": "Staffel verwalten",
"no_pending_requests": "Keine ausstehenden Anfragen.",
"disbanded_title": "Staffel [Aufgelöst]",
"disbanded_desc": "Diese Staffel wurde vom Anführer aufgelöst.",
"expired_title": "Staffel [Abgelaufen]",
"expired_desc": "Diese Staffel ist abgelaufen.",
"join_modal_title": "Staffel beitreten",
"join_vehicle_label": "Was wirst du spielen?",
"join_vehicle_placeholder": "z.B. F-16C, WZ305...",
"ping_modal_title": "Ping-Nachricht",
"ping_message_label": "Eigene Nachricht (optional)",
"ping_message_placeholder": "z.B. Kommt jetzt! Staffel startet!",
"rename_modal_title": "Staffel umbenennen",
"rename_label": "Staffel-Name",
"rename_placeholder": "z.B. Nachtschwärmer, Alpha Team...",
"select_new_leader": "Neuen Anführer auswählen…",
"select_applicants": "Bewerber auswählen…",
"no_pending_applications": "Keine ausstehenden Bewerbungen.",
"select_to_remove": "Personen zum Entfernen auswählen…",
"no_members_or_applicants": "Keine Mitglieder oder Bewerber.",
"select_to_ping": "Personen einzeln anpingen…",
"stack_not_found": "❌ Staffel nicht gefunden.",
"no_longer_exists": "❌ Diese Staffel existiert nicht mehr.",
"member_not_exists": "❌ Dieses Mitglied existiert nicht mehr.",
"already_has_stack": "❌ Dieser Spieler hat bereits eine aktive Staffel.",
"already_member": "❌ Du bist bereits Mitglied dieser Staffel.",
"already_applied": "❌ Du hast bereits eine ausstehende Bewerbung für diese Staffel.",
"queue_full": "❌ Die Warteschlange ist voll ({max}/{max}). Versuche es später erneut.",
"application_sent": "✅ Bewerbung gesendet! Der Staffel-Anführer wird sie prüfen.",
"stack_disbanded": "✅ Staffel aufgelöst.",
"cancelled": "Abgebrochen.",
"select_member_transfer": "❌ Bitte wähle ein Mitglied für die Übertragung aus.",
"ownership_transferred": "✅ Führung an {nick} übertragen. Du hast die Staffel verlassen.",
"select_applicant_first": "❌ Bitte wähle zuerst mindestens einen Bewerber aus.",
"stack_full": "❌ Staffel ist bereits voll ({max}/{max} Mitglieder).",
"select_person_first": "❌ Bitte wähle zuerst mindestens eine Person aus.",
"no_one_to_ping": "❌ Niemand zum Anpingen.",
"ping_footer": "Angepingt von {leader} für {stack}.",
"pinged": "✅ Angepingt!",
"select_from_dropdown": "❌ Bitte wähle zuerst mindestens eine Person aus dem Dropdown.",
"stack_renamed": "✅ Staffel umbenannt zu **{name}**.",
"only_member_use_disband": "❌ Du bist das einzige Mitglied. Verwende **Staffel auflösen** zum Beenden.",
"select_transfer_prompt": "Wähle ein Mitglied, an das du die Führung übertragen möchtest:",
"left_stack": "✅ Du hast die Staffel verlassen.",
"application_withdrawn": "✅ Deine Bewerbung wurde zurückgezogen.",
"not_member_or_applicant": "❌ Du bist weder Mitglied noch Bewerber dieser Staffel.",
"leader_only_manage": "❌ Nur der Staffel-Anführer kann diese Staffel verwalten.",
"leader_only_disband": "❌ Nur der Staffel-Anführer kann diese Staffel auflösen.",
"confirm_disband": "Bist du sicher, dass du diese Staffel auflösen möchtest? Dies kann nicht rückgängig gemacht werden.",
"already_active_stack": "⚠️ Du hast bereits eine aktive Staffel. Falls die ursprüngliche Nachricht nicht mehr existiert (z.B. nach einem Bot-Neustart), kannst du die Auflösung erzwingen und neu starten.",
"force_created": "✅ Vorherige Staffel aufgelöst. Neue Staffel erstellt.",
"no_active_stack": "❌ Du hast keine aktive Staffel. Verwende `/stack-create` um einen zu erstellen.",
"could_not_parse_channel": "⚠️ Gespeicherte Kanal-ID konnte nicht verarbeitet werden."
},
"commands": {
"common": {
"season": "Saison für die Karte",
"theme": "Farbschema der Karte",
"squadron_short": "Kurzname der Staffel",
"player_username": "Spielername",
"choice_dark": "Dunkel",
"choice_light": "Hell"
},
"comp": {
"description": "Letzte bekannte Aufstellungen eines Teams finden",
"squadron_short": "Kurzname des gegnerischen Teams"
},
"quick_log": {
"description": "Alarm für diese Staffel in diesem Kanal setzen",
"squadron_name": "KURZNAME der zu überwachenden Staffel",
"type": "Wähle Logs, Punkte, Leaderboard, Wöchentlicher BR oder Beide",
"choice_logs": "Logs",
"choice_points": "Punkte",
"choice_leaderboard": "Rangliste",
"choice_both": "Beides (Logs + Punkte)",
"choice_weekly_br": "Wöchentlicher BR"
},
"sq_info": {
"description": "Informationen zu einer Staffel abrufen"
},
"sq_info_graph": {
"description": "Aufstellungsgrafik nach Aktivität und Siegrate anzeigen (aktuelle Saison)"
},
"sq_card": {
"description": "Saisonkarte für eine Staffel erstellen",
"squadron": "Kurzname der Staffel"
},
"sq_stats": {
"description": "Staffelpunkte im Zeitverlauf anzeigen"
},
"loss_calculator": {
"description": "Punkteverlust berechnen, wenn Spieler eine Staffel verlassen",
"player1": "Spieler verlässt",
"player_optional": "Spieler verlässt (optional)"
},
"website": {
"description": "Link zur SRE Bot-Webseite erhalten"
},
"card": {
"description": "Saisonkarte für einen Spieler erstellen"
},
"player_stats": {
"description": "Detaillierte Fahrzeugstatistiken eines Spielers anzeigen",
"username": "WT-Benutzername für Stats",
"uid": "WT-UID für Stats"
},
"view_player_games": {
"description": "Die letzten 20 Spiele eines Spielers anzeigen"
},
"view_match": {
"description": "Match-Scoreboard per ID oder Spieler anzeigen",
"match_id": "Hex-Session-ID des Matches",
"player_name": "Spielername zum Durchsuchen neuer Matches"
},
"compare": {
"description": "Gesamte SQB-Stats von Spielern vergleichen",
"player1": "Erster Spielername",
"player2": "Zweiter Spielername",
"player_optional": "Weiterer Spielername (optional)"
},
"leaderboard": {
"description": "Globale SRE Bot-Rangliste öffnen"
},
"set_squadron": {
"description": "Staffel-Tag für diesen Server setzen",
"abbreviated_name": "Kurzname der zu setzenden Staffel"
},
"setup": {
"description": "Bot für diesen Server einrichten"
},
"meta_management": {
"description": "Zugriff auf Meta-Daten für diesen Server verwalten"
},
"meta": {
"description": "Meta-Roster nach Fahrzeugname durchsuchen",
"vehicle": "Zu suchender Fahrzeugname"
},
"top": {
"description": "Top 20 Staffeln mit Detailstats anzeigen"
},
"language": {
"description": "Sprache des Bots ändern."
},
"translate_message": {
"name": "Nachricht übersetzen"
},
"sq_track": {
"description": "Staffel verfolgen und mit der letzten Prüfung vergleichen",
"squadron_short_name": "Kurzname der zu verfolgenden Staffel"
},
"analytics": {
"description": "Erweiterte SQB-Analysen für eine Staffel anzeigen",
"view": "Welche Analyseansicht angezeigt wird",
"choice_maps": "Kartensiegquoten",
"choice_comps": "Teamaufstellungen",
"choice_consistency": "Spielerkonstanz",
"choice_time": "Tageszeit",
"choice_matchups": "Duellverlauf"
},
"recent": {
"description": "Neue Staffelkämpfe einer Staffel anzeigen",
"length": "Anzahl der anzuzeigenden Matches"
},
"vs": {
"description": "Direktvergleich zwischen zwei Staffeln",
"squadron_a": "Erste Staffel",
"squadron_b": "Zweite Staffel"
},
"autolog_management": {
"description": "Autolog-Benachrichtigungen verwalten und Rechte prüfen"
},
"diagnose_perms": {
"description": "Autolog-Rechte für diesen Kanal prüfen"
},
"unlock": {
"description": "Premium-Funktionen für diesen Server freischalten"
},
"credits": {
"description": "Das Team hinter diesem Projekt anzeigen"
},
"schedule": {
"description": "Aktuellen Saison-BR-Plan anzeigen"
},
"news": {
"description": "Neueste SRE Bot-News und Ankündigungen anzeigen"
},
"help": {
"description": "Guide, Nutzungsbedingungen und Supportlinks anzeigen"
},
"donate": {
"description": "Entwicklung von SRE Bot unterstützen"
},
"stack_create": {
"description": "Neuen Spieler-Stack erstellen",
"vehicle": "Mit welchem Fahrzeug startest du?"
},
"stack_manage": {
"description": "Deinen aktiven Stack in diesem Kanal neu posten"
},
"bot_status": {
"description": "Bot-Status anzeigen: letztes empfangenes Spiel und durchschn. TTL"
}
},
"permission": {
"blacklisted_title": "❌ Gesperrt",
"blacklisted_desc": "Du bist für diese Command-Nutzung gesperrt.",
"reason_line": "**Grund:** {reason}",
"access_denied_title": "⛔ Zugriff verweigert",
"no_permission_desc": "Du hast keine Berechtigung für diesen Command.",
"unexpected_error_title": "❗ Fehler, bitte melden...."
},
"weekly_br": {
"title_wildcard": "Wöchentlicher BR-Bericht — {br} BR",
"title_squadron": "Wöchentlicher BR-Bericht — [{tag}] {long} • {br} BR",
"window_label": "Zeitraum: {start} → {end}",
"wildcard_desc_first": "Top {count} Geschwader nach ELO • Plätze {low}{high}",
"wildcard_desc_second": "Top {count} Geschwader nach ELO • Plätze {low}{high}",
"squadron_stats_line": "- {games} Spiele • K/D {kdr} • Siegrate {wr}%",
"top_players_inline_header": "🥇 Top-Spieler:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}S)",
"top_players_header": "**Top {count} Spieler nach ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} Spiele • K/D {kdr}",
"squadron_header_line": "Geschwader-ELO: {score} • {games} Spiele • Siegrate {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "Geschwader-ELO: nicht genügend Teamspiele in dieser Woche.",
"no_data": "Keine Spiele für [{tag}] in dieser BR-Rotation aufgezeichnet."
}
}
+857
View File
@@ -0,0 +1,857 @@
{
"common": {
"error_title": "Error",
"no_data_title": "No Data",
"access_denied_title": "Access Denied",
"access_denied_desc": "This server has been blacklisted.",
"no_players_selected": "No players selected. Please select at least one player.",
"must_use_in_server": "This command must be used in a server.",
"could_not_resolve_channel": "Could not resolve the selected channel.",
"failed_update_setting": "❌ Failed to update setting.",
"configuration_not_found": "Configuration not found.",
"no_channel_selected": "No channel selected.",
"no_selection_received": "No selection received.",
"database_error": "❌ Database error: {error}",
"enabled": "Enabled",
"disabled": "Disabled",
"not_configured": "Not configured",
"unknown": "Unknown",
"rating_field": "Rating",
"battles_field": "Battles",
"wins_field": "Wins",
"losses_field": "Losses",
"win_rate_field": "Win Rate",
"kills_field": "Kills",
"deaths_field": "Deaths",
"kd_field": "K/D",
"members_field": "Members",
"placement_field": "Placement",
"points_field": "Points",
"ground_kills_field": "Ground Kills",
"air_kills_field": "Air Kills",
"total_kills_field": "Total Kills",
"assists_field": "Assists",
"captures_field": "Captures",
"none_option": "None"
},
"buttons": {
"skip": "Skip",
"previous": "Previous",
"next": "Next",
"prev": "Prev",
"prev_arrow": "◀ Previous",
"next_arrow": "Next ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Generate Chart",
"show_graph": "Show Graph",
"view_player_stats": "📊 View Player Stats",
"compare_nearby": "📈 Compare Nearby Squadrons",
"confirm_swap": "Yes, swap it",
"cancel_swap": "No, keep the old one",
"set_squadron": "Set Squadron",
"same_as_logs": "Same as Logs",
"require_password": "🔒 Require Password",
"password_required": "🔒 Password Required",
"lock_data": "🔐 Bind Squadron Data",
"data_locked": "🔐 Data Bound to Server",
"allow_public": "👥 Allow Public Meta",
"public_enabled": "👥 Public Meta Enabled",
"update_accounts": "📋 Update Meta Accounts",
"change_password": "🔑 Change Password",
"help": "❓ Help",
"add_player": " Add Player",
"update_all": "🔄 Update All Members",
"back_to_settings": "⬅ Back to Settings",
"manage_notifications": "Manage Notifications",
"diagnose_permissions": "Diagnose Permissions",
"enable": "Enable",
"disable": "Disable",
"change_channel": "Change Channel",
"view_replay": "View Replay",
"view_website": "View on Website",
"view_video": "View Video",
"view_log": "View Log",
"view_chat": "View Chat",
"subscribe_website": "Subscribe via Website",
"yes_disband": "Yes, Disband",
"cancel": "Cancel",
"transfer_leave": "Transfer & Leave",
"accept_selected": "Accept Selected",
"accept_all": "Accept All",
"decline_selected": "Decline Selected",
"back": "Back",
"remove_all": "Remove All",
"remove_active": "Remove Active",
"remove_queued": "Remove Queued",
"remove_selected": "Remove Selected",
"ping_all": "Ping All",
"ping_active": "Ping Active",
"ping_queued": "Ping Queued",
"ping_selected": "Ping Selected",
"accept_members": "Accept Members",
"remove_members": "Remove Members",
"ping_members": "Ping Members",
"rename_stack": "Rename Stack",
"request_to_join": "Request to Join",
"leave_withdraw": "Leave / Withdraw",
"manage_stack": "Manage Stack ⚙️",
"disband_stack": "Disband Stack",
"force_disband_create": "Force Disband & Create New"
},
"events": {
"guild_join_title": "Thanks for adding me!",
"guild_join_desc": "Run `/setup` to configure the bot for this server."
},
"comp": {
"not_found_title": "Comps Not Found",
"not_found_desc": "No data for **{squadron}**, try again later.",
"error_loading_title": "Error Loading Comps",
"error_loading_desc": "Failed to load comp data: {error}",
"title": "Comps for {squadron}",
"desc": "Comps seen in the last {minutes} minutes",
"no_recent_title": "No Recent Comps",
"no_recent_desc": "No comps in the last {minutes} minutes.",
"comp_title": "COMP {index}",
"last_seen_label": "**Last seen** : {timestamp}{warning}",
"comp_label": "**Comp**: {notation}",
"no_players_recorded": "No players recorded.",
"limit_reached_title": "Comp Limit Reached",
"limit_reached_desc": "This server has used all {limit} comp lookups for this timeslot. Subscribe (with /unlock) for unlimited access or wait for the next timeslot.",
"remaining_footer": "{remaining}/{limit} comp lookups remaining this timeslot"
},
"quick_log": {
"invalid_type": "Type can only be set to Logs, Points, Leaderboard, Weekly BR, or Both.",
"squadron_required": "You must provide a squadron name for Logs, Points, or Both alarms.",
"wildcard_logs_only": "Only Logs can be set to wildcard squadron.",
"squadron_not_resolved": "Squadron `{squadron}` could not be resolved.",
"save_failed": "Failed to save preferences. Please try again later.",
"premium_warning": "\n\n> ⚠️ **Game logs require Premium.** Run `/unlock` to subscribe ($2.99/mo) — logs won't post until then.",
"leaderboard_set": "Global Leaderboard alarm set to this channel.",
"both_set": "Logs and Points alarms for {squadron} set to this channel.{premium_note}",
"alarm_set": "{alarm_type} alarm for {squadron} set to this channel.{premium_note}",
"weekly_br_wildcard_set": "Weekly BR Report (top-20 squadrons) set to this channel. Fires at the end of every BR rotation.",
"weekly_br_squadron_set": "Weekly BR Report for {squadron} (top-15 players) set to this channel. Fires at the end of every BR rotation."
},
"diagnostics": {
"title": "Autolog Diagnostics",
"channel_permissions_header": "**Channel Permissions** (<#{channel_id}>)",
"perms_needed": " ^ Autologging needs all of the above to send scoreboards.",
"server_squadron_header": "**Server Squadron** (`/set-squadron`)",
"server_squadron_short": " Short: `{short}`",
"server_squadron_long": " Long: `{long}`",
"server_squadron_not_set": " Not set (scoreboard bar color will show as 'not_set')",
"autolog_prefs_header": "**Autolog Preferences** (`/quick-log`)",
"autolog_none_configured": " ❌ NONE configured - autologging will NOT send anything to this server.",
"autolog_setup_hint": " Use `/quick-log <squadron_short> Logs` in the target channel to set up.",
"autolog_no_logs_channels": " ❌ No Logs channels configured. Only Points/Leaderboard found.",
"autolog_enable_hint": " Use `/quick-log <squadron_short> Logs` to enable autologging.",
"selected_channel_tag": " **(selected channel)**",
"missing_send_attach": " (missing send/attach)",
"channel_not_found": " (channel not found)",
"invalid_channel_id": " (invalid channel ID)",
"premium_status_header": "**Premium Status** (`/unlock`)",
"premium_active": " ✅ This server has an active Premium subscription.",
"premium_not_subscribed": " ❌ This server does **not** have a Premium subscription.",
"premium_autolog_required": " Autologging requires Premium. Use `/unlock` to subscribe.",
"premium_not_subscribed_free": " ⚪ Not subscribed — use `/unlock` to subscribe ($2.99/mo).",
"premium_free_note": " *(Autologs are free for all servers right now.)*"
},
"sq_info": {
"title": "Squadron Info: {squadron}",
"placement_field": "Placement",
"total_points_field": "Total Points",
"total_members_field": "Total Members",
"members_field": "Members",
"fetch_failed": "Failed to fetch squadron info."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Season {season})",
"embed_title": "{squadron} — Roster Composition",
"embed_desc": "Season **{season}** · Median games: **{median}** · Core: **{core}** · Active: **{active}** · Weak: **{weak}**\nBars sorted by games desc; height = WR%. Core = top 30% WR rank & games ≥ median. Active = top 3045% WR rank & games ≈ median. Weak = everyone else.",
"core_threshold_line": "CORE ≥ {wr}%",
"weak_threshold_line": "WEAK < {wr}%",
"y_label": "Win Rate",
"core_header": "CORE — {count} · WR {avg}%",
"active_header": "ACTIVE — {count} · WR {avg}%",
"weak_header": "WEAK — {count} · WR {avg}%",
"no_active_season": "No active season was found. Try again once the next season starts.",
"no_members": "No current members were found for {squadron}."
},
"recap_card": {
"unknown_season": "Unknown season: `{season}`.",
"no_clan_id": "Could not resolve a squadron ID for `{squadron}`.",
"render_failed": "Failed to generate the season recap card. Please try again later."
},
"sq_stats": {
"no_data_title": "No Data",
"no_data_desc": "No historical data found for squadron: {squadron}",
"title": "{squadron} // SQUADRON",
"desc": "Total Score Trend (Last {count} data points)",
"previous_score_field": "Previous Score",
"current_score_field": "Current Score",
"change_field": "Change",
"player_title": "{squadron} // PLAYERS",
"player_desc": "Individual player point trends",
"comparison_title": "{squadron} // LEADERBOARD COMPARISON",
"comparison_desc": "Comparing with squadrons ranked {range}",
"current_position_field": "Current Position",
"squadrons_shown_field": "Squadrons Shown",
"squadron_not_found_error": "Squadron not found in leaderboard",
"no_nearby_error": "No nearby squadrons found",
"no_historical_error": "No historical data found for nearby squadrons",
"comparison_chart_failed": "Failed to generate comparison chart",
"select_players_placeholder": "Select players (Page {page})"
},
"loss_calc": {
"title": "Point Loss — {squadron}",
"players_leaving_field": "Players Leaving",
"share_of_total_field": "% Share of Total",
"points_lost_real_field": "Points Lost (Real)",
"points_lost_raw_field": "Points Lost (Raw)",
"squadron_rating_field": "Squadron Rating",
"squadron_position_field": "Squadron Position",
"positions_lost_field": "Positions Lost",
"not_found_footer": "Not found in squadron: {players}",
"fetch_failed": "Failed to fetch squadron data: {error}",
"no_point_data": "No point data available for this squadron.",
"no_matching_players": "No matching players found in **{squadron}**."
},
"player": {
"select_player_placeholder": "Select a player",
"no_stats_found": "❌ No stats found for UID: {uid}",
"no_vehicle_stats": "❌ No vehicle stats found for this player.",
"vehicles_found": "Found **{count}** vehicles for **{nick}**\nSelect a vehicle to view detailed stats:",
"vehicle_select_placeholder": "Select a vehicle (Page {page}/{total})",
"combat_stats_header": "**__COMBAT STATS__**",
"ground_kills_label": "**Ground Kills:** {value}",
"air_kills_label": "**Air Kills:** {value}",
"total_kills_label": "**Total Kills:** {value}",
"assists_label": "**Assists:** {value}",
"deaths_label": "**Deaths:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Captures:** {value}",
"battle_record_header": "**__BATTLE RECORD__**",
"total_battles_label": "**Total Battles:** {value}",
"wins_label": "**Wins:** {value}",
"losses_label": "**Losses:** {value}",
"win_rate_label": "**Win Rate:** {value}%",
"stats_desc": "Stats for **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Player Not Found",
"not_found_desc": "No game history found for `{player}`.",
"no_players_found": "No players found matching **{username}**\nTry using `/website` to search on the website.",
"multiple_matches": "Multiple matches found, choose the correct one below:",
"must_provide_input": "You must provide at least a UID or username."
},
"player_games": {
"no_recent_title": "No Recent Games",
"no_recent_desc": "No games found for **{player}** in the last 8 hours.",
"squadron_label": "**Squadron:** {squadron}",
"record_label": "**W:** {wins} **L:** {losses} **WR:** {wr}%",
"comps_played_header": "\n\n**Comps Played**"
},
"match": {
"missing_input_title": "Missing Input",
"missing_input_desc": "Provide either a `match_id` or a `player_name`.",
"not_found_title": "Match Not Found",
"not_found_desc": "Could not find a match with ID `{match_id}`.",
"invalid_data_title": "Invalid Match Data",
"invalid_data_desc": "The replay data could not be parsed.",
"scoreboard_error_title": "Scoreboard Error",
"scoreboard_error_desc": "Failed to generate the scoreboard image.",
"no_games_title": "No Games Found",
"no_games_desc": "No game history found for **{player}**.",
"recent_matches_title": "Recent matches for {player}",
"recent_matches_desc": "Showing up to {count} recent games. Select one to view the full scoreboard.",
"select_match_placeholder": "Select a match to view..."
},
"compare": {
"no_players_found": "No players found matching **{name}**.",
"multiple_matches": "Multiple matches for **{name}**: {matches}\nPlease use a more specific name (the autocomplete suggestions are exact).",
"could_not_resolve": "Could not resolve players.",
"could_not_fetch": "❌ Could not fetch stats for **{name}**.",
"no_graph_data": "No data available for the last 90 days.",
"no_squadron_points_data": "No squadron points data for {names} (player not found in tracked squadron history).",
"graph_title": "Player Points — Last 90 Days",
"battles_label": "Battles",
"wins_label": "Wins",
"losses_label": "Losses",
"win_rate_label": "Win Rate",
"ground_kills_label": "Ground Kills",
"air_kills_label": "Air Kills",
"total_kills_label": "Total Kills",
"assists_label": "Assists",
"deaths_label": "Deaths",
"kd_label": "K/D",
"captures_label": "Captures"
},
"squadron": {
"not_found_desc": "Squadron `{squadron}` not found.",
"set_title": "✅ Squadron Set",
"set_desc": "Squadron **{squadron}** has been set for this server.",
"short_name_field": "Short Name",
"long_name_field": "Long Name",
"swap_title": "✅ Squadron Swapped",
"swap_desc": "Replaced **{old}** with **{new}** for this server.",
"already_set_title": "⚠️ Squadron Already Set",
"already_set_desc": "This server is currently set to **{old}**.\nSwap it to **{new}**?",
"swap_cancelled": "❌ Squadron change cancelled."
},
"setup": {
"step1_title": "Server Setup — Step 1 of 3",
"step1_desc": "This wizard will walk you through configuring the bot for your server.\n\n**Step 1** — Set your squadron\n**Step 2** — Choose a logs channel\n**Step 3** — Choose a points channel\n",
"step1_current_sq": "\nCurrently configured squadron: **[{short}] {long}**",
"step2_title": "Server Setup — Step 2 of 3",
"step2_desc": "Squadron set to **[{short}] {long}**.\n\nWhere should **battle logs** be posted?\nSelect a text channel below, or skip this step.",
"step3_title": "Server Setup — Step 3 of 3",
"step3_desc": "Where should **points notifications** be posted?\nSelect a text channel below, or skip this step.",
"step3_same_as_logs": "\n\nYou can also click \"Same as Logs\" to reuse the logs channel.",
"summary_title": "Setup Complete",
"summary_desc": "You can use `/autolog-management` to change these settings later.",
"squadron_field": "Squadron",
"logs_channel_field": "Logs Channel",
"points_channel_field": "Points Channel",
"premium_required_field": "⚠️ Game Logs Require Premium",
"premium_required_value": "Automatic game scoreboards won't post until this server has an active subscription. Run `/unlock` to subscribe ($2.99/mo).",
"modal_title": "Set Squadron",
"modal_label": "Squadron Short Name",
"modal_placeholder": "e.g. AXYS",
"squadron_not_found": "Squadron `{squadron}` not found. Please try again.",
"logs_channel_placeholder": "Select a logs channel...",
"points_channel_placeholder": "Select a points channel..."
},
"meta_management": {
"squadron_not_found_title": "❌ Squadron Not Found",
"squadron_not_found_desc": "Could not find clan ID for squadron: **{squadron}**",
"access_denied_title": "❌ Access Denied",
"access_denied_desc": "Incorrect password. This squadron's meta data is protected.",
"data_locked_title": "🔐 Squadron Data Bound",
"data_locked_desc": "**{squadron}** has data binding enabled and cannot be transferred to another server.\n\nThe squadron owner must disable **Bind Squadron Data** before it can be moved.",
"error_retrieving_settings": "❌ Error retrieving guild settings after transfer. Please try again.",
"error_retrieving_settings_retry": "❌ Error retrieving guild settings. Please try running the command again.",
"authenticated_title": "✅ Authenticated",
"authenticated_desc": "Password verified. Managing settings for **{squadron}**.",
"claimed_title": "✅ Squadron Claimed",
"claimed_desc": "**{squadron}** has been successfully claimed for this server!",
"password_requirement_field": "🔒 Password Requirement",
"data_lock_field": "🔐 Squadron Data Binding",
"public_meta_field": "👥 Public Meta Access",
"access_password_field": "🔑 Access Password",
"enabled_value": "✅ Enabled",
"disabled_value": "❌ Disabled",
"settings_title": "🔐 Meta Management Settings",
"settings_desc": "**Squadron:** {squadron}\n**Clan ID:** {clan_id}",
"first_time_title": "🔐 Meta Management - First Time Setup",
"first_time_owner_desc": "**Squadron:** {squadron}\n**Clan ID:** {clan_id}\n\n🔑 Your access password has been generated. **Save this password** — you'll need it to authenticate meta data access in the future.\n\n**Password:** `{password}`",
"first_time_non_owner_desc": "**Squadron:** {squadron}\n**Clan ID:** {clan_id}\n\nSquadron has been set up. Ask the server owner for the access password.",
"settings_field": "Settings",
"settings_hint": "Use the buttons below to configure access settings.",
"password_toggled": "✅ Password requirement: **{state}**",
"lock_toggled": "✅ Squadron data binding: **{state}**",
"public_meta_toggled": "✅ Public meta access: **{state}**\n{detail}",
"public_meta_enabled_detail": "Non-admins can now use `/meta` command.",
"public_meta_disabled_detail": "Only admins can use `/meta` command.",
"owner_only_password": "❌ Only the server owner can change the squadron password.",
"help_title": "📖 Meta Management Help",
"help_desc": "Explanation of each setting and feature:",
"help_password_field": "🔑 Access Password",
"help_password_value": "Your squadron's access password. Only the **server owner** can see the password in the settings panel. Anyone with the password can claim your squadron's meta data on their server, so keep it secure.",
"help_require_field": "🔒 Require Password",
"help_require_value": "When enabled, even admins on this server must enter the squadron password to access `/meta-management`. Adds an extra layer of security to prevent accidental changes.",
"help_lock_field": "🔐 Bind Squadron Data",
"help_lock_value": "When enabled, prevents the squadron from being transferred to other servers, even with the correct password. Must be disabled before the squadron can be transferred.",
"help_public_field": "👥 Allow Public Meta",
"help_public_value": "When enabled, allows non-admin members to use the `/meta` command to search squadron vehicles. When disabled, only server administrators can use `/meta`.",
"help_accounts_field": "📋 Update Meta Accounts",
"help_accounts_value": "Opens the player roster manager where you can add or remove players from your squadron's meta roster. Use **Update All Members** to sync your entire squadron at once.",
"help_change_pw_field": "🔑 Change Password",
"help_change_pw_value": "**Server owner only.** Change the squadron's access password and set an optional hint. The hint is shown in the password prompt to help remember it.",
"password_modal_title": "Squadron Access Password",
"password_modal_label": "Enter Squadron Password",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Change Squadron Password",
"current_password_label": "Current Password",
"current_password_placeholder": "Enter your current password",
"new_password_label": "New Password",
"new_password_placeholder": "Enter your new password",
"confirm_password_label": "Confirm New Password",
"confirm_password_placeholder": "Re-enter your new password",
"hint_label": "Password Hint (Optional)",
"hint_placeholder": "A hint to help remember the password",
"pw_incorrect": "❌ Current password is incorrect.",
"pw_mismatch": "❌ New passwords do not match. Please try again.",
"pw_empty": "❌ New password cannot be empty.",
"pw_changed": "✅ Password updated successfully for **{squadron}**.\n**New Password:** `{password}`",
"pw_changed_hint": "\n**Hint:** {hint}",
"player_add_modal_title": "Add Player to Meta Roster",
"player_add_label": "Player UID or Nickname",
"player_add_placeholder": "Enter player's UID (e.g., 12345678) or nickname",
"player_not_found": "❌ Player `{player}` not found in Players_Global database.\n",
"roster_title": "📋 Meta Roster Management - {squadron}",
"roster_desc": "**Squadron Clan ID:** {clan_id}\n**Total Players:** {count}",
"roster_page_field": "Players (Page {page}/{total})",
"no_players_field": "No Players",
"no_players_hint": "No players added to meta roster yet. Click **Add Player** to get started.",
"remove_player_placeholder": "Select player to remove...",
"fetch_members_failed": "❌ Failed to fetch squadron members: {error}",
"no_members_found": "❌ No members found in squadron or API call failed.",
"roster_synced": "✅ Synced roster with squadron.",
"roster_added": "**+{count}** added",
"roster_removed": "**-{count}** removed (left squadron)",
"roster_up_to_date": "**{count}** already up to date",
"refreshing_vehicles": "Refreshing vehicle data in background..."
},
"meta": {
"not_configured": "❌ Meta data not configured for this server. Run `/meta-management` first.",
"no_permission": "❌ You need administrator permissions to use this command.\nAdmins can enable public access via `/meta-management`.",
"no_results": "❌ No players in your squadron roster have **{vehicle}**.",
"no_results_admin_hint": "\n*Expecting someone to have this? Click the update members button in `/meta-management` and double check.*",
"search_title": "🔍 Search Results - {vehicle}",
"matches_found": "**Matches Found:** {count} player(s)",
"spawns_label": "Spawns",
"deaths_label": "Deaths",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "Points",
"kdr_label": "KDR",
"games_label": "Games",
"no_points": "—"
},
"top": {
"title": "**Top 20 Squadrons**",
"rating_label": "**Rating:** {value}",
"air_kills_label": "**Air Kills:** {value}",
"ground_kills_label": "**Ground Kills:** {value}",
"deaths_label": "**Deaths:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Win Rate:** {value}",
"playtime_label": "**Playtime:** {value}",
"fetch_failed": "Failed to retrieve squadron data."
},
"analytics": {
"no_data_title": "No Data",
"no_matches_desc": "No matches found.",
"no_comp_desc": "No composition data found.",
"no_consistency_desc": "Not enough player data (minimum 50 matches).",
"no_time_desc": "No time data found.",
"unknown_view": "Unknown view.",
"map_title": "Map Win Rates: {squadron}",
"comp_title": "Team Compositions: {squadron}",
"consistency_title": "Player Consistency: {squadron}",
"consistency_desc": "Sorted by K/D ratio",
"time_title": "Time of Day Performance: {squadron}",
"eu_timeslot": "\n**EU Timeslot**",
"na_timeslot": "\n**NA Timeslot**",
"off_peak": "\n**Off-Peak**",
"matchups_title": "📜 {squadron} — Matchup History",
"matchups_won_field": "🏆 Most Won Against",
"matchups_lost_field": "💀 Most Lost To",
"no_matchups_desc": "No recorded matches against other squadrons yet."
},
"recent": {
"title": "Recent Matches: {squadron}",
"no_matches_desc": "No matches found for this squadron."
},
"h2h": {
"two_required_title": "Two Squadrons Required",
"two_required_desc": "Provide at least one squadron, or use `/set-squadron` and provide the opponent.",
"provide_a_desc": "Provide `squadron_a` or use `/set-squadron` first.",
"provide_b_desc": "Provide `squadron_b` or use `/set-squadron` first.",
"squadron_not_found_title": "Squadron Not Found",
"same_squadron_title": "Same Squadron",
"same_squadron_desc": "You can't check head-to-head against yourself.",
"record_desc": "**Record:** {a_wins}W - {b_wins}L ({total} games)",
"no_matches_desc": "No recorded matches between **{a}** and **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Active — autologging is enabled for this server.",
"premium_not_subscribed_line": "❌ **Premium:** Not subscribed — use `/unlock` to enable autologging.",
"premium_free_line": "⚪ **Premium:** Not subscribed — use `/unlock` to subscribe ($2.99/mo). *(Autologs are free for all servers right now.)*",
"what_to_do": "\n\nWhat would you like to do?",
"select_notif_type": "Select the notification type to manage:",
"select_notif_placeholder": "Select notification type",
"logs_option": "Logs",
"logs_option_desc": "Manage Logs notifications",
"points_option": "Points",
"points_option_desc": "Manage Points notifications",
"leaderboard_option": "Leaderboard",
"leaderboard_option_desc": "Manage Leaderboard notifications",
"selected_type": "Selected **{type}**. Now choose the squadron to manage:",
"select_squadron_placeholder": "Select a squadron",
"select_squadron_page_placeholder": "Select a squadron (Page {page})",
"no_squadrons_available": "No squadron available for this notification type.",
"managing_global": "Managing **{type}** (global) in channel **{channel}**.",
"managing_squadron": "Managing **{type}** for squadron **{squadron}** in channel **{channel}**.",
"select_channel": "Select a new channel:",
"select_channel_placeholder": "Select a channel",
"select_channel_page_placeholder": "Select a channel (Page {page})",
"global_toggled": "{type} (global) is now {state}.",
"squadron_toggled": "{type} for **{squadron}** is now {state}.",
"channel_updated_global": "Updated {type} (global) to {channel}",
"channel_updated_squadron": "Updated {type} for **{squadron}** to {channel}",
"diagnose_channel_placeholder": "Select a channel to diagnose...",
"select_channel_diagnose": "Select the channel to diagnose:",
"game_not_logged_title": "Game Not Logged",
"game_not_logged_desc": "Use `/unlock` to subscribe to the **Standard** tier (or higher) to receive automatic game scoreboards.",
"server_not_upgraded_title": "⚠️ Server Not Upgraded",
"server_not_upgraded_autolog_desc": "This server does not have an active Premium subscription.\n\n**Automatic game scoreboards will stop being sent to non-upgraded servers after <t:{deadline}:D>.**\n\nUse `/unlock` to subscribe and keep receiving automatic game logs.",
"replay_not_available": "Replay data isn't available yet — wait a bit then try again!",
"too_many_videos": "Too many videos rendering right now — please try again in a moment.",
"video_gen_failed": "Error generating video: `{error}`",
"video_missing": "Failed to generate replay video - output file missing or empty.",
"video_too_large": "Replay video too large to upload ({file_mb:.1f} MB). Server limit is {limit_mb:.0f} MB.",
"video_web_fallback": "You can also view this match at {url}",
"video_upload_failed": "Video too large to upload — view it on the website:\n{url}",
"video_unexpected_error": "Unexpected error generating replay video: `{error}`",
"replay_not_found": "Replay data not found for session `{session_id}` on disk.",
"chat_log_title": "**Chat Log for Game [{session_id}]({url})**",
"chat_log_part_title": "**Chat Log for Game [{session_id}]({url}) (Part {part}/{total})**",
"chat_log_part_only": "**Chat Log (Part {part}/{total})**",
"no_chat_log": "No chat log found for session `{session_id}`.",
"chat_log_error": "Unexpected error loading chat log: `{error}`",
"battle_log_title": "**Battle Log for Game [{session_id}]({url})**",
"battle_log_part_title": "**Battle Log for Game [{session_id}]({url}) (Part {part}/{total})**",
"battle_log_part_only": "**Battle Log (Part {part}/{total})**",
"no_battle_log": "No combat events found for session `{session_id}`.",
"battle_log_error": "Unexpected error loading battle log: `{error}`",
"points_update_title": "**{squadron} {region} Points Update**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Player Changes:**",
"points_table_header": "Name Change Now\n",
"wl_line": "\n**{squadron}** went **{wins}W-{losses}L** this session",
"placement_rose": "\n**{squadron}** rose to **{new_place}** from **{old_place}**",
"placement_fell": "\n**{squadron}** fell to **{new_place}** from **{old_place}**",
"points_not_logged_title": "Points Not Logged",
"points_not_logged_desc": "Use `/unlock` to subscribe to the **Standard** tier (or higher) to receive automatic points updates.",
"server_not_upgraded_points_desc": "This server does not have an active Premium subscription.\n\n**Automatic updates will stop being sent to non-upgraded servers after <t:{deadline}:D>.**\n\nUse `/unlock` to subscribe and keep receiving automatic updates.",
"leave_title": "⚠️ Player Left {squadron}",
"leave_desc": "**{nick}** ({uid}) has left the squadron.\n\nLast recorded points: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Squadron over your tier cap",
"over_cap_desc": "Your server is on the **{tier}** tier, which allows **{cap} {notif}** squadrons. The squadron **{squadron}** is currently over that limit and not being logged. Upgrade to a higher tier to restore it.",
"over_cap_footer": "Upgrade at srebot-meow.ing/premium or via /unlock",
"wildcard_blocked_title": "Wildcard logging requires a higher tier",
"wildcard_blocked_desc": "Wildcard squadron entries (*, all, everything) are only available on Pro or Max tiers. Your server is on **{tier}** for {notif}. Upgrade to re-enable wildcard logging.",
"cap_header": "{used}/{cap} {notif} enabled — {tier} tier"
},
"track": {
"squadron_not_found": "Squadron not found.",
"fetch_failed": "Failed to fetch squadron info."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Unlock premium features for this server.**\n\nPremium includes:\n> • Auto scoreboard posts\n> • Chat & battle logs\n> • Replay lookups\n> • Unlimited /comp lookups\n> • Priority support\n\n**$2.99 / month · per server · cancel anytime**\n\n⚠️ Discord billing is only available in select countries. If the button below shows **\"Product Unavailable\"**, this may be due to an unsupported country or using a **mobile device**. Use the **Subscribe via Website** button instead.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **This server is already subscribed!**",
"manage_discord_field": "Manage Subscription",
"manage_discord_value": "Your subscription is through **Discord**.\nTo cancel, go to **User Settings → Subscriptions** in Discord.",
"manage_website_field": "Manage Subscription",
"manage_website_value": "Your subscription is through the **website**.\nManage it at [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Coming Soon",
"coming_soon_value": "Premium subscriptions are not yet available. Check back soon!",
"current_tier": "You're on the **{tier}** plan.",
"upgrade_to": "Upgrade to {tier}",
"upgrade_to_value": "Get a higher squadron cap and more features by upgrading to **{tier}**."
},
"language": {
"prompt": "Please select your server language:",
"select_placeholder": "Choose your server language",
"language_set": "Language set to {language}.",
"translate_prompt": "Select a target language below 👇",
"translate_placeholder": "Choose a target language…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Translation unavailable (DeepL not configured)",
"translation_failed": "Translation failed"
},
"misc": {
"credits_title": "Credits",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Lead Developer, Bot Manager, Community Manager\n> **Z3R0** - Developer, Optimization Developer, Database Engineer\n> **Clippii (Heidi) ** - Developer, Website Developer, Community Manager\n> **LivingTheDagor** - Developer, Parser Developer, Consultant\n> **Lux_** - API Engineer, Spectra Developer\n> **Konigallerwaffen** - Feedback and Feature Consultant\n> **Žralok Tonda** - Czech Translator\n> **Styevy**, **Lopais** - German Translators\n> **Susogus**, **playforfun698** - Polish Translators\n> **Bobr** - Russian Translator\n\n\n[Feel like joining us?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "SEASON SCHEDULE",
"schedule_timeslot_label": "{region} TIMESLOT",
"schedule_not_found_title": "Schedule Not Found",
"schedule_not_found_desc": "No schedule data is available yet.",
"news_no_news_title": "No News",
"news_no_news_desc": "There are no announcements right now. Check back later!",
"news_footer": "Thank you for your support! ᕙᘘᗢ",
"help_title": "Bot Guide",
"donate_title": "Support SRE Bot",
"donate_desc": "If you enjoy using SRE Bot and want to support its development, consider buying me a coffee!\n\n**[Donate on Ko-fi](https://ko-fi.com/notsotoothless)**\n\nEvery contribution helps keep the bot running and supports new features. Thank you!",
"status_title": "Bot Status",
"status_last_received": "Last Game Received",
"status_avg_ttl": "Avg TTL (Last 30)",
"status_no_data": "No data yet",
"status_gaijin_slow": "⚠️ Gaijin servers slow",
"help_commands_header": "**Commands Overview**",
"help_links": "For detailed information, read the documentation [here]({docs}) or get support [here]({support}).",
"help_terms": "[Terms of Service]({terms}) • [Privacy Policy]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Squadron Leaderboard",
"top15_desc": "Top 15 squadrons with statistics, sent 35 minutes after timeslot close.\nThis one sent <t:{timestamp}:R>.",
"top30_desc": "Squadrons 16-30 with statistics.",
"not_logged_title": "Leaderboard Not Logged",
"not_logged_desc": "Use `/unlock` to subscribe to the **Standard** tier (or higher) to receive automatic leaderboard updates.",
"server_not_upgraded_title": "⚠️ Server Not Upgraded",
"server_not_upgraded_desc": "This server does not have an active Premium subscription.\n\n**Automatic updates will stop being sent to non-upgraded servers after <t:{deadline}:D>.**\n\nUse `/unlock` to subscribe and keep receiving automatic updates."
},
"stacks": {
"stack_title": "{leader}'s Stack",
"stack_named_title": "{name}",
"no_members": "No members yet.",
"members_field": "Members ({count}/{max})",
"queue_field": "Queue ({count}/{max})",
"manage_title": "Manage Stack",
"no_pending_requests": "No pending requests.",
"disbanded_title": "Stack [Disbanded]",
"disbanded_desc": "This stack was disbanded by the leader.",
"expired_title": "Stack [Expired]",
"expired_desc": "This stack has expired.",
"join_modal_title": "Apply to Join Stack",
"join_vehicle_label": "What will you play?",
"join_vehicle_placeholder": "e.g. F-16C, WZ305...",
"ping_modal_title": "Ping Message",
"ping_message_label": "Custom message (optional)",
"ping_message_placeholder": "e.g. Come now! Stack starting!",
"rename_modal_title": "Rename Stack",
"rename_label": "Stack name",
"rename_placeholder": "e.g. Night Owls, Alpha Squad...",
"select_new_leader": "Select new leader…",
"select_applicants": "Select applicants…",
"no_pending_applications": "No pending applications.",
"select_to_remove": "Select people to remove…",
"no_members_or_applicants": "No members or applicants.",
"select_to_ping": "Select people to ping individually…",
"stack_not_found": "❌ Stack not found.",
"no_longer_exists": "❌ This stack no longer exists.",
"member_not_exists": "❌ That member no longer exists.",
"already_has_stack": "❌ That player already has an active stack.",
"already_member": "❌ You are already a member of this stack.",
"already_applied": "❌ You already have a pending application for this stack.",
"queue_full": "❌ The queue is full ({max}/{max}). Try again later.",
"application_sent": "✅ Application sent! The stack leader will review it.",
"stack_disbanded": "✅ Stack disbanded.",
"cancelled": "Cancelled.",
"select_member_transfer": "❌ Please select a member to transfer ownership to.",
"ownership_transferred": "✅ Ownership transferred to {nick}. You have left the stack.",
"select_applicant_first": "❌ Please select at least one applicant first.",
"stack_full": "❌ Stack is already full ({max}/{max} members).",
"select_person_first": "❌ Please select at least one person first.",
"no_one_to_ping": "❌ No one to ping.",
"ping_footer": "Pinged by {leader} for {stack}.",
"pinged": "✅ Pinged!",
"select_from_dropdown": "❌ Please select at least one person from the dropdown first.",
"stack_renamed": "✅ Stack renamed to **{name}**.",
"only_member_use_disband": "❌ You are the only member. Use **Disband Stack** to end it.",
"select_transfer_prompt": "Select a member to transfer ownership to before leaving:",
"left_stack": "✅ You have left the stack.",
"application_withdrawn": "✅ Your application has been withdrawn.",
"not_member_or_applicant": "❌ You are not a member of or applicant to this stack.",
"leader_only_manage": "❌ Only the stack leader can manage this stack.",
"leader_only_disband": "❌ Only the stack leader can disband this stack.",
"confirm_disband": "Are you sure you want to disband this stack? This cannot be undone.",
"already_active_stack": "⚠️ You already have an active stack. If the original embed is gone (e.g. after a bot restart), you can force disband it and start fresh.",
"force_created": "✅ Previous stack disbanded. New stack created.",
"no_active_stack": "❌ You don't have an active stack. Use `/stack-create` to start one.",
"could_not_parse_channel": "⚠️ Couldn't parse stored channel ID."
},
"commands": {
"common": {
"season": "The season to generate the card for",
"theme": "Card color theme",
"squadron_short": "The short name of the squadron",
"player_username": "The player's username",
"choice_dark": "Dark",
"choice_light": "Light"
},
"comp": {
"description": "Find the last known comps for a given team",
"squadron_short": "The shortname of the enemy team"
},
"quick_log": {
"description": "Quickly set an alarm for this squadron in this channel",
"squadron_name": "The SHORT name of the squadron to monitor",
"type": "Choose Logs, Points, Leaderboard, Weekly BR, or Both",
"choice_logs": "Logs",
"choice_points": "Points",
"choice_leaderboard": "Leaderboard",
"choice_both": "Both (Logs + Points)",
"choice_weekly_br": "Weekly BR"
},
"sq_info": {
"description": "Fetch information about a squadron"
},
"sq_info_graph": {
"description": "Show a roster composition graph by activity and WR (current season)"
},
"sq_card": {
"description": "Generate a season recap card for a squadron",
"squadron": "The short name of the squadron"
},
"sq_stats": {
"description": "Display a squadron's points over time"
},
"loss_calculator": {
"description": "Calculate the point loss if players leave a squadron",
"player1": "Player leaving",
"player_optional": "Player leaving (optional)"
},
"website": {
"description": "Get a link to the SRE Bot website"
},
"card": {
"description": "Generate a season recap card for a player"
},
"player_stats": {
"description": "View detailed vehicle statistics for a player",
"username": "The WT username for stats request",
"uid": "The WT UID for stats request"
},
"view_player_games": {
"description": "View the last 20 games for a player"
},
"view_match": {
"description": "View a match scoreboard by ID or player",
"match_id": "The session hex ID of the match to view",
"player_name": "A player's username to browse recent matches"
},
"compare": {
"description": "Compare aggregate SQB stats between players",
"player1": "First player username",
"player2": "Second player username",
"player_optional": "Additional player username (optional)"
},
"leaderboard": {
"description": "Get the SRE Bot global leaderboard"
},
"set_squadron": {
"description": "Set the squadron tag for this server",
"abbreviated_name": "The short name of the squadron to set"
},
"setup": {
"description": "Set up the bot for this server"
},
"meta_management": {
"description": "Manage meta data access settings for this server"
},
"meta": {
"description": "Search squadron meta roster by vehicle name",
"vehicle": "Vehicle name to search for"
},
"top": {
"description": "Get the top 20 squadrons with detailed stats"
},
"language": {
"description": "Change the bot's language."
},
"translate_message": {
"name": "Translate Message"
},
"sq_track": {
"description": "Track a squadron and compare stats against the last check",
"squadron_short_name": "Short name of the squadron to track"
},
"analytics": {
"description": "View advanced SQB analytics for a squadron",
"view": "Which analytics view to show",
"choice_maps": "Map Win Rates",
"choice_comps": "Team Compositions",
"choice_consistency": "Player Consistency",
"choice_time": "Time of Day",
"choice_matchups": "Matchup History"
},
"recent": {
"description": "Show recent squadron battles for a squadron",
"length": "Number of matches to show"
},
"vs": {
"description": "Head-to-head record between two squadrons",
"squadron_a": "First squadron",
"squadron_b": "Second squadron"
},
"autolog_management": {
"description": "Manage autolog notifications and diagnose permissions"
},
"diagnose_perms": {
"description": "Diagnose autolog permissions for this channel"
},
"unlock": {
"description": "Unlock premium features for this server"
},
"credits": {
"description": "View the team credited for building this"
},
"schedule": {
"description": "View the current season BR schedule"
},
"news": {
"description": "View the latest SRE Bot news and announcements"
},
"help": {
"description": "View the guide, ToS, and support links"
},
"donate": {
"description": "Support the development of SRE Bot"
},
"stack_create": {
"description": "Create a new player stack",
"vehicle": "What vehicle will you start with?"
},
"stack_manage": {
"description": "Re-post your active stack embed to this channel"
},
"bot_status": {
"description": "View bot status: last game received and average TTL"
}
},
"permission": {
"blacklisted_title": "❌ Blacklisted",
"blacklisted_desc": "You are blacklisted from using this command.",
"reason_line": "**Reason:** {reason}",
"access_denied_title": "⛔ Access Denied",
"no_permission_desc": "You do not have permission to use this command.",
"unexpected_error_title": "❗ Error, report this...."
},
"weekly_br": {
"title_wildcard": "Weekly BR Report — {br} BR",
"title_squadron": "Weekly BR Report — [{tag}] {long} • {br} BR",
"window_label": "Window: {start} → {end}",
"wildcard_desc_first": "Top {count} squadrons by ELO • Ranks {low}{high}",
"wildcard_desc_second": "Top {count} squadrons by ELO • Ranks {low}{high}",
"squadron_stats_line": "- {games} games • K/D {kdr} • WR {wr}%",
"top_players_inline_header": "🥇 Top players:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}g)",
"top_players_header": "**Top {count} players by ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} games • K/D {kdr}",
"squadron_header_line": "Squadron ELO: {score} • {games} games • WR {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "Squadron ELO: not enough team activity to score this week.",
"no_data": "No matches recorded for [{tag}] this BR rotation."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Error",
"no_data_title": "Sin datos",
"access_denied_title": "Acceso denegado",
"access_denied_desc": "Este servidor ha sido bloqueado.",
"no_players_selected": "No hay jugadores seleccionados. Selecciona al menos uno.",
"must_use_in_server": "Este comando debe usarse en un servidor.",
"could_not_resolve_channel": "No se pudo resolver el canal seleccionado.",
"failed_update_setting": "❌ Error al actualizar la configuración.",
"configuration_not_found": "Configuración no encontrada.",
"no_channel_selected": "Ningún canal seleccionado.",
"no_selection_received": "No se recibió ninguna selección.",
"database_error": "❌ Error de base de datos: {error}",
"enabled": "Habilitado",
"disabled": "Deshabilitado",
"not_configured": "No configurado",
"unknown": "Desconocido",
"rating_field": "Clasificación",
"battles_field": "Batallas",
"wins_field": "Victorias",
"losses_field": "Derrotas",
"win_rate_field": "% Victorias",
"kills_field": "Eliminaciones",
"deaths_field": "Muertes",
"kd_field": "K/D",
"members_field": "Miembros",
"placement_field": "Posición",
"points_field": "Puntos",
"ground_kills_field": "Elim. terrestres",
"air_kills_field": "Elim. aéreas",
"total_kills_field": "Eliminaciones totales",
"assists_field": "Asistencias",
"captures_field": "Capturas",
"none_option": "Ninguno"
},
"buttons": {
"skip": "Omitir",
"previous": "Anterior",
"next": "Siguiente",
"prev": "Ant.",
"prev_arrow": "◀ Anterior",
"next_arrow": "Siguiente ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Generar gráfico",
"show_graph": "Mostrar gráfico",
"view_player_stats": "📊 Ver estadísticas de jugadores",
"compare_nearby": "📈 Comparar escuadrones cercanos",
"confirm_swap": "Sí, cambiarlo",
"cancel_swap": "No, mantener el anterior",
"set_squadron": "Configurar escuadrón",
"same_as_logs": "Igual que registros",
"require_password": "🔒 Requerir contraseña",
"password_required": "🔒 Contraseña requerida",
"lock_data": "🔐 Vincular datos del escuadrón",
"data_locked": "🔐 Datos vinculados al servidor",
"allow_public": "👥 Permitir meta público",
"public_enabled": "👥 Meta público habilitado",
"update_accounts": "📋 Actualizar cuentas meta",
"change_password": "🔑 Cambiar contraseña",
"help": "❓ Ayuda",
"add_player": " Agregar Jugador",
"update_all": "🔄 Actualizar Todos los Miembros",
"back_to_settings": "⬅ Volver a Configuración",
"manage_notifications": "Gestionar Notificaciones",
"diagnose_permissions": "Diagnosticar Permisos",
"enable": "Habilitar",
"disable": "Deshabilitar",
"change_channel": "Cambiar Canal",
"view_replay": "Ver Repetición",
"view_website": "Ver en el Sitio Web",
"view_video": "Ver Video",
"view_log": "Ver Registro",
"view_chat": "Ver Chat",
"subscribe_website": "Suscribirse desde el Sitio Web",
"yes_disband": "Sí, disolver",
"cancel": "Cancelar",
"transfer_leave": "Transferir y salir",
"accept_selected": "Aceptar seleccionados",
"accept_all": "Aceptar todos",
"decline_selected": "Rechazar seleccionados",
"back": "Volver",
"remove_all": "Eliminar todos",
"remove_active": "Eliminar activos",
"remove_queued": "Eliminar en espera",
"remove_selected": "Eliminar seleccionados",
"ping_all": "Notificar a todos",
"ping_active": "Notificar activos",
"ping_queued": "Notificar en espera",
"ping_selected": "Notificar seleccionados",
"accept_members": "Aceptar miembros",
"remove_members": "Eliminar miembros",
"ping_members": "Notificar miembros",
"rename_stack": "Renombrar stack",
"request_to_join": "Solicitar unirse",
"leave_withdraw": "Salir / Retirar",
"manage_stack": "Gestionar stack ⚙️",
"disband_stack": "Disolver stack",
"force_disband_create": "Forzar disolución y crear nuevo"
},
"events": {
"guild_join_title": "¡Gracias por agregarme!",
"guild_join_desc": "Ejecuta `/setup` para configurar el bot en este servidor."
},
"comp": {
"not_found_title": "Comps no encontradas",
"not_found_desc": "Sin datos para **{squadron}**, inténtalo más tarde.",
"error_loading_title": "Error al cargar comps",
"error_loading_desc": "Error al cargar datos de comp: {error}",
"title": "Comps de {squadron}",
"desc": "Comps vistas en los últimos {minutes} minutos",
"no_recent_title": "Sin comps recientes",
"no_recent_desc": "Sin comps en los últimos {minutes} minutos.",
"comp_title": "COMP {index}",
"last_seen_label": "**Visto por última vez** : {timestamp}{warning}",
"comp_label": "**Comp**: {notation}",
"no_players_recorded": "No hay jugadores registrados.",
"limit_reached_title": "Límite de comps alcanzado",
"limit_reached_desc": "Este servidor ha usado las {limit} consultas de comps para este horario. Suscríbete (con /unlock) para acceso ilimitado o espera al siguiente horario.",
"remaining_footer": "{remaining}/{limit} consultas de comps restantes en este horario"
},
"quick_log": {
"invalid_type": "El tipo solo puede ser Logs, Puntos, Clasificación, BR Semanal o Ambos.",
"squadron_required": "Debes indicar el nombre del escuadrón para alarmas de Registros, Puntos o Ambos.",
"wildcard_logs_only": "Solo los Registros pueden configurarse con escuadrón comodín.",
"squadron_not_resolved": "No se pudo resolver el escuadrón `{squadron}`.",
"save_failed": "Error al guardar preferencias. Inténtalo de nuevo más tarde.",
"premium_warning": "\n\n> ⚠️ **Los registros de partidas requieren Premium.** Ejecuta `/unlock` para suscribirte ($2.99/mes) — los registros no se publicarán hasta entonces.",
"leaderboard_set": "La alarma de Clasificación Global se ha configurado en este canal.",
"both_set": "Las alarmas de Registros y Puntos para {squadron} se han configurado en este canal.{premium_note}",
"alarm_set": "La alarma de {alarm_type} para {squadron} se ha configurado en este canal.{premium_note}",
"weekly_br_wildcard_set": "Informe BR Semanal (top 20 escuadrones) configurado para este canal. Se envía al final de cada rotación de BR.",
"weekly_br_squadron_set": "Informe BR Semanal para {squadron} (top 15 jugadores) configurado para este canal. Se envía al final de cada rotación de BR."
},
"diagnostics": {
"title": "Diagnóstico de Autolog",
"channel_permissions_header": "**Permisos del Canal** (<#{channel_id}>)",
"perms_needed": " ^ El autoregistro necesita todos los permisos anteriores para enviar marcadores.",
"server_squadron_header": "**Escuadrón del Servidor** (`/set-squadron`)",
"server_squadron_short": " Corto: `{short}`",
"server_squadron_long": " Largo: `{long}`",
"server_squadron_not_set": " No configurado (el color de la barra del marcador mostrará 'not_set')",
"autolog_prefs_header": "**Preferencias de Autolog** (`/quick-log`)",
"autolog_none_configured": " ❌ NINGUNO configurado - el autoregistro NO enviará nada a este servidor.",
"autolog_setup_hint": " Usa `/quick-log <squadron_short> Logs` en el canal de destino para configurarlo.",
"autolog_no_logs_channels": " ❌ No hay canales de Registros configurados. Solo se encontraron canales de Puntos/Clasificación.",
"autolog_enable_hint": " Usa `/quick-log <squadron_short> Logs` para habilitar el autoregistro.",
"selected_channel_tag": " **(canal seleccionado)**",
"missing_send_attach": " (sin permiso de envío/adjunto)",
"channel_not_found": " (canal no encontrado)",
"invalid_channel_id": " (ID de canal inválido)",
"premium_status_header": "**Estado Premium** (`/unlock`)",
"premium_active": " ✅ Este servidor tiene una suscripción Premium activa.",
"premium_not_subscribed": " ❌ Este servidor **no** tiene una suscripción Premium.",
"premium_autolog_required": " El autoregistro requiere Premium. Usa `/unlock` para suscribirte.",
"premium_not_subscribed_free": " ⚪ Sin suscripción — usa `/unlock` para suscribirte ($2.99/mes).",
"premium_free_note": " *(Los autologs son gratuitos para todos los servidores por ahora.)*"
},
"sq_info": {
"title": "Info del escuadrón: {squadron}",
"placement_field": "Posición",
"total_points_field": "Puntos totales",
"total_members_field": "Total de miembros",
"members_field": "Miembros",
"fetch_failed": "Error al obtener información del escuadrón."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Temporada {season})",
"embed_title": "{squadron} — Composición de la plantilla",
"embed_desc": "Temporada **{season}** · Mediana de partidas: **{median}** · Núcleo: **{core}** · Activos: **{active}** · Débiles: **{weak}**\nBarras ordenadas por partidas desc; altura = tasa de victoria. Núcleo = top 30 % de TV y partidas ≥ mediana. Activos = top 3045 % de TV y partidas ≈ mediana. Débiles = el resto.",
"core_threshold_line": "NÚCLEO ≥ {wr} %",
"weak_threshold_line": "DÉBILES < {wr} %",
"y_label": "Tasa de victoria",
"core_header": "NÚCLEO — {count} · TV {avg}%",
"active_header": "ACTIVOS — {count} · TV {avg}%",
"weak_header": "DÉBILES — {count} · TV {avg}%",
"no_active_season": "No se encontró ninguna temporada activa. Inténtalo de nuevo cuando comience la siguiente.",
"no_members": "No se encontraron miembros actuales para {squadron}."
},
"recap_card": {
"unknown_season": "Temporada desconocida: `{season}`.",
"no_clan_id": "No se pudo resolver el ID del escuadrón `{squadron}`.",
"render_failed": "Error al generar la tarjeta de resumen de temporada. Inténtalo más tarde."
},
"sq_stats": {
"no_data_title": "Sin datos",
"no_data_desc": "No se encontraron datos históricos para el escuadrón: {squadron}",
"title": "{squadron} // ESCUADRÓN",
"desc": "Tendencia de puntuación total (últimos {count} puntos de datos)",
"previous_score_field": "Puntuación anterior",
"current_score_field": "Puntuación actual",
"change_field": "Cambio",
"player_title": "{squadron} // JUGADORES",
"player_desc": "Tendencias de puntos individuales por jugador",
"comparison_title": "{squadron} // COMPARACIÓN EN CLASIFICACIÓN",
"comparison_desc": "Comparando con escuadrones clasificados {range}",
"current_position_field": "Posición actual",
"squadrons_shown_field": "Escuadrones mostrados",
"squadron_not_found_error": "Escuadrón no encontrado en la clasificación",
"no_nearby_error": "No se encontraron escuadrones cercanos",
"no_historical_error": "No se encontraron datos históricos para escuadrones cercanos",
"comparison_chart_failed": "Error al generar el gráfico de comparación",
"select_players_placeholder": "Selecciona jugadores (Página {page})"
},
"loss_calc": {
"title": "Pérdida de puntos — {squadron}",
"players_leaving_field": "Jugadores que se van",
"share_of_total_field": "% del Total",
"points_lost_real_field": "Puntos perdidos (real)",
"points_lost_raw_field": "Puntos perdidos (bruto)",
"squadron_rating_field": "Clasificación del escuadrón",
"squadron_position_field": "Posición del escuadrón",
"positions_lost_field": "Posiciones perdidas",
"not_found_footer": "No encontrado en el escuadrón: {players}",
"fetch_failed": "Error al obtener datos del escuadrón: {error}",
"no_point_data": "No hay datos de puntos disponibles para este escuadrón.",
"no_matching_players": "No se encontraron jugadores coincidentes en **{squadron}**."
},
"player": {
"select_player_placeholder": "Selecciona un jugador",
"no_stats_found": "❌ No se encontraron estadísticas para el UID: {uid}",
"no_vehicle_stats": "❌ No se encontraron estadísticas de vehículos para este jugador.",
"vehicles_found": "Se encontraron **{count}** vehículos para **{nick}**\nSelecciona un vehículo para ver estadísticas detalladas:",
"vehicle_select_placeholder": "Selecciona un vehículo (Página {page}/{total})",
"combat_stats_header": "**__ESTADÍSTICAS DE COMBATE__**",
"ground_kills_label": "**Elim. terrestres:** {value}",
"air_kills_label": "**Elim. aéreas:** {value}",
"total_kills_label": "**Eliminaciones totales:** {value}",
"assists_label": "**Asistencias:** {value}",
"deaths_label": "**Muertes:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Capturas:** {value}",
"battle_record_header": "**__HISTORIAL DE BATALLAS__**",
"total_battles_label": "**Batallas totales:** {value}",
"wins_label": "**Victorias:** {value}",
"losses_label": "**Derrotas:** {value}",
"win_rate_label": "**% Victorias:** {value}%",
"stats_desc": "Estadísticas de **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Jugador no encontrado",
"not_found_desc": "No se encontró historial de partidas para `{player}`.",
"no_players_found": "No se encontraron jugadores que coincidan con **{username}**\nIntenta usar `/website` para buscar en el sitio web.",
"multiple_matches": "Se encontraron varias coincidencias, elige la correcta:",
"must_provide_input": "Debes proporcionar al menos un UID o nombre de usuario."
},
"player_games": {
"no_recent_title": "Sin partidas recientes",
"no_recent_desc": "No se encontraron partidas para **{player}** en las últimas 8 horas.",
"squadron_label": "**Escuadrón:** {squadron}",
"record_label": "**V:** {wins} **D:** {losses} **%V:** {wr}%",
"comps_played_header": "\n\n**Comps jugadas**"
},
"match": {
"missing_input_title": "Entrada faltante",
"missing_input_desc": "Proporciona un `match_id` o un `player_name`.",
"not_found_title": "Partida no encontrada",
"not_found_desc": "No se encontró una partida con el ID `{match_id}`.",
"invalid_data_title": "Datos de partida inválidos",
"invalid_data_desc": "No se pudieron analizar los datos de la repetición.",
"scoreboard_error_title": "Error en el marcador",
"scoreboard_error_desc": "Error al generar la imagen del marcador.",
"no_games_title": "Sin partidas",
"no_games_desc": "No se encontró historial de partidas para **{player}**.",
"recent_matches_title": "Partidas recientes de {player}",
"recent_matches_desc": "Mostrando hasta {count} partidas recientes. Selecciona una para ver el marcador completo.",
"select_match_placeholder": "Selecciona una partida para ver..."
},
"compare": {
"no_players_found": "No se encontraron jugadores que coincidan con **{name}**.",
"multiple_matches": "Varias coincidencias para **{name}**: {matches}\nUsa un nombre más específico (las sugerencias del autocompletado son exactas).",
"could_not_resolve": "No se pudieron resolver los jugadores.",
"could_not_fetch": "❌ No se pudieron obtener estadísticas de **{name}**.",
"no_graph_data": "No hay datos disponibles de los últimos 90 días.",
"no_squadron_points_data": "No hay datos de puntos del escuadrón para {names} (jugador no encontrado en el historial del escuadrón rastreado).",
"graph_title": "Puntos del jugador — últimos 90 días",
"battles_label": "Batallas",
"wins_label": "Victorias",
"losses_label": "Derrotas",
"win_rate_label": "% Victorias",
"ground_kills_label": "Elim. terrestres",
"air_kills_label": "Elim. aéreas",
"total_kills_label": "Eliminaciones totales",
"assists_label": "Asistencias",
"deaths_label": "Muertes",
"kd_label": "K/D",
"captures_label": "Capturas"
},
"squadron": {
"not_found_desc": "Escuadrón `{squadron}` no encontrado.",
"set_title": "✅ Escuadrón configurado",
"set_desc": "El escuadrón **{squadron}** ha sido configurado para este servidor.",
"short_name_field": "Nombre corto",
"long_name_field": "Nombre largo",
"swap_title": "✅ Escuadrón cambiado",
"swap_desc": "Se reemplazó **{old}** por **{new}** en este servidor.",
"already_set_title": "⚠️ Escuadrón ya configurado",
"already_set_desc": "Este servidor está configurado actualmente con **{old}**.\n¿Cambiarlo a **{new}**?",
"swap_cancelled": "❌ Cambio de escuadrón cancelado."
},
"setup": {
"step1_title": "Configuración del servidor — Paso 1 de 3",
"step1_desc": "Este asistente te guiará para configurar el bot en tu servidor.\n\n**Paso 1** — Configura tu escuadrón\n**Paso 2** — Elige un canal de registros\n**Paso 3** — Elige un canal de puntos\n",
"step1_current_sq": "\nEscuadrón configurado actualmente: **[{short}] {long}**",
"step2_title": "Configuración del servidor — Paso 2 de 3",
"step2_desc": "Escuadrón configurado como **[{short}] {long}**.\n\n¿Dónde deben publicarse los **registros de batalla**?\nSelecciona un canal de texto a continuación, o salta este paso.",
"step3_title": "Configuración del servidor — Paso 3 de 3",
"step3_desc": "¿Dónde deben publicarse las **notificaciones de puntos**?\nSelecciona un canal de texto a continuación, o salta este paso.",
"step3_same_as_logs": "\n\nTambién puedes hacer clic en \"Igual que registros\" para reutilizar el canal de registros.",
"summary_title": "Configuración completa",
"summary_desc": "Puedes usar `/autolog-management` para cambiar estas opciones más tarde.",
"squadron_field": "Escuadrón",
"logs_channel_field": "Canal de registros",
"points_channel_field": "Canal de puntos",
"premium_required_field": "⚠️ Los registros de partidas requieren Premium",
"premium_required_value": "Los marcadores automáticos de partidas no se publicarán hasta que el servidor tenga una suscripción activa. Ejecuta `/unlock` para suscribirte ($2.99/mes).",
"modal_title": "Configurar escuadrón",
"modal_label": "Nombre corto del escuadrón",
"modal_placeholder": "ej. AXYS",
"squadron_not_found": "Escuadrón `{squadron}` no encontrado. Inténtalo de nuevo.",
"logs_channel_placeholder": "Selecciona un canal de registros...",
"points_channel_placeholder": "Selecciona un canal de puntos..."
},
"meta_management": {
"squadron_not_found_title": "❌ Escuadrón no encontrado",
"squadron_not_found_desc": "No se encontró el ID de clan para el escuadrón: **{squadron}**",
"access_denied_title": "❌ Acceso denegado",
"access_denied_desc": "Contraseña incorrecta. Los datos meta de este escuadrón están protegidos.",
"data_locked_title": "🔐 Datos del escuadrón vinculados",
"data_locked_desc": "**{squadron}** tiene la vinculación de datos habilitada y no puede transferirse a otro servidor.\n\nEl propietario del escuadrón debe deshabilitar **Vincular datos del escuadrón** antes de que pueda trasladarse.",
"error_retrieving_settings": "❌ Error al obtener la configuración del servidor después de la transferencia. Inténtalo de nuevo.",
"error_retrieving_settings_retry": "❌ Error al obtener la configuración del servidor. Intenta ejecutar el comando de nuevo.",
"authenticated_title": "✅ Autenticado",
"authenticated_desc": "Contraseña verificada. Gestionando la configuración de **{squadron}**.",
"claimed_title": "✅ Escuadrón reclamado",
"claimed_desc": "**{squadron}** ha sido reclamado exitosamente para este servidor.",
"password_requirement_field": "🔒 Requisito de contraseña",
"data_lock_field": "🔐 Vinculación de datos del escuadrón",
"public_meta_field": "👥 Acceso meta público",
"access_password_field": "🔑 Contraseña de acceso",
"enabled_value": "✅ Habilitado",
"disabled_value": "❌ Deshabilitado",
"settings_title": "🔐 Configuración de gestión meta",
"settings_desc": "**Escuadrón:** {squadron}\n**ID de Clan:** {clan_id}",
"first_time_title": "🔐 Gestión meta - Primera configuración",
"first_time_owner_desc": "**Escuadrón:** {squadron}\n**ID de Clan:** {clan_id}\n\n🔑 Tu contraseña de acceso ha sido generada. **Guarda esta contraseña** — la necesitarás para autenticar el acceso a los datos meta en el futuro.\n\n**Contraseña:** `{password}`",
"first_time_non_owner_desc": "**Escuadrón:** {squadron}\n**ID de Clan:** {clan_id}\n\nEl escuadrón ha sido configurado. Pídele la contraseña de acceso al propietario del servidor.",
"settings_field": "Configuración",
"settings_hint": "Usa los botones de abajo para configurar los ajustes de acceso.",
"password_toggled": "✅ Requisito de contraseña: **{state}**",
"lock_toggled": "✅ Vinculación de datos del escuadrón: **{state}**",
"public_meta_toggled": "✅ Acceso meta público: **{state}**\n{detail}",
"public_meta_enabled_detail": "Los no administradores ahora pueden usar el comando `/meta`.",
"public_meta_disabled_detail": "Solo los administradores pueden usar el comando `/meta`.",
"owner_only_password": "❌ Solo el propietario del servidor puede cambiar la contraseña del escuadrón.",
"help_title": "📖 Ayuda de gestión meta",
"help_desc": "Explicación de cada ajuste y función:",
"help_password_field": "🔑 Contraseña de Acceso",
"help_password_value": "La contraseña de acceso de tu escuadrón. Solo el **propietario del servidor** puede ver la contraseña en el panel de configuración. Cualquier persona con la contraseña puede reclamar los datos meta de tu escuadrón en su servidor, así que mantenla segura.",
"help_require_field": "🔒 Requerir contraseña",
"help_require_value": "Cuando está habilitado, incluso los administradores de este servidor deben ingresar la contraseña del escuadrón para acceder a `/meta-management`. Agrega una capa extra de seguridad para evitar cambios accidentales.",
"help_lock_field": "🔐 Vincular datos del escuadrón",
"help_lock_value": "Cuando está habilitado, impide que el escuadrón sea transferido a otros servidores, incluso con la contraseña correcta. Debe deshabilitarse antes de que el escuadrón pueda transferirse.",
"help_public_field": "👥 Permitir meta público",
"help_public_value": "Cuando está habilitado, permite que los miembros no administradores usen el comando `/meta` para buscar vehículos del escuadrón. Cuando está deshabilitado, solo los administradores del servidor pueden usar `/meta`.",
"help_accounts_field": "📋 Actualizar cuentas meta",
"help_accounts_value": "Abre el gestor de lista de jugadores donde puedes agregar o eliminar jugadores del roster meta de tu escuadrón. Usa **Actualizar Todos los Miembros** para sincronizar todo tu escuadrón a la vez.",
"help_change_pw_field": "🔑 Cambiar contraseña",
"help_change_pw_value": "**Solo el propietario del servidor.** Cambia la contraseña de acceso del escuadrón y establece una pista opcional. La pista se muestra en el aviso de contraseña para ayudar a recordarla.",
"password_modal_title": "Contraseña de acceso del escuadrón",
"password_modal_label": "Ingresa la contraseña del escuadrón",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Cambiar contraseña del escuadrón",
"current_password_label": "Contraseña actual",
"current_password_placeholder": "Ingresa tu contraseña actual",
"new_password_label": "Nueva contraseña",
"new_password_placeholder": "Ingresa tu nueva contraseña",
"confirm_password_label": "Confirmar nueva contraseña",
"confirm_password_placeholder": "Vuelve a ingresar tu nueva contraseña",
"hint_label": "Pista de Contraseña (Opcional)",
"hint_placeholder": "Una pista para recordar la contraseña",
"pw_incorrect": "❌ La contraseña actual es incorrecta.",
"pw_mismatch": "❌ Las contraseñas nuevas no coinciden. Inténtalo de nuevo.",
"pw_empty": "❌ La nueva contraseña no puede estar vacía.",
"pw_changed": "✅ Contraseña actualizada correctamente para **{squadron}**.\n**Nueva Contraseña:** `{password}`",
"pw_changed_hint": "\n**Pista:** {hint}",
"player_add_modal_title": "Agregar Jugador al Roster Meta",
"player_add_label": "UID o Apodo del Jugador",
"player_add_placeholder": "Ingresa el UID del jugador (ej. 12345678) o su apodo",
"player_not_found": "❌ Jugador `{player}` no encontrado en la base de datos Players_Global.\n",
"roster_title": "📋 Gestión de Roster Meta - {squadron}",
"roster_desc": "**ID de Clan del Escuadrón:** {clan_id}\n**Total de Jugadores:** {count}",
"roster_page_field": "Jugadores (Página {page}/{total})",
"no_players_field": "Sin Jugadores",
"no_players_hint": "Aún no hay jugadores en el roster meta. Haz clic en **Agregar Jugador** para comenzar.",
"remove_player_placeholder": "Selecciona el jugador a eliminar...",
"fetch_members_failed": "❌ Error al obtener los miembros del escuadrón: {error}",
"no_members_found": "❌ No se encontraron miembros en el escuadrón o la llamada a la API falló.",
"roster_synced": "✅ Roster sincronizado con el escuadrón.",
"roster_added": "**+{count}** agregados",
"roster_removed": "**-{count}** eliminados (salieron del escuadrón)",
"roster_up_to_date": "**{count}** ya actualizados",
"refreshing_vehicles": "Actualizando datos de vehículos en segundo plano..."
},
"meta": {
"not_configured": "❌ Datos meta no configurados para este servidor. Ejecuta `/meta-management` primero.",
"no_permission": "❌ Necesitas permisos de administrador para usar este comando.\nLos administradores pueden habilitar el acceso público desde `/meta-management`.",
"no_results": "❌ Ningún jugador en el roster de tu escuadrón tiene **{vehicle}**.",
"no_results_admin_hint": "\n*¿Esperas que alguien lo tenga? Haz clic en el botón de actualizar miembros en `/meta-management` y verifica.*",
"search_title": "🔍 Resultados de Búsqueda - {vehicle}",
"matches_found": "**Coincidencias Encontradas:** {count} jugador(es)",
"spawns_label": "Apariciones",
"deaths_label": "Muertes",
"gk_label": "ET",
"ak_label": "EA",
"points_label": "Puntos",
"kdr_label": "KDR",
"games_label": "Partidas",
"no_points": "—"
},
"top": {
"title": "**Top 20 Escuadrones**",
"rating_label": "**Clasificación:** {value}",
"air_kills_label": "**Elim. Aéreas:** {value}",
"ground_kills_label": "**Elim. Terrestres:** {value}",
"deaths_label": "**Muertes:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**% Victorias:** {value}",
"playtime_label": "**Tiempo de Juego:** {value}",
"fetch_failed": "Error al obtener datos del escuadrón."
},
"analytics": {
"no_data_title": "Sin Datos",
"no_matches_desc": "No se encontraron partidas.",
"no_comp_desc": "No se encontraron datos de composición.",
"no_consistency_desc": "Datos de jugadores insuficientes (mínimo 50 partidas).",
"no_time_desc": "No se encontraron datos de tiempo.",
"unknown_view": "Vista desconocida.",
"map_title": "% Victorias por Mapa: {squadron}",
"comp_title": "Composiciones de Equipo: {squadron}",
"consistency_title": "Consistencia de Jugadores: {squadron}",
"consistency_desc": "Ordenado por ratio K/D",
"time_title": "Rendimiento por Hora del Día: {squadron}",
"eu_timeslot": "\n**Franja Horaria EU**",
"na_timeslot": "\n**Franja Horaria NA**",
"off_peak": "\n**Horas Bajas**",
"matchups_title": "📜 {squadron} — Historial de Enfrentamientos",
"matchups_won_field": "🏆 Más Victorias Contra",
"matchups_lost_field": "💀 Más Derrotas Contra",
"no_matchups_desc": "No hay partidas registradas contra otros escuadrones."
},
"recent": {
"title": "Partidas Recientes: {squadron}",
"no_matches_desc": "No se encontraron partidas para este escuadrón."
},
"h2h": {
"two_required_title": "Se Requieren Dos Escuadrones",
"two_required_desc": "Proporciona al menos un escuadrón, o usa `/set-squadron` e indica el escuadrón oponente.",
"provide_a_desc": "Proporciona `squadron_a` o usa `/set-squadron` primero.",
"provide_b_desc": "Proporciona `squadron_b` o usa `/set-squadron` primero.",
"squadron_not_found_title": "Escuadrón No Encontrado",
"same_squadron_title": "Mismo Escuadrón",
"same_squadron_desc": "No puedes verificar el cara a cara contra ti mismo.",
"record_desc": "**Historial:** {a_wins}V - {b_wins}D ({total} partidas)",
"no_matches_desc": "No hay partidas registradas entre **{a}** y **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Activo — el autoregistro está habilitado para este servidor.",
"premium_not_subscribed_line": "❌ **Premium:** Sin suscripción — usa `/unlock` para habilitar el autoregistro.",
"premium_free_line": "⚪ **Premium:** Sin suscripción — usa `/unlock` para suscribirte ($2.99/mes). *(Los autologs son gratuitos para todos los servidores por ahora.)*",
"what_to_do": "\n\n¿Qué deseas hacer?",
"select_notif_type": "Selecciona el tipo de notificación a gestionar:",
"select_notif_placeholder": "Selecciona el tipo de notificación",
"logs_option": "Registros",
"logs_option_desc": "Gestionar notificaciones de Registros",
"points_option": "Puntos",
"points_option_desc": "Gestionar notificaciones de Puntos",
"leaderboard_option": "Clasificación",
"leaderboard_option_desc": "Gestionar notificaciones de Clasificación",
"selected_type": "Seleccionado **{type}**. Ahora elige el escuadrón a gestionar:",
"select_squadron_placeholder": "Selecciona un escuadrón",
"select_squadron_page_placeholder": "Selecciona un escuadrón (Página {page})",
"no_squadrons_available": "No hay escuadrones disponibles para este tipo de notificación.",
"managing_global": "Gestionando **{type}** (global) en el canal **{channel}**.",
"managing_squadron": "Gestionando **{type}** para el escuadrón **{squadron}** en el canal **{channel}**.",
"select_channel": "Selecciona un nuevo canal:",
"select_channel_placeholder": "Selecciona un canal",
"select_channel_page_placeholder": "Selecciona un canal (Página {page})",
"global_toggled": "{type} (global) está ahora {state}.",
"squadron_toggled": "{type} para **{squadron}** está ahora {state}.",
"channel_updated_global": "Se actualizó {type} (global) a {channel}",
"channel_updated_squadron": "Se actualizó {type} para **{squadron}** a {channel}",
"diagnose_channel_placeholder": "Selecciona un canal para diagnosticar...",
"select_channel_diagnose": "Selecciona el canal a diagnosticar:",
"game_not_logged_title": "Partida No Registrada",
"game_not_logged_desc": "Usa `/unlock` para suscribirte al nivel **Standard** (o superior) y recibir marcadores automáticos de partidas.",
"server_not_upgraded_title": "⚠️ Servidor No Actualizado",
"server_not_upgraded_autolog_desc": "Este servidor no tiene una suscripción Premium activa.\n\n**Los marcadores automáticos de partidas dejarán de enviarse a servidores no actualizados después del <t:{deadline}:D>.**\n\nUsa `/unlock` para suscribirte y seguir recibiendo registros automáticos de partidas.",
"replay_not_available": "Los datos de la repetición aún no están disponibles — ¡espera un momento e inténtalo de nuevo!",
"too_many_videos": "Demasiados videos en proceso ahora mismo — inténtalo de nuevo en un momento.",
"video_gen_failed": "Error al generar el video: `{error}`",
"video_missing": "Error al generar el video de repetición - el archivo de salida falta o está vacío.",
"video_too_large": "El video de repetición es demasiado grande para subir ({file_mb:.1f} MB). El límite del servidor es {limit_mb:.0f} MB.",
"video_web_fallback": "También puedes ver esta partida en {url}",
"video_upload_failed": "El video es demasiado grande para subir — véalo en el sitio web:\n{url}",
"video_unexpected_error": "Error inesperado al generar el video de repetición: `{error}`",
"replay_not_found": "Datos de repetición no encontrados para la sesión `{session_id}` en disco.",
"chat_log_title": "**Registro de Chat para la Partida [{session_id}]({url})**",
"chat_log_part_title": "**Registro de Chat para la Partida [{session_id}]({url}) (Parte {part}/{total})**",
"chat_log_part_only": "**Registro de Chat (Parte {part}/{total})**",
"no_chat_log": "No se encontró registro de chat para la sesión `{session_id}`.",
"chat_log_error": "Error inesperado al cargar el registro de chat: `{error}`",
"battle_log_title": "**Registro de Batalla para la Partida [{session_id}]({url})**",
"battle_log_part_title": "**Registro de Batalla para la Partida [{session_id}]({url}) (Parte {part}/{total})**",
"battle_log_part_only": "**Registro de Batalla (Parte {part}/{total})**",
"no_battle_log": "No se encontraron eventos de combate para la sesión `{session_id}`.",
"battle_log_error": "Error inesperado al cargar el registro de batalla: `{error}`",
"points_update_title": "**{squadron} {region} Actualización de Puntos**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Cambios de Jugadores:**",
"points_table_header": "Nombre Cambio Ahora\n",
"wl_line": "\n**{squadron}** fue **{wins}V-{losses}D** esta sesión",
"placement_rose": "\n**{squadron}** subió al **{new_place}** desde el **{old_place}**",
"placement_fell": "\n**{squadron}** bajó al **{new_place}** desde el **{old_place}**",
"points_not_logged_title": "Puntos No Registrados",
"points_not_logged_desc": "Usa `/unlock` para suscribirte al nivel **Standard** (o superior) y recibir actualizaciones automáticas de puntos.",
"server_not_upgraded_points_desc": "Este servidor no tiene una suscripción Premium activa.\n\n**Las actualizaciones automáticas dejarán de enviarse a servidores no actualizados después del <t:{deadline}:D>.**\n\nUsa `/unlock` para suscribirte y seguir recibiendo actualizaciones automáticas.",
"leave_title": "⚠️ Jugador Abandonó {squadron}",
"leave_desc": "**{nick}** ({uid}) ha abandonado el escuadrón.\n\nÚltimos puntos registrados: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Escuadrón sobre el límite de tu nivel",
"over_cap_desc": "Tu servidor está en el nivel **{tier}**, que permite **{cap} {notif}** escuadrones. El escuadrón **{squadron}** supera el límite y no se está registrando. Mejora tu nivel para restaurarlo.",
"over_cap_footer": "Mejora en srebot-meow.ing/premium o con /unlock",
"wildcard_blocked_title": "Wildcard requiere un nivel superior",
"wildcard_blocked_desc": "Las entradas wildcard (*, all, everything) sólo están disponibles en Pro o Max. Tu servidor está en **{tier}** para {notif}. Mejora para habilitarlo.",
"cap_header": "{used}/{cap} {notif} activos — nivel {tier}"
},
"track": {
"squadron_not_found": "Escuadrón no encontrado.",
"fetch_failed": "Error al obtener información del escuadrón."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Desbloquea funciones premium para este servidor.**\n\nPremium incluye:\n> • Publicación automática de marcadores\n> • Registros de chat y batalla\n> • Búsqueda de repeticiones\n> • Consultas de /comp ilimitadas\n> • Soporte prioritario\n\n**$2.99 / mes · por servidor · cancela cuando quieras**\n\n⚠️ La facturación de Discord solo está disponible en países seleccionados. Si el botón de abajo muestra **\"Producto No Disponible\"**, puede ser por un país no compatible o el uso de un **dispositivo móvil**. Usa el botón **Suscribirse desde el Sitio Web** en su lugar.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **¡Este servidor ya está suscrito!**",
"manage_discord_field": "Gestionar Suscripción",
"manage_discord_value": "Tu suscripción es a través de **Discord**.\nPara cancelar, ve a **Configuración de Usuario → Suscripciones** en Discord.",
"manage_website_field": "Gestionar Suscripción",
"manage_website_value": "Tu suscripción es a través del **sitio web**.\nAdministrala en [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Próximamente",
"coming_soon_value": "Las suscripciones Premium aún no están disponibles. ¡Vuelve pronto!",
"current_tier": "Estás en el plan **{tier}**.",
"upgrade_to": "Mejorar a {tier}",
"upgrade_to_value": "Más escuadrones y funciones mejorando a **{tier}**."
},
"language": {
"prompt": "Selecciona el idioma de tu servidor:",
"select_placeholder": "Elige el idioma de tu servidor",
"language_set": "Idioma configurado a {language}.",
"translate_prompt": "Selecciona un idioma de destino abajo 👇",
"translate_placeholder": "Elige un idioma de destino…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Traducción no disponible (DeepL no configurado)",
"translation_failed": "Error de traducción"
},
"misc": {
"credits_title": "Créditos",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Desarrollador Principal, Manager del Bot, Manager de Comunidad\n> **Z3R0** - Desarrollador, Desarrollador de Optimización, Ingeniero de Bases de Datos\n> **Clippii (Heidi)** - Desarrollador, Desarrollador de Sitio Web, Manager de Comunidad\n> **LivingTheDagor** - Desarrollador, Desarrollador de Parser, Consultor\n> **Lux_** - Ingeniero de API, Desarrollador de Spectra\n> **Konigallerwaffen** - Consultor de Feedback y Funcionalidades\n> **Žralok Tonda** - Traductor Checo\n> **Styevy**, **Lopais** - Traductores Alemanes\n> **Susogus**, **playforfun698** - Traductores Polacos\n> **Bobr** - Traductor Ruso\n\n\n[¿Te gustaría unirte?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "CALENDARIO DE TEMPORADA",
"schedule_not_found_title": "Calendario No Encontrado",
"schedule_not_found_desc": "Aún no hay datos del calendario disponibles.",
"news_no_news_title": "Sin Novedades",
"news_no_news_desc": "No hay anuncios en este momento. ¡Vuelve más tarde!",
"news_footer": "¡Gracias por tu apoyo! ᕙᘘᗢ",
"help_title": "Guía del Bot",
"donate_title": "Apoya a SRE Bot",
"donate_desc": "Si disfrutas usar SRE Bot y quieres apoyar su desarrollo, ¡considera invitarme un café!\n\n**[Donar en Ko-fi](https://ko-fi.com/notsotoothless)**\n\nCada contribución ayuda a mantener el bot en funcionamiento y apoya nuevas funciones. ¡Gracias!",
"status_title": "Estado del bot",
"status_last_received": "Última partida recibida",
"status_avg_ttl": "TTL promedio (últimas 30)",
"status_no_data": "Sin datos aún",
"status_gaijin_slow": "⚠️ Servidores de Gaijin lentos",
"help_commands_header": "**Resumen de comandos**",
"help_links": "Para más detalles, lee la documentación [aquí]({docs}) o pide soporte [aquí]({support}).",
"help_terms": "[Términos de servicio]({terms}) • [Política de privacidad]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Clasificación de Escuadrones",
"top15_desc": "Los 15 mejores escuadrones con estadísticas, enviado 35 minutos después del cierre de la franja horaria.\nEste se envió <t:{timestamp}:R>.",
"top30_desc": "Escuadrones del 16 al 30 con estadísticas.",
"not_logged_title": "Clasificación No Registrada",
"not_logged_desc": "Usa `/unlock` para suscribirte al nivel **Standard** (o superior) y recibir actualizaciones automáticas de clasificación.",
"server_not_upgraded_title": "⚠️ Servidor No Actualizado",
"server_not_upgraded_desc": "Este servidor no tiene una suscripción Premium activa.\n\n**Las actualizaciones automáticas dejarán de enviarse a servidores no actualizados después del <t:{deadline}:D>.**\n\nUsa `/unlock` para suscribirte y seguir recibiendo actualizaciones automáticas."
},
"stacks": {
"stack_title": "Stack de {leader}",
"stack_named_title": "{name}",
"no_members": "Sin miembros aún.",
"members_field": "Miembros ({count}/{max})",
"queue_field": "Cola ({count}/{max})",
"manage_title": "Gestionar Stack",
"no_pending_requests": "Sin solicitudes pendientes.",
"disbanded_title": "Stack [Disuelto]",
"disbanded_desc": "Este stack fue disuelto por el líder.",
"expired_title": "Stack [Expirado]",
"expired_desc": "Este stack ha expirado.",
"join_modal_title": "Solicitar unirse al stack",
"join_vehicle_label": "¿Con qué jugarás?",
"join_vehicle_placeholder": "ej. F-16C, WZ305...",
"ping_modal_title": "Mensaje de ping",
"ping_message_label": "Mensaje personalizado (opcional)",
"ping_message_placeholder": "ej. ¡Vengan ahora! ¡El stack empieza!",
"rename_modal_title": "Renombrar stack",
"rename_label": "Nombre del stack",
"rename_placeholder": "ej. Búhos Nocturnos, Escuadrón Alfa...",
"select_new_leader": "Seleccionar nuevo líder…",
"select_applicants": "Seleccionar solicitantes…",
"no_pending_applications": "Sin solicitudes pendientes.",
"select_to_remove": "Seleccionar personas a eliminar…",
"no_members_or_applicants": "Sin miembros ni solicitantes.",
"select_to_ping": "Seleccionar personas para notificar individualmente…",
"stack_not_found": "❌ Stack no encontrado.",
"no_longer_exists": "❌ Este stack ya no existe.",
"member_not_exists": "❌ Ese miembro ya no existe.",
"already_has_stack": "❌ Ese jugador ya tiene un stack activo.",
"already_member": "❌ Ya eres miembro de este stack.",
"already_applied": "❌ Ya tienes una solicitud pendiente para este stack.",
"queue_full": "❌ La cola está llena ({max}/{max}). Inténtalo más tarde.",
"application_sent": "✅ ¡Solicitud enviada! El líder del stack la revisará.",
"stack_disbanded": "✅ Stack disuelto.",
"cancelled": "Cancelado.",
"select_member_transfer": "❌ Selecciona un miembro para transferir el liderazgo.",
"ownership_transferred": "✅ Liderazgo transferido a {nick}. Has salido del stack.",
"select_applicant_first": "❌ Selecciona al menos un solicitante primero.",
"stack_full": "❌ El stack está lleno ({max}/{max} miembros).",
"select_person_first": "❌ Selecciona al menos una persona primero.",
"no_one_to_ping": "❌ No hay nadie a quien notificar.",
"ping_footer": "Notificado por {leader} para {stack}.",
"pinged": "✅ ¡Notificado!",
"select_from_dropdown": "❌ Selecciona al menos una persona del menú desplegable primero.",
"stack_renamed": "✅ Stack renombrado a **{name}**.",
"only_member_use_disband": "❌ Eres el único miembro. Usa **Disolver stack** para terminarlo.",
"select_transfer_prompt": "Selecciona un miembro para transferir el liderazgo antes de salir:",
"left_stack": "✅ Has salido del stack.",
"application_withdrawn": "✅ Tu solicitud ha sido retirada.",
"not_member_or_applicant": "❌ No eres miembro ni solicitante de este stack.",
"leader_only_manage": "❌ Solo el líder del stack puede gestionarlo.",
"leader_only_disband": "❌ Solo el líder del stack puede disolverlo.",
"confirm_disband": "¿Estás seguro de que quieres disolver este stack? Esta acción no se puede deshacer.",
"already_active_stack": "⚠️ Ya tienes un stack activo. Si el mensaje original desapareció (ej. tras reinicio del bot), puedes forzar la disolución y empezar de nuevo.",
"force_created": "✅ Stack anterior disuelto. Nuevo stack creado.",
"no_active_stack": "❌ No tienes un stack activo. Usa `/stack-create` para crear uno.",
"could_not_parse_channel": "⚠️ No se pudo procesar el ID del canal almacenado."
},
"commands": {
"common": {
"season": "La temporada para generar la tarjeta",
"theme": "Tema de color de la tarjeta",
"squadron_short": "Nombre corto del escuadrón",
"player_username": "Nombre del jugador",
"choice_dark": "Oscuro",
"choice_light": "Claro"
},
"comp": {
"description": "Buscar las últimas compos conocidas de un equipo",
"squadron_short": "Nombre corto del equipo enemigo"
},
"quick_log": {
"description": "Configurar una alarma para este escuadrón en este canal",
"squadron_name": "Nombre CORTO del escuadrón a vigilar",
"type": "Elige Logs, Puntos, Clasificación, BR Semanal o Ambos",
"choice_logs": "Logs",
"choice_points": "Puntos",
"choice_leaderboard": "Clasificación",
"choice_both": "Ambos (Logs + Puntos)",
"choice_weekly_br": "BR Semanal"
},
"sq_info": {
"description": "Obtener información de un escuadrón"
},
"sq_info_graph": {
"description": "Mostrar un gráfico de la composición de la plantilla por actividad y tasa de victoria (temporada actual)"
},
"sq_card": {
"description": "Generar una tarjeta de temporada para un escuadrón",
"squadron": "Nombre corto del escuadrón"
},
"sq_stats": {
"description": "Mostrar los puntos de un escuadrón en el tiempo"
},
"loss_calculator": {
"description": "Calcular pérdida de puntos si jugadores dejan un escuadrón",
"player1": "Jugador que se va",
"player_optional": "Jugador que se va (opcional)"
},
"website": {
"description": "Obtener un enlace al sitio de SRE Bot"
},
"card": {
"description": "Generar una tarjeta de temporada para un jugador"
},
"player_stats": {
"description": "Ver estadísticas detalladas de vehículos de un jugador",
"username": "Usuario WT para pedir stats",
"uid": "UID WT para pedir stats"
},
"view_player_games": {
"description": "Ver las últimas 20 partidas de un jugador"
},
"view_match": {
"description": "Ver marcador de partida por ID o jugador",
"match_id": "ID hexadecimal de sesión de la partida",
"player_name": "Jugador para ver sus partidas recientes"
},
"compare": {
"description": "Comparar estadísticas SQB acumuladas de jugadores",
"player1": "Primer jugador",
"player2": "Segundo jugador",
"player_optional": "Jugador adicional (opcional)"
},
"leaderboard": {
"description": "Obtener la clasificación global de SRE Bot"
},
"set_squadron": {
"description": "Definir el tag de escuadrón de este servidor",
"abbreviated_name": "Nombre corto del escuadrón a definir"
},
"setup": {
"description": "Configurar el bot para este servidor"
},
"meta_management": {
"description": "Gestionar acceso a datos meta de este servidor"
},
"meta": {
"description": "Buscar roster meta por nombre de vehículo",
"vehicle": "Nombre del vehículo a buscar"
},
"top": {
"description": "Ver los 20 mejores escuadrones con estadísticas"
},
"language": {
"description": "Cambiar el idioma del bot."
},
"translate_message": {
"name": "Traducir mensaje"
},
"sq_track": {
"description": "Seguir un escuadrón y comparar desde la última revisión",
"squadron_short_name": "Nombre corto del escuadrón a seguir"
},
"analytics": {
"description": "Ver análisis SQB avanzados de un escuadrón",
"view": "Qué vista de análisis mostrar",
"choice_maps": "Victorias por mapa",
"choice_comps": "Composiciones de equipo",
"choice_consistency": "Consistencia de jugadores",
"choice_time": "Hora del día",
"choice_matchups": "Historial de enfrentamientos"
},
"recent": {
"description": "Mostrar batallas recientes de un escuadrón",
"length": "Número de partidas a mostrar"
},
"vs": {
"description": "Historial cara a cara entre dos escuadrones",
"squadron_a": "Primer escuadrón",
"squadron_b": "Segundo escuadrón"
},
"autolog_management": {
"description": "Gestionar notificaciones autolog y diagnosticar permisos"
},
"diagnose_perms": {
"description": "Diagnosticar permisos autolog de este canal"
},
"unlock": {
"description": "Desbloquear Premium para este servidor"
},
"credits": {
"description": "Ver el equipo acreditado por este proyecto"
},
"schedule": {
"description": "Ver el calendario BR de la temporada actual"
},
"news": {
"description": "Ver últimas noticias y anuncios de SRE Bot"
},
"help": {
"description": "Ver guía, ToS y enlaces de soporte"
},
"donate": {
"description": "Apoyar el desarrollo de SRE Bot"
},
"stack_create": {
"description": "Crear un stack de jugadores",
"vehicle": "¿Con qué vehículo empezarás?"
},
"stack_manage": {
"description": "Volver a publicar tu stack activo en este canal"
},
"bot_status": {
"description": "Ver estado del bot: última partida recibida y TTL promedio"
}
},
"permission": {
"blacklisted_title": "❌ Bloqueado",
"blacklisted_desc": "No puedes usar este comando porque estás bloqueado.",
"reason_line": "**Motivo:** {reason}",
"access_denied_title": "⛔ Acceso denegado",
"no_permission_desc": "No tienes permiso para usar este comando.",
"unexpected_error_title": "❗ Error, repórtalo...."
},
"weekly_br": {
"title_wildcard": "Informe BR Semanal — {br} BR",
"title_squadron": "Informe BR Semanal — [{tag}] {long} • {br} BR",
"window_label": "Periodo: {start} → {end}",
"wildcard_desc_first": "Top {count} escuadrones por ELO • Puestos {low}{high}",
"wildcard_desc_second": "Top {count} escuadrones por ELO • Puestos {low}{high}",
"squadron_stats_line": "- {games} partidas • K/D {kdr} • Victorias {wr}%",
"top_players_inline_header": "🥇 Mejores jugadores:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}p)",
"top_players_header": "**Top {count} jugadores por ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} partidas • K/D {kdr}",
"squadron_header_line": "ELO de escuadrón: {score} • {games} partidas • Victorias {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO de escuadrón: poca actividad de equipo esta semana.",
"no_data": "No hay partidas registradas de [{tag}] en esta rotación de BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Erreur",
"no_data_title": "Aucune donnée",
"access_denied_title": "Accès refusé",
"access_denied_desc": "Ce serveur a été mis sur liste noire.",
"no_players_selected": "Aucun joueur sélectionné. Veuillez sélectionner au moins un joueur.",
"must_use_in_server": "Cette commande doit être utilisée dans un serveur.",
"could_not_resolve_channel": "Impossible de résoudre le salon sélectionné.",
"failed_update_setting": "❌ Échec de la mise à jour du paramètre.",
"configuration_not_found": "Configuration introuvable.",
"no_channel_selected": "Aucun salon sélectionné.",
"no_selection_received": "Aucune sélection reçue.",
"database_error": "❌ Erreur de base de données : {error}",
"enabled": "Activé",
"disabled": "Désactivé",
"not_configured": "Non configuré",
"unknown": "Inconnu",
"rating_field": "Classement",
"battles_field": "Batailles",
"wins_field": "Victoires",
"losses_field": "Défaites",
"win_rate_field": "Taux de victoire",
"kills_field": "Éliminations",
"deaths_field": "Morts",
"kd_field": "K/D",
"members_field": "Membres",
"placement_field": "Position",
"points_field": "Points",
"ground_kills_field": "Éliminations terrestres",
"air_kills_field": "Éliminations aériennes",
"total_kills_field": "Éliminations totales",
"assists_field": "Assistances",
"captures_field": "Captures",
"none_option": "Aucun"
},
"buttons": {
"skip": "Passer",
"previous": "Précédent",
"next": "Suivant",
"prev": "Préc.",
"prev_arrow": "◀ Précédent",
"next_arrow": "Suivant ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Générer le graphique",
"show_graph": "Afficher le graphique",
"view_player_stats": "📊 Voir les stats des joueurs",
"compare_nearby": "📈 Comparer les escadrons proches",
"confirm_swap": "Oui, remplacer",
"cancel_swap": "Non, garder l'ancien",
"set_squadron": "Définir l'escadron",
"same_as_logs": "Même que Logs",
"require_password": "🔒 Exiger un mot de passe",
"password_required": "🔒 Mot de passe requis",
"lock_data": "🔐 Lier les données de l'escadron",
"data_locked": "🔐 Données liées à ce serveur",
"allow_public": "👥 Autoriser le meta public",
"public_enabled": "👥 Meta public activé",
"update_accounts": "📋 Mettre à jour les comptes meta",
"change_password": "🔑 Changer le mot de passe",
"help": "❓ Aide",
"add_player": " Ajouter un Joueur",
"update_all": "🔄 Mettre à Jour Tous les Membres",
"back_to_settings": "⬅ Retour aux Paramètres",
"manage_notifications": "Gérer les notifications",
"diagnose_permissions": "Diagnostiquer les permissions",
"enable": "Activer",
"disable": "Désactiver",
"change_channel": "Changer de salon",
"view_replay": "Voir le Replay",
"view_website": "Voir sur le Site",
"view_video": "Voir la Vidéo",
"view_log": "Voir le Log",
"view_chat": "Voir le Chat",
"subscribe_website": "S'abonner via le Site",
"yes_disband": "Oui, dissoudre",
"cancel": "Annuler",
"transfer_leave": "Transférer et quitter",
"accept_selected": "Accepter la sélection",
"accept_all": "Tout accepter",
"decline_selected": "Refuser la sélection",
"back": "Retour",
"remove_all": "Tout retirer",
"remove_active": "Retirer les actifs",
"remove_queued": "Retirer en attente",
"remove_selected": "Retirer la sélection",
"ping_all": "Notifier tout le monde",
"ping_active": "Notifier les actifs",
"ping_queued": "Notifier en attente",
"ping_selected": "Notifier la sélection",
"accept_members": "Accepter des membres",
"remove_members": "Retirer des membres",
"ping_members": "Notifier les membres",
"rename_stack": "Renommer le stack",
"request_to_join": "Demander à rejoindre",
"leave_withdraw": "Quitter / Se retirer",
"manage_stack": "Gérer le stack ⚙️",
"disband_stack": "Dissoudre le stack",
"force_disband_create": "Forcer la dissolution et créer un nouveau"
},
"events": {
"guild_join_title": "Merci de m'avoir ajouté !",
"guild_join_desc": "Lancez `/setup` pour configurer le bot sur ce serveur."
},
"comp": {
"not_found_title": "Comps introuvables",
"not_found_desc": "Aucune donnée pour **{squadron}**, réessayez plus tard.",
"error_loading_title": "Erreur de chargement des comps",
"error_loading_desc": "Impossible de charger les données de comp : {error}",
"title": "Comps pour {squadron}",
"desc": "Comps vues dans les {minutes} dernières minutes",
"no_recent_title": "Aucune comp récente",
"no_recent_desc": "Aucune comp dans les {minutes} dernières minutes.",
"comp_title": "COMP {index}",
"last_seen_label": "**Vu pour la dernière fois** : {timestamp}{warning}",
"comp_label": "**Comp** : {notation}",
"no_players_recorded": "Aucun joueur enregistré.",
"limit_reached_title": "Limite de comps atteinte",
"limit_reached_desc": "Ce serveur a utilisé les {limit} recherches de comps pour ce créneau. Abonnez-vous (avec /unlock) pour un accès illimité ou attendez le prochain créneau.",
"remaining_footer": "{remaining}/{limit} recherches de comps restantes pour ce créneau"
},
"quick_log": {
"invalid_type": "Le type ne peut être défini que sur Logs, Points, Classement, BR Hebdomadaire ou Les deux.",
"squadron_required": "Vous devez fournir un nom d'escadron pour les alarmes Logs, Points ou les deux.",
"wildcard_logs_only": "Seuls les Logs peuvent être définis sur un escadron générique.",
"squadron_not_resolved": "L'escadron `{squadron}` n'a pas pu être résolu.",
"save_failed": "Échec de la sauvegarde des préférences. Veuillez réessayer plus tard.",
"premium_warning": "\n\n> ⚠️ **Les logs de partie nécessitent Premium.** Lancez `/unlock` pour vous abonner (2,99 $/mois) — les logs ne seront pas publiés avant cela.",
"leaderboard_set": "L'alarme du Classement Global est définie sur ce salon.",
"both_set": "Les alarmes Logs et Points pour {squadron} sont définies sur ce salon.{premium_note}",
"alarm_set": "L'alarme {alarm_type} pour {squadron} est définie sur ce salon.{premium_note}",
"weekly_br_wildcard_set": "Rapport BR hebdomadaire (top 20 escadrons) configuré pour ce salon. Envoyé à la fin de chaque rotation BR.",
"weekly_br_squadron_set": "Rapport BR hebdomadaire pour {squadron} (top 15 joueurs) configuré pour ce salon. Envoyé à la fin de chaque rotation BR."
},
"diagnostics": {
"title": "Diagnostics autolog",
"channel_permissions_header": "**Permissions du salon** (<#{channel_id}>)",
"perms_needed": " ^ L'autologging a besoin de tout ce qui précède pour envoyer des tableaux de scores.",
"server_squadron_header": "**Escadron du serveur** (`/set-squadron`)",
"server_squadron_short": " Court : `{short}`",
"server_squadron_long": " Long : `{long}`",
"server_squadron_not_set": " Non défini (la couleur de la barre du tableau de scores affichera 'not_set')",
"autolog_prefs_header": "**Préférences Autolog** (`/quick-log`)",
"autolog_none_configured": " ❌ AUCUN configuré - l'autologging n'enverra RIEN à ce serveur.",
"autolog_setup_hint": " Utilisez `/quick-log <squadron_short> Logs` dans le salon cible pour configurer.",
"autolog_no_logs_channels": " ❌ Aucun salon Logs configuré. Seulement Points/Classement trouvés.",
"autolog_enable_hint": " Utilisez `/quick-log <squadron_short> Logs` pour activer l'autologging.",
"selected_channel_tag": " **(salon sélectionné)**",
"missing_send_attach": " (envoi/pièce jointe manquant)",
"channel_not_found": " (salon introuvable)",
"invalid_channel_id": " (ID de salon invalide)",
"premium_status_header": "**Statut Premium** (`/unlock`)",
"premium_active": " ✅ Ce serveur dispose d'un abonnement Premium actif.",
"premium_not_subscribed": " ❌ Ce serveur n'a **pas** d'abonnement Premium.",
"premium_autolog_required": " L'autologging nécessite Premium. Utilisez `/unlock` pour vous abonner.",
"premium_not_subscribed_free": " ⚪ Non abonné — utilisez `/unlock` pour vous abonner (2,99 $/mois).",
"premium_free_note": " *(Les autologs sont gratuits pour tous les serveurs pour le moment.)*"
},
"sq_info": {
"title": "Info escadron : {squadron}",
"placement_field": "Position",
"total_points_field": "Points totaux",
"total_members_field": "Membres totaux",
"members_field": "Membres",
"fetch_failed": "Échec de la récupération des informations de l'escadron."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Saison {season})",
"embed_title": "{squadron} — Composition de l'effectif",
"embed_desc": "Saison **{season}** · Médiane de parties : **{median}** · Noyau : **{core}** · Actifs : **{active}** · Faibles : **{weak}**\nBarres triées par parties décr. ; hauteur = taux de victoire. Noyau = top 30 % de TV et parties ≥ médiane. Actifs = top 3045 % de TV et parties ≈ médiane. Faibles = tous les autres.",
"core_threshold_line": "NOYAU ≥ {wr} %",
"weak_threshold_line": "FAIBLES < {wr} %",
"y_label": "Taux de victoire",
"core_header": "NOYAU — {count} · TV {avg}%",
"active_header": "ACTIFS — {count} · TV {avg}%",
"weak_header": "FAIBLES — {count} · TV {avg}%",
"no_active_season": "Aucune saison active trouvée. Réessayez au début de la prochaine.",
"no_members": "Aucun membre actuel trouvé pour {squadron}."
},
"recap_card": {
"unknown_season": "Saison inconnue : `{season}`.",
"no_clan_id": "Impossible de résoudre l'identifiant de l'escadron `{squadron}`.",
"render_failed": "Échec de la génération de la carte récapitulative de saison. Réessayez plus tard."
},
"sq_stats": {
"no_data_title": "Aucune donnée",
"no_data_desc": "Aucune donnée historique trouvée pour l'escadron : {squadron}",
"title": "{squadron} // ESCADRON",
"desc": "Tendance du score total (Derniers {count} points de données)",
"previous_score_field": "Score précédent",
"current_score_field": "Score actuel",
"change_field": "Variation",
"player_title": "{squadron} // JOUEURS",
"player_desc": "Tendances des points par joueur",
"comparison_title": "{squadron} // COMPARAISON AU CLASSEMENT",
"comparison_desc": "Comparaison avec les escadrons classés {range}",
"current_position_field": "Position actuelle",
"squadrons_shown_field": "Escadrons affichés",
"squadron_not_found_error": "Escadron introuvable dans le classement",
"no_nearby_error": "Aucun escadron proche trouvé",
"no_historical_error": "Aucune donnée historique trouvée pour les escadrons proches",
"comparison_chart_failed": "Échec de la génération du graphique de comparaison",
"select_players_placeholder": "Sélectionner des joueurs (Page {page})"
},
"loss_calc": {
"title": "Perte de points — {squadron}",
"players_leaving_field": "Joueurs partant",
"share_of_total_field": "% part du total",
"points_lost_real_field": "Points perdus (réel)",
"points_lost_raw_field": "Points perdus (brut)",
"squadron_rating_field": "Classement de l'escadron",
"squadron_position_field": "Position de l'escadron",
"positions_lost_field": "Positions perdues",
"not_found_footer": "Introuvable dans l'escadron : {players}",
"fetch_failed": "Échec de la récupération des données de l'escadron : {error}",
"no_point_data": "Aucune donnée de points disponible pour cet escadron.",
"no_matching_players": "Aucun joueur correspondant trouvé dans **{squadron}**."
},
"player": {
"select_player_placeholder": "Sélectionner un joueur",
"no_stats_found": "❌ Aucune stat trouvée pour l'UID : {uid}",
"no_vehicle_stats": "❌ Aucune stat de véhicule trouvée pour ce joueur.",
"vehicles_found": "**{count}** véhicules trouvés pour **{nick}**\nSélectionnez un véhicule pour voir les stats détaillées :",
"vehicle_select_placeholder": "Sélectionner un véhicule (Page {page}/{total})",
"combat_stats_header": "**__STATS DE COMBAT__**",
"ground_kills_label": "**Éliminations terrestres :** {value}",
"air_kills_label": "**Éliminations aériennes :** {value}",
"total_kills_label": "**Éliminations totales :** {value}",
"assists_label": "**Assistances :** {value}",
"deaths_label": "**Morts :** {value}",
"kd_label": "**K/D :** {value}",
"captures_label": "**Captures :** {value}",
"battle_record_header": "**__BILAN DE BATAILLES__**",
"total_battles_label": "**Batailles totales :** {value}",
"wins_label": "**Victoires :** {value}",
"losses_label": "**Défaites :** {value}",
"win_rate_label": "**Taux de victoire :** {value}%",
"stats_desc": "Stats pour **{nick}** (**{squadron}**)\nUID : `{uid}`",
"not_found_title": "Joueur introuvable",
"not_found_desc": "Aucun historique de partie trouvé pour `{player}`.",
"no_players_found": "Aucun joueur trouvé correspondant à **{username}**\nEssayez d'utiliser `/website` pour rechercher sur le site.",
"multiple_matches": "Plusieurs correspondances trouvées, choisissez la bonne ci-dessous :",
"must_provide_input": "Vous devez fournir au moins un UID ou un nom d'utilisateur."
},
"player_games": {
"no_recent_title": "Aucune partie récente",
"no_recent_desc": "Aucune partie trouvée pour **{player}** dans les 8 dernières heures.",
"squadron_label": "**Escadron :** {squadron}",
"record_label": "**V :** {wins} **D :** {losses} **TV :** {wr}%",
"comps_played_header": "\n\n**Comps Jouées**"
},
"match": {
"missing_input_title": "Entrée manquante",
"missing_input_desc": "Fournissez soit un `match_id` soit un `player_name`.",
"not_found_title": "Match introuvable",
"not_found_desc": "Impossible de trouver un match avec l'ID `{match_id}`.",
"invalid_data_title": "Données de match invalides",
"invalid_data_desc": "Les données de replay n'ont pas pu être analysées.",
"scoreboard_error_title": "Erreur du tableau de scores",
"scoreboard_error_desc": "Échec de la génération de l'image du tableau de scores.",
"no_games_title": "Aucune partie trouvée",
"no_games_desc": "Aucun historique de partie trouvé pour **{player}**.",
"recent_matches_title": "Matchs récents pour {player}",
"recent_matches_desc": "Affichage de jusqu'à {count} parties récentes. Sélectionnez-en une pour voir le tableau de scores complet.",
"select_match_placeholder": "Sélectionner un match à voir..."
},
"compare": {
"no_players_found": "Aucun joueur trouvé correspondant à **{name}**.",
"multiple_matches": "Plusieurs correspondances pour **{name}** : {matches}\nVeuillez utiliser un nom plus précis (les suggestions de saisie automatique sont exactes).",
"could_not_resolve": "Impossible de résoudre les joueurs.",
"could_not_fetch": "❌ Impossible de récupérer les stats pour **{name}**.",
"no_graph_data": "Aucune donnée disponible pour les 90 derniers jours.",
"no_squadron_points_data": "Aucune donnée de points d'escadron pour {names} (joueur introuvable dans l'historique de l'escadron suivi).",
"graph_title": "Points des joueurs — 90 derniers jours",
"battles_label": "Batailles",
"wins_label": "Victoires",
"losses_label": "Défaites",
"win_rate_label": "Taux de victoire",
"ground_kills_label": "Éliminations terrestres",
"air_kills_label": "Éliminations aériennes",
"total_kills_label": "Éliminations totales",
"assists_label": "Assistances",
"deaths_label": "Morts",
"kd_label": "K/D",
"captures_label": "Captures"
},
"squadron": {
"not_found_desc": "Escadron `{squadron}` introuvable.",
"set_title": "✅ Escadron défini",
"set_desc": "L'escadron **{squadron}** a été défini pour ce serveur.",
"short_name_field": "Nom court",
"long_name_field": "Nom long",
"swap_title": "✅ Escadron remplacé",
"swap_desc": "Remplacé **{old}** par **{new}** pour ce serveur.",
"already_set_title": "⚠️ Escadron déjà défini",
"already_set_desc": "Ce serveur est actuellement défini sur **{old}**.\nLe remplacer par **{new}** ?",
"swap_cancelled": "❌ Changement d'escadron annulé."
},
"setup": {
"step1_title": "Configuration du serveur — Étape 1 sur 3",
"step1_desc": "Cet assistant vous guidera dans la configuration du bot pour votre serveur.\n\n**Étape 1** — Définir votre escadron\n**Étape 2** — Choisir un salon de logs\n**Étape 3** — Choisir un salon de points\n",
"step1_current_sq": "\nEscadron actuellement configuré : **[{short}] {long}**",
"step2_title": "Configuration du serveur — Étape 2 sur 3",
"step2_desc": "Escadron défini sur **[{short}] {long}**.\n\nOù les **logs de batailles** doivent-ils être publiés ?\nSélectionnez un salon textuel ci-dessous, ou passez cette étape.",
"step3_title": "Configuration du serveur — Étape 3 sur 3",
"step3_desc": "Où les **notifications de points** doivent-elles être publiées ?\nSélectionnez un salon textuel ci-dessous, ou passez cette étape.",
"step3_same_as_logs": "\n\nVous pouvez aussi cliquer sur \"Même que Logs\" pour réutiliser le salon des logs.",
"summary_title": "Configuration terminée",
"summary_desc": "Vous pouvez utiliser `/autolog-management` pour modifier ces paramètres plus tard.",
"squadron_field": "Escadron",
"logs_channel_field": "Salon des logs",
"points_channel_field": "Salon des points",
"premium_required_field": "⚠️ Les logs de partie nécessitent Premium",
"premium_required_value": "Les tableaux de scores automatiques ne seront pas publiés tant que ce serveur n'aura pas un abonnement actif. Lancez `/unlock` pour vous abonner (2,99 $/mois).",
"modal_title": "Définir l'escadron",
"modal_label": "Nom court de l'escadron",
"modal_placeholder": "ex. AXYS",
"squadron_not_found": "Escadron `{squadron}` introuvable. Veuillez réessayer.",
"logs_channel_placeholder": "Sélectionner un salon de logs...",
"points_channel_placeholder": "Sélectionner un salon de points..."
},
"meta_management": {
"squadron_not_found_title": "❌ Escadron introuvable",
"squadron_not_found_desc": "Impossible de trouver l'ID de clan pour l'escadron : **{squadron}**",
"access_denied_title": "❌ Accès Refusé",
"access_denied_desc": "Mot de passe incorrect. Les données meta de cet escadron sont protégées.",
"data_locked_title": "🔐 Données de l'escadron liées",
"data_locked_desc": "**{squadron}** a la liaison des données activée et ne peut pas être transféré vers un autre serveur.\n\nLe propriétaire de l'escadron doit désactiver **Lier les données de l'escadron** avant de pouvoir le déplacer.",
"error_retrieving_settings": "❌ Erreur lors de la récupération des paramètres du serveur après le transfert. Veuillez réessayer.",
"error_retrieving_settings_retry": "❌ Erreur lors de la récupération des paramètres du serveur. Veuillez relancer la commande.",
"authenticated_title": "✅ Authentifié",
"authenticated_desc": "Mot de passe vérifié. Gestion des paramètres pour **{squadron}**.",
"claimed_title": "✅ Escadron revendiqué",
"claimed_desc": "**{squadron}** a été revendiqué avec succès pour ce serveur !",
"password_requirement_field": "🔒 Exigence de mot de passe",
"data_lock_field": "🔐 Liaison des données de l'escadron",
"public_meta_field": "👥 Accès meta public",
"access_password_field": "🔑 Mot de passe d'accès",
"enabled_value": "✅ Activé",
"disabled_value": "❌ Désactivé",
"settings_title": "🔐 Paramètres de gestion meta",
"settings_desc": "**Escadron :** {squadron}\n**ID de Clan :** {clan_id}",
"first_time_title": "🔐 Gestion meta — Première configuration",
"first_time_owner_desc": "**Escadron :** {squadron}\n**ID de Clan :** {clan_id}\n\n🔑 Votre mot de passe d'accès a été généré. **Sauvegardez ce mot de passe** — vous en aurez besoin pour authentifier l'accès aux données meta à l'avenir.\n\n**Mot de Passe :** `{password}`",
"first_time_non_owner_desc": "**Escadron :** {squadron}\n**ID de Clan :** {clan_id}\n\nL'escadron a été configuré. Demandez le mot de passe d'accès au propriétaire du serveur.",
"settings_field": "Paramètres",
"settings_hint": "Utilisez les boutons ci-dessous pour configurer les paramètres d'accès.",
"password_toggled": "✅ Exigence de mot de passe : **{state}**",
"lock_toggled": "✅ Liaison des données de l'escadron : **{state}**",
"public_meta_toggled": "✅ Accès meta public : **{state}**\n{detail}",
"public_meta_enabled_detail": "Les non-administrateurs peuvent maintenant utiliser la commande `/meta`.",
"public_meta_disabled_detail": "Seuls les administrateurs peuvent utiliser la commande `/meta`.",
"owner_only_password": "❌ Seul le propriétaire du serveur peut changer le mot de passe de l'escadron.",
"help_title": "📖 Aide de gestion meta",
"help_desc": "Explication de chaque paramètre et fonctionnalité :",
"help_password_field": "🔑 Mot de passe d'accès",
"help_password_value": "Le mot de passe d'accès de votre escadron. Seul le **propriétaire du serveur** peut voir le mot de passe dans le panneau de paramètres. Toute personne ayant le mot de passe peut revendiquer les données meta de votre escadron sur son serveur, alors gardez-le sécurisé.",
"help_require_field": "🔒 Exiger un mot de passe",
"help_require_value": "Quand activé, même les administrateurs de ce serveur doivent saisir le mot de passe de l'escadron pour accéder à `/meta-management`. Ajoute une couche de sécurité supplémentaire pour prévenir les modifications accidentelles.",
"help_lock_field": "🔐 Lier les données de l'escadron",
"help_lock_value": "Quand activé, empêche le transfert de l'escadron vers d'autres serveurs, même avec le bon mot de passe. Doit être désactivé avant de pouvoir transférer l'escadron.",
"help_public_field": "👥 Autoriser le meta public",
"help_public_value": "Quand activé, permet aux membres non-administrateurs d'utiliser la commande `/meta` pour rechercher des véhicules d'escadron. Quand désactivé, seuls les administrateurs du serveur peuvent utiliser `/meta`.",
"help_accounts_field": "📋 Mettre à jour les comptes meta",
"help_accounts_value": "Ouvre le gestionnaire de liste de joueurs où vous pouvez ajouter ou supprimer des joueurs de la liste meta de votre escadron. Utilisez **Mettre à jour tous les membres** pour synchroniser tout votre escadron en une fois.",
"help_change_pw_field": "🔑 Changer le mot de passe",
"help_change_pw_value": "**Propriétaire du serveur uniquement.** Modifiez le mot de passe d'accès de l'escadron et définissez un indice optionnel. L'indice s'affiche dans l'invite de mot de passe pour aider à s'en souvenir.",
"password_modal_title": "Mot de passe d'accès de l'escadron",
"password_modal_label": "Entrez le mot de passe de l'escadron",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Changer le mot de passe de l'escadron",
"current_password_label": "Mot de passe actuel",
"current_password_placeholder": "Entrez votre mot de passe actuel",
"new_password_label": "Nouveau mot de passe",
"new_password_placeholder": "Entrez votre nouveau mot de passe",
"confirm_password_label": "Confirmer le nouveau mot de passe",
"confirm_password_placeholder": "Ressaisissez votre nouveau mot de passe",
"hint_label": "Indice de mot de passe (optionnel)",
"hint_placeholder": "Un indice pour se souvenir du mot de passe",
"pw_incorrect": "❌ Le mot de passe actuel est incorrect.",
"pw_mismatch": "❌ Les nouveaux mots de passe ne correspondent pas. Veuillez réessayer.",
"pw_empty": "❌ Le nouveau mot de passe ne peut pas être vide.",
"pw_changed": "✅ Mot de passe mis à jour avec succès pour **{squadron}**.\n**Nouveau mot de passe :** `{password}`",
"pw_changed_hint": "\n**Indice :** {hint}",
"player_add_modal_title": "Ajouter un Joueur à la Liste Meta",
"player_add_label": "UID ou Pseudo du Joueur",
"player_add_placeholder": "Entrez l'UID du joueur (ex. 12345678) ou son pseudo",
"player_not_found": "❌ Joueur `{player}` introuvable dans la base de données Players_Global.\n",
"roster_title": "📋 Gestion de la liste meta - {squadron}",
"roster_desc": "**ID de Clan de l'Escadron :** {clan_id}\n**Joueurs Totaux :** {count}",
"roster_page_field": "Joueurs (Page {page}/{total})",
"no_players_field": "Aucun joueur",
"no_players_hint": "Aucun joueur ajouté à la liste meta pour l'instant. Cliquez sur **Ajouter un Joueur** pour commencer.",
"remove_player_placeholder": "Sélectionner le joueur à supprimer...",
"fetch_members_failed": "❌ Échec de la récupération des membres de l'escadron : {error}",
"no_members_found": "❌ Aucun membre trouvé dans l'escadron ou l'appel API a échoué.",
"roster_synced": "✅ Liste synchronisée avec l'escadron.",
"roster_added": "**+{count}** ajouté(s)",
"roster_removed": "**-{count}** supprimé(s) (ont quitté l'escadron)",
"roster_up_to_date": "**{count}** déjà à jour",
"refreshing_vehicles": "Actualisation des données de véhicules en arrière-plan..."
},
"meta": {
"not_configured": "❌ Données meta non configurées pour ce serveur. Lancez d'abord `/meta-management`.",
"no_permission": "❌ Vous devez avoir les permissions d'administrateur pour utiliser cette commande.\nLes administrateurs peuvent activer l'accès public via `/meta-management`.",
"no_results": "❌ Aucun joueur de votre liste d'escadron ne possède **{vehicle}**.",
"no_results_admin_hint": "\n*Vous vous attendiez à ce que quelqu'un l'ait ? Cliquez sur le bouton de mise à jour des membres dans `/meta-management` et vérifiez.*",
"search_title": "🔍 Résultats de Recherche - {vehicle}",
"matches_found": "**Correspondances Trouvées :** {count} joueur(s)",
"spawns_label": "Apparitions",
"deaths_label": "Morts",
"gk_label": "EL",
"ak_label": "EA",
"points_label": "Points",
"kdr_label": "KDR",
"games_label": "Parties",
"no_points": "—"
},
"top": {
"title": "**Top 20 escadrons**",
"rating_label": "**Classement :** {value}",
"air_kills_label": "**Éliminations aériennes :** {value}",
"ground_kills_label": "**Éliminations terrestres :** {value}",
"deaths_label": "**Morts :** {value}",
"kd_label": "**K/D :** {value}",
"win_rate_label": "**Taux de victoire :** {value}",
"playtime_label": "**Temps de jeu :** {value}",
"fetch_failed": "Échec de la récupération des données de l'escadron."
},
"analytics": {
"no_data_title": "Aucune donnée",
"no_matches_desc": "Aucun match trouvé.",
"no_comp_desc": "Aucune donnée de composition trouvée.",
"no_consistency_desc": "Pas assez de données de joueurs (minimum 50 matchs).",
"no_time_desc": "Aucune donnée temporelle trouvée.",
"unknown_view": "Vue inconnue.",
"map_title": "Taux de victoire par carte : {squadron}",
"comp_title": "Compositions d'équipe : {squadron}",
"consistency_title": "Constance des joueurs : {squadron}",
"consistency_desc": "Trié par ratio K/D",
"time_title": "Performance par heure de la journée : {squadron}",
"eu_timeslot": "\n**Plage Horaire EU**",
"na_timeslot": "\n**Plage Horaire NA**",
"off_peak": "\n**Hors Pic**",
"matchups_title": "📜 {squadron} — Historique des Affrontements",
"matchups_won_field": "🏆 Plus de Victoires Contre",
"matchups_lost_field": "💀 Plus de Défaites Contre",
"no_matchups_desc": "Aucun match enregistré contre d'autres escadrons."
},
"recent": {
"title": "Matchs récents : {squadron}",
"no_matches_desc": "Aucun match trouvé pour cet escadron."
},
"h2h": {
"two_required_title": "Deux escadrons requis",
"two_required_desc": "Fournissez au moins un escadron, ou utilisez `/set-squadron` et indiquez l'adversaire.",
"provide_a_desc": "Fournissez `squadron_a` ou utilisez d'abord `/set-squadron`.",
"provide_b_desc": "Fournissez `squadron_b` ou utilisez d'abord `/set-squadron`.",
"squadron_not_found_title": "Escadron introuvable",
"same_squadron_title": "Même escadron",
"same_squadron_desc": "Vous ne pouvez pas vérifier les face-à-face contre vous-même.",
"record_desc": "**Bilan :** {a_wins}V - {b_wins}D ({total} parties)",
"no_matches_desc": "Aucun match enregistré entre **{a}** et **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium :** Actif — l'autologging est activé pour ce serveur.",
"premium_not_subscribed_line": "❌ **Premium :** Non abonné — utilisez `/unlock` pour activer l'autologging.",
"premium_free_line": "⚪ **Premium :** Non abonné — utilisez `/unlock` pour vous abonner (2,99 $/mois). *(Les autologs sont gratuits pour tous les serveurs pour le moment.)*",
"what_to_do": "\n\nQue souhaitez-vous faire ?",
"select_notif_type": "Sélectionnez le type de notification à gérer :",
"select_notif_placeholder": "Sélectionner le type de notification",
"logs_option": "Logs",
"logs_option_desc": "Gérer les notifications de Logs",
"points_option": "Points",
"points_option_desc": "Gérer les notifications de Points",
"leaderboard_option": "Classement",
"leaderboard_option_desc": "Gérer les notifications de Classement",
"selected_type": "**{type}** sélectionné. Choisissez maintenant l'escadron à gérer :",
"select_squadron_placeholder": "Sélectionner un escadron",
"select_squadron_page_placeholder": "Sélectionner un escadron (Page {page})",
"no_squadrons_available": "Aucun escadron disponible pour ce type de notification.",
"managing_global": "Gestion de **{type}** (global) dans le salon **{channel}**.",
"managing_squadron": "Gestion de **{type}** pour l'escadron **{squadron}** dans le salon **{channel}**.",
"select_channel": "Sélectionnez un nouveau salon :",
"select_channel_placeholder": "Sélectionner un salon",
"select_channel_page_placeholder": "Sélectionner un salon (Page {page})",
"global_toggled": "{type} (global) est maintenant {state}.",
"squadron_toggled": "{type} pour **{squadron}** est maintenant {state}.",
"channel_updated_global": "{type} (global) mis à jour vers {channel}",
"channel_updated_squadron": "{type} pour **{squadron}** mis à jour vers {channel}",
"diagnose_channel_placeholder": "Sélectionner un salon à diagnostiquer...",
"select_channel_diagnose": "Sélectionnez le salon à diagnostiquer :",
"game_not_logged_title": "Partie non enregistrée",
"game_not_logged_desc": "Utilisez `/unlock` pour souscrire au tier **Standard** (ou supérieur) et recevoir les tableaux de scores automatiques.",
"server_not_upgraded_title": "⚠️ Serveur non mis à niveau",
"server_not_upgraded_autolog_desc": "Ce serveur ne dispose pas d'un abonnement Premium actif.\n\n**Les tableaux de scores automatiques cesseront d'être envoyés aux serveurs non mis à niveau après le <t:{deadline}:D>.**\n\nUtilisez `/unlock` pour vous abonner et continuer à recevoir les logs de parties automatiques.",
"replay_not_available": "Les données de replay ne sont pas encore disponibles — attendez un peu puis réessayez !",
"too_many_videos": "Trop de vidéos en cours de rendu — veuillez réessayer dans un moment.",
"video_gen_failed": "Erreur lors de la génération de la vidéo : `{error}`",
"video_missing": "Échec de la génération de la vidéo de replay - fichier de sortie manquant ou vide.",
"video_too_large": "Vidéo de replay trop grande pour être téléchargée ({file_mb:.1f} Mo). La limite du serveur est de {limit_mb:.0f} Mo.",
"video_web_fallback": "Vous pouvez aussi voir ce match sur {url}",
"video_upload_failed": "Vidéo trop grande pour être téléchargée — voyez-la sur le site :\n{url}",
"video_unexpected_error": "Erreur inattendue lors de la génération de la vidéo de replay : `{error}`",
"replay_not_found": "Données de replay introuvables pour la session `{session_id}` sur le disque.",
"chat_log_title": "**Log de Chat pour la Partie [{session_id}]({url})**",
"chat_log_part_title": "**Log de Chat pour la Partie [{session_id}]({url}) (Partie {part}/{total})**",
"chat_log_part_only": "**Log de Chat (Partie {part}/{total})**",
"no_chat_log": "Aucun log de chat trouvé pour la session `{session_id}`.",
"chat_log_error": "Erreur inattendue lors du chargement du log de chat : `{error}`",
"battle_log_title": "**Log de Bataille pour la Partie [{session_id}]({url})**",
"battle_log_part_title": "**Log de Bataille pour la Partie [{session_id}]({url}) (Partie {part}/{total})**",
"battle_log_part_only": "**Log de Bataille (Partie {part}/{total})**",
"no_battle_log": "Aucun événement de combat trouvé pour la session `{session_id}`.",
"battle_log_error": "Erreur inattendue lors du chargement du log de bataille : `{error}`",
"points_update_title": "**{squadron} {region} Mise à jour des points**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Changements des joueurs :**",
"points_table_header": "Nom Changement Maintenant\n",
"wl_line": "\n**{squadron}** a terminé **{wins}V-{losses}D** cette session",
"placement_rose": "\n**{squadron}** est monté au **{new_place}** depuis le **{old_place}**",
"placement_fell": "\n**{squadron}** est descendu au **{new_place}** depuis le **{old_place}**",
"points_not_logged_title": "Points non enregistrés",
"points_not_logged_desc": "Utilisez `/unlock` pour souscrire au tier **Standard** (ou supérieur) et recevoir les mises à jour automatiques des points.",
"server_not_upgraded_points_desc": "Ce serveur ne dispose pas d'un abonnement Premium actif.\n\n**Les mises à jour automatiques cesseront d'être envoyées aux serveurs non mis à niveau après le <t:{deadline}:D>.**\n\nUtilisez `/unlock` pour vous abonner et continuer à recevoir les mises à jour automatiques.",
"leave_title": "⚠️ Joueur ayant quitté {squadron}",
"leave_desc": "**{nick}** ({uid}) a quitté l'escadron.\n\nDerniers points enregistrés : **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Escadron au-dessus de la limite du tier",
"over_cap_desc": "Votre serveur est sur le tier **{tier}**, qui autorise **{cap} {notif}** escadrons. L'escadron **{squadron}** dépasse la limite et n'est pas enregistré. Passez à un tier supérieur pour le restaurer.",
"over_cap_footer": "Mise à niveau sur srebot-meow.ing/premium ou via /unlock",
"wildcard_blocked_title": "Le logging wildcard nécessite un tier supérieur",
"wildcard_blocked_desc": "Les entrées wildcard (*, all, everything) ne sont disponibles que sur Pro ou Max. Votre serveur est sur **{tier}** pour {notif}. Mise à niveau requise.",
"cap_header": "{used}/{cap} {notif} activés — tier {tier}"
},
"track": {
"squadron_not_found": "Escadron introuvable.",
"fetch_failed": "Échec de la récupération des informations de l'escadron."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Débloquez les fonctionnalités premium pour ce serveur.**\n\nPremium inclut :\n> • Publications automatiques de tableaux de scores\n> • Logs de chat et de bataille\n> • Recherches de replays\n> • Recherches /comp illimitées\n> • Support prioritaire\n\n**2,99 $ / mois · par serveur · résiliable à tout moment**\n\n⚠️ La facturation Discord n'est disponible que dans certains pays. Si le bouton ci-dessous affiche **«Produit Indisponible»**, cela peut être dû à un pays non pris en charge ou à l'utilisation d'un **appareil mobile**. Utilisez le bouton **S'abonner via le Site** à la place.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Ce serveur est déjà abonné !**",
"manage_discord_field": "Gérer l'Abonnement",
"manage_discord_value": "Votre abonnement est via **Discord**.\nPour annuler, allez dans **Paramètres Utilisateur → Abonnements** sur Discord.",
"manage_website_field": "Gérer l'Abonnement",
"manage_website_value": "Votre abonnement est via le **site web**.\nGérez-le sur [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Bientôt disponible",
"coming_soon_value": "Les abonnements Premium ne sont pas encore disponibles. Revenez bientôt !",
"current_tier": "Vous êtes sur le plan **{tier}**.",
"upgrade_to": "Passer à {tier}",
"upgrade_to_value": "Plus d'escadrons et de fonctionnalités en passant à **{tier}**."
},
"language": {
"prompt": "Veuillez sélectionner la langue de votre serveur :",
"select_placeholder": "Choisissez la langue de votre serveur",
"language_set": "Langue définie sur {language}.",
"translate_prompt": "Sélectionne une langue cible ci-dessous 👇",
"translate_placeholder": "Choisissez une langue cible…",
"translate_result": "**{author} → {language} :**\n{text}",
"translation_unavailable": "Traduction indisponible (DeepL non configuré)",
"translation_failed": "Traduction échouée"
},
"misc": {
"credits_title": "Crédits",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Développeur Principal, Gestionnaire du Bot, Gestionnaire de Communauté\n> **Z3R0** - Développeur, Développeur en Optimisation, Ingénieur Base de Données\n> **Clippii (Heidi)** - Développeur, Développeur Web, Gestionnaire de Communauté\n> **LivingTheDagor** - Développeur, Développeur de Parser, Consultant\n> **Lux_** - Ingénieur API, Développeur Spectra\n> **Konigallerwaffen** - Consultant Retours et Fonctionnalités\n> **Žralok Tonda** - Traducteur Tchèque\n> **Styevy**, **Lopais** - Traducteurs Allemands\n> **Susogus**, **playforfun698** - Traducteurs Polonais\n> **Bobr** - Traducteur Russe\n\n\n[Envie de nous rejoindre ?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "CALENDRIER DE SAISON",
"schedule_not_found_title": "Calendrier introuvable",
"schedule_not_found_desc": "Aucune donnée de calendrier n'est disponible pour l'instant.",
"news_no_news_title": "Aucune actualité",
"news_no_news_desc": "Il n'y a aucune annonce pour le moment. Revenez plus tard !",
"news_footer": "Merci pour votre soutien ! ᙙᙖᘢ",
"help_title": "Guide du Bot",
"donate_title": "Soutenir SRE Bot",
"donate_desc": "Si vous aimez utiliser SRE Bot et souhaitez soutenir son développement, pensez à m'offrir un café !\n\n**[Faire un don sur Ko-fi](https://ko-fi.com/notsotoothless)**\n\nChaque contribution aide à maintenir le bot en marche et soutient de nouvelles fonctionnalités. Merci !",
"status_title": "Statut du bot",
"status_last_received": "Dernière partie reçue",
"status_avg_ttl": "TTL moyen (30 dernières)",
"status_no_data": "Aucune donnée pour le moment",
"status_gaijin_slow": "⚠️ Serveurs Gaijin lents",
"help_commands_header": "**Aperçu des commandes**",
"help_links": "Pour les détails, lis la documentation [ici]({docs}) ou demande du support [ici]({support}).",
"help_terms": "[Conditions d'utilisation]({terms}) • [Politique de confidentialité]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Classement des Escadrons",
"top15_desc": "Top 15 escadrons avec statistiques, envoyé 35 minutes après la clôture du créneau.\nCelui-ci envoyé <t:{timestamp}:R>.",
"top30_desc": "Escadrons 16 à 30 avec statistiques.",
"not_logged_title": "Classement non enregistré",
"not_logged_desc": "Utilisez `/unlock` pour souscrire au tier **Standard** (ou supérieur) et recevoir les mises à jour automatiques du classement.",
"server_not_upgraded_title": "⚠️ Serveur non mis à niveau",
"server_not_upgraded_desc": "Ce serveur ne dispose pas d'un abonnement Premium actif.\n\n**Les mises à jour automatiques cesseront d'être envoyées aux serveurs non mis à niveau après le <t:{deadline}:D>.**\n\nUtilisez `/unlock` pour vous abonner et continuer à recevoir les mises à jour automatiques."
},
"stacks": {
"stack_title": "Stack de {leader}",
"stack_named_title": "{name}",
"no_members": "Aucun membre pour l'instant.",
"members_field": "Membres ({count}/{max})",
"queue_field": "File d'attente ({count}/{max})",
"manage_title": "Gérer le stack",
"no_pending_requests": "Aucune demande en attente.",
"disbanded_title": "Stack [Dissous]",
"disbanded_desc": "Ce stack a été dissous par le chef.",
"expired_title": "Stack [Expiré]",
"expired_desc": "Ce stack a expiré.",
"join_modal_title": "Demander à rejoindre le stack",
"join_vehicle_label": "Avec quoi allez-vous jouer ?",
"join_vehicle_placeholder": "ex. F-16C, WZ305...",
"ping_modal_title": "Message de notification",
"ping_message_label": "Message personnalisé (facultatif)",
"ping_message_placeholder": "ex. Venez maintenant ! Le stack commence !",
"rename_modal_title": "Renommer le stack",
"rename_label": "Nom du stack",
"rename_placeholder": "ex. Hiboux de nuit, Équipe Alpha...",
"select_new_leader": "Sélectionner un nouveau chef…",
"select_applicants": "Sélectionner des candidats…",
"no_pending_applications": "Aucune candidature en attente.",
"select_to_remove": "Sélectionner des personnes à retirer…",
"no_members_or_applicants": "Aucun membre ni candidat.",
"select_to_ping": "Sélectionner des personnes à notifier individuellement…",
"stack_not_found": "❌ Stack introuvable.",
"no_longer_exists": "❌ Ce stack n'existe plus.",
"member_not_exists": "❌ Ce membre n'existe plus.",
"already_has_stack": "❌ Ce joueur a déjà un stack actif.",
"already_member": "❌ Vous êtes déjà membre de ce stack.",
"already_applied": "❌ Vous avez déjà une candidature en attente pour ce stack.",
"queue_full": "❌ La file d'attente est pleine ({max}/{max}). Réessayez plus tard.",
"application_sent": "✅ Candidature envoyée ! Le chef du stack l'examinera.",
"stack_disbanded": "✅ Stack dissous.",
"cancelled": "Annulé.",
"select_member_transfer": "❌ Veuillez sélectionner un membre pour le transfert.",
"ownership_transferred": "✅ Direction transférée à {nick}. Vous avez quitté le stack.",
"select_applicant_first": "❌ Veuillez d'abord sélectionner au moins un candidat.",
"stack_full": "❌ Le stack est déjà plein ({max}/{max} membres).",
"select_person_first": "❌ Veuillez d'abord sélectionner au moins une personne.",
"no_one_to_ping": "❌ Personne à notifier.",
"ping_footer": "Notifié par {leader} pour {stack}.",
"pinged": "✅ Notifié !",
"select_from_dropdown": "❌ Veuillez d'abord sélectionner au moins une personne dans le menu déroulant.",
"stack_renamed": "✅ Stack renommé en **{name}**.",
"only_member_use_disband": "❌ Vous êtes le seul membre. Utilisez **Dissoudre le stack** pour terminer.",
"select_transfer_prompt": "Sélectionnez un membre à qui transférer la direction avant de partir :",
"left_stack": "✅ Vous avez quitté le stack.",
"application_withdrawn": "✅ Votre candidature a été retirée.",
"not_member_or_applicant": "❌ Vous n'êtes ni membre ni candidat de ce stack.",
"leader_only_manage": "❌ Seul le chef du stack peut le gérer.",
"leader_only_disband": "❌ Seul le chef du stack peut le dissoudre.",
"confirm_disband": "Êtes-vous sûr de vouloir dissoudre ce stack ? Cette action est irréversible.",
"already_active_stack": "⚠️ Vous avez déjà un stack actif. Si le message original a disparu (ex. après redémarrage du bot), vous pouvez forcer la dissolution et recommencer.",
"force_created": "✅ Stack précédent dissous. Nouveau stack créé.",
"no_active_stack": "❌ Vous n'avez pas de stack actif. Utilisez `/stack-create` pour en créer un.",
"could_not_parse_channel": "⚠️ Impossible de traiter l'ID du canal enregistré."
},
"commands": {
"common": {
"season": "La saison pour générer la carte",
"theme": "Thème de couleur de la carte",
"squadron_short": "Le nom court de l'escadron",
"player_username": "Le pseudo du joueur",
"choice_dark": "Sombre",
"choice_light": "Clair"
},
"comp": {
"description": "Trouver les dernières compos connues d'une équipe",
"squadron_short": "Nom court de l'équipe ennemie"
},
"quick_log": {
"description": "Créer une alerte pour cet escadron dans ce salon",
"squadron_name": "Nom COURT de l'escadron à surveiller",
"type": "Choisissez Logs, Points, Classement, BR Hebdomadaire ou Les deux",
"choice_logs": "Logs",
"choice_points": "Points",
"choice_leaderboard": "Classement",
"choice_both": "Les deux (Logs + Points)",
"choice_weekly_br": "BR Hebdomadaire"
},
"sq_info": {
"description": "Afficher les informations d'un escadron"
},
"sq_info_graph": {
"description": "Afficher un graphique de la composition de l'effectif par activité et taux de victoire (saison actuelle)"
},
"sq_card": {
"description": "Générer une carte de saison pour un escadron",
"squadron": "Nom court de l'escadron"
},
"sq_stats": {
"description": "Afficher les points d'un escadron dans le temps"
},
"loss_calculator": {
"description": "Calculer la perte de points si des joueurs quittent un escadron",
"player1": "Joueur qui part",
"player_optional": "Joueur qui part (facultatif)"
},
"website": {
"description": "Obtenir le lien du site SRE Bot"
},
"card": {
"description": "Générer une carte de saison pour un joueur"
},
"player_stats": {
"description": "Voir les statistiques détaillées des véhicules d'un joueur",
"username": "Pseudo WT pour la demande de stats",
"uid": "UID WT pour la demande de stats"
},
"view_player_games": {
"description": "Voir les 20 dernières parties d'un joueur"
},
"view_match": {
"description": "Voir un score de match par ID ou joueur",
"match_id": "ID hex de session du match",
"player_name": "Pseudo d'un joueur pour parcourir ses matchs récents"
},
"compare": {
"description": "Comparer les stats SQB globales de joueurs",
"player1": "Premier pseudo joueur",
"player2": "Deuxième pseudo joueur",
"player_optional": "Pseudo joueur supplémentaire (facultatif)"
},
"leaderboard": {
"description": "Obtenir le classement global de SRE Bot"
},
"set_squadron": {
"description": "Définir le tag d'escadron de ce serveur",
"abbreviated_name": "Nom court de l'escadron à définir"
},
"setup": {
"description": "Configurer le bot pour ce serveur"
},
"meta_management": {
"description": "Gérer l'accès aux données méta de ce serveur"
},
"meta": {
"description": "Chercher le roster méta par nom de véhicule",
"vehicle": "Nom du véhicule à rechercher"
},
"top": {
"description": "Voir le top 20 des escadrons avec stats détaillées"
},
"language": {
"description": "Changer la langue du bot."
},
"translate_message": {
"name": "Traduire le message"
},
"sq_track": {
"description": "Suivre un escadron et comparer depuis la dernière vérification",
"squadron_short_name": "Nom court de l'escadron à suivre"
},
"analytics": {
"description": "Voir les analyses SQB avancées d'un escadron",
"view": "Vue d'analyse à afficher",
"choice_maps": "Taux de victoire par carte",
"choice_comps": "Compositions d'équipe",
"choice_consistency": "Régularité des joueurs",
"choice_time": "Heure de la journée",
"choice_matchups": "Historique des duels"
},
"recent": {
"description": "Afficher les batailles récentes d'un escadron",
"length": "Nombre de matchs à afficher"
},
"vs": {
"description": "Face-à-face entre deux escadrons",
"squadron_a": "Premier escadron",
"squadron_b": "Deuxième escadron"
},
"autolog_management": {
"description": "Gérer les notifications autolog et diagnostiquer les permissions"
},
"diagnose_perms": {
"description": "Diagnostiquer les permissions autolog de ce salon"
},
"unlock": {
"description": "Débloquer les fonctionnalités Premium pour ce serveur"
},
"credits": {
"description": "Voir l'équipe créditée pour ce projet"
},
"schedule": {
"description": "Voir le calendrier BR de la saison actuelle"
},
"news": {
"description": "Voir les dernières nouvelles et annonces de SRE Bot"
},
"help": {
"description": "Voir le guide, les CGU et les liens de support"
},
"donate": {
"description": "Soutenir le développement de SRE Bot"
},
"stack_create": {
"description": "Créer une stack de joueurs",
"vehicle": "Avec quel véhicule vas-tu commencer ?"
},
"stack_manage": {
"description": "Republier ta stack active dans ce salon"
},
"bot_status": {
"description": "Voir le statut du bot : dernière partie reçue et TTL moyen"
}
},
"permission": {
"blacklisted_title": "❌ Liste noire",
"blacklisted_desc": "Tu es bloqué et ne peux pas utiliser cette commande.",
"reason_line": "**Raison :** {reason}",
"access_denied_title": "⛔ Accès refusé",
"no_permission_desc": "Tu n'as pas la permission d'utiliser cette commande.",
"unexpected_error_title": "❗ Erreur, signale-la...."
},
"weekly_br": {
"title_wildcard": "Rapport BR hebdomadaire — {br} BR",
"title_squadron": "Rapport BR hebdomadaire — [{tag}] {long} • {br} BR",
"window_label": "Période : {start} → {end}",
"wildcard_desc_first": "Top {count} escadrons par ELO • Rangs {low}{high}",
"wildcard_desc_second": "Top {count} escadrons par ELO • Rangs {low}{high}",
"squadron_stats_line": "- {games} parties • K/D {kdr} • Victoires {wr}%",
"top_players_inline_header": "🥇 Meilleurs joueurs :",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}p)",
"top_players_header": "**Top {count} joueurs par ELO :**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} parties • K/D {kdr}",
"squadron_header_line": "ELO escadron : {score} • {games} parties • Victoires {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO escadron : pas assez d'activité d'équipe cette semaine.",
"no_data": "Aucun match enregistré pour [{tag}] cette rotation BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Errore",
"no_data_title": "Nessun dato",
"access_denied_title": "Accesso negato",
"access_denied_desc": "Questo server è stato inserito nella lista nera.",
"no_players_selected": "Nessun giocatore selezionato. Seleziona almeno un giocatore.",
"must_use_in_server": "Questo comando deve essere usato in un server.",
"could_not_resolve_channel": "Impossibile trovare il canale selezionato.",
"failed_update_setting": "❌ Impossibile aggiornare l'impostazione.",
"configuration_not_found": "Configurazione non trovata.",
"no_channel_selected": "Nessun canale selezionato.",
"no_selection_received": "Nessuna selezione ricevuta.",
"database_error": "❌ Errore database: {error}",
"enabled": "Attivo",
"disabled": "Disattivato",
"not_configured": "Non configurato",
"unknown": "Sconosciuto",
"rating_field": "Valutazione",
"battles_field": "Battaglie",
"wins_field": "Vittorie",
"losses_field": "Sconfitte",
"win_rate_field": "Percentuale vittorie",
"kills_field": "Eliminazioni",
"deaths_field": "Morti",
"kd_field": "K/D",
"members_field": "Membri",
"placement_field": "Posizione",
"points_field": "Punti",
"ground_kills_field": "Eliminazioni terrestri",
"air_kills_field": "Eliminazioni aeree",
"total_kills_field": "Eliminazioni totali",
"assists_field": "Assistenze",
"captures_field": "Catture",
"none_option": "Nessuno"
},
"buttons": {
"skip": "Salta",
"previous": "Precedente",
"next": "Successivo",
"prev": "Prec",
"prev_arrow": "◀ Precedente",
"next_arrow": "Successivo ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Genera grafico",
"show_graph": "Mostra grafico",
"view_player_stats": "📊 Vedi statistiche giocatori",
"compare_nearby": "📈 Confronta squadriglie vicine",
"confirm_swap": "Sì, cambiala",
"cancel_swap": "No, mantieni quella vecchia",
"set_squadron": "Imposta Squadriglia",
"same_as_logs": "Come Registri",
"require_password": "🔒 Richiedi Password",
"password_required": "🔒 Password Richiesta",
"lock_data": "🔐 Vincola Dati Squadriglia",
"data_locked": "🔐 Dati Vincolati al Server",
"allow_public": "👥 Consenti Meta Pubblico",
"public_enabled": "👥 Meta Pubblico Attivo",
"update_accounts": "📋 Aggiorna Account Meta",
"change_password": "🔑 Cambia Password",
"help": "❓ Aiuto",
"add_player": " Aggiungi Giocatore",
"update_all": "🔄 Aggiorna Tutti i Membri",
"back_to_settings": "⬅ Torna alle Impostazioni",
"manage_notifications": "Gestisci Notifiche",
"diagnose_permissions": "Diagnostica Permessi",
"enable": "Abilita",
"disable": "Disabilita",
"change_channel": "Cambia Canale",
"view_replay": "Visualizza Replay",
"view_website": "Visualizza sul Sito",
"view_video": "Visualizza Video",
"view_log": "Visualizza Registro",
"view_chat": "Visualizza Chat",
"subscribe_website": "Abbonati tramite Sito Web",
"yes_disband": "Sì, sciogli",
"cancel": "Annulla",
"transfer_leave": "Trasferisci e lascia",
"accept_selected": "Accetta selezionati",
"accept_all": "Accetta tutti",
"decline_selected": "Rifiuta selezionati",
"back": "Indietro",
"remove_all": "Rimuovi tutti",
"remove_active": "Rimuovi attivi",
"remove_queued": "Rimuovi in coda",
"remove_selected": "Rimuovi selezionati",
"ping_all": "Notifica tutti",
"ping_active": "Notifica attivi",
"ping_queued": "Notifica in coda",
"ping_selected": "Notifica selezionati",
"accept_members": "Accetta membri",
"remove_members": "Rimuovi membri",
"ping_members": "Notifica membri",
"rename_stack": "Rinomina stack",
"request_to_join": "Richiedi di unirti",
"leave_withdraw": "Lascia / Ritira",
"manage_stack": "Gestisci stack ⚙️",
"disband_stack": "Sciogli stack",
"force_disband_create": "Forza scioglimento e crea nuovo"
},
"events": {
"guild_join_title": "Grazie per avermi aggiunto!",
"guild_join_desc": "Esegui `/setup` per configurare il bot per questo server."
},
"comp": {
"not_found_title": "Composizioni non trovate",
"not_found_desc": "Nessun dato per **{squadron}**, riprova più tardi.",
"error_loading_title": "Errore nel caricamento delle composizioni",
"error_loading_desc": "Impossibile caricare i dati delle composizioni: {error}",
"title": "Composizioni per {squadron}",
"desc": "Composizioni viste negli ultimi {minutes} minuti",
"no_recent_title": "Nessuna composizione recente",
"no_recent_desc": "Nessuna composizione negli ultimi {minutes} minuti.",
"comp_title": "COMP {index}",
"last_seen_label": "**Ultima volta**: {timestamp}{warning}",
"comp_label": "**Comp**: {notation}",
"no_players_recorded": "Nessun giocatore registrato.",
"limit_reached_title": "Limite composizioni raggiunto",
"limit_reached_desc": "Questo server ha esaurito tutte le {limit} ricerche di composizioni per questo slot. Abbonati (con /unlock) per accesso illimitato o attendi il prossimo slot.",
"remaining_footer": "{remaining}/{limit} ricerche di composizioni rimanenti in questo slot"
},
"quick_log": {
"invalid_type": "Il tipo può essere impostato solo su Log, Punti, Classifica, BR Settimanale o Entrambi.",
"squadron_required": "Devi fornire il nome di una Squadriglia per gli allarmi Registri, Punti o Entrambi.",
"wildcard_logs_only": "Solo i Registri possono essere impostati su Squadriglia wildcard.",
"squadron_not_resolved": "La Squadriglia `{squadron}` non è stata trovata.",
"save_failed": "Impossibile salvare le preferenze. Riprova più tardi.",
"premium_warning": "\n\n> ⚠️ **I registri di gioco richiedono Premium.** Esegui `/unlock` per abbonarti ($2.99/mese) — i registri non verranno inviati fino ad allora.",
"leaderboard_set": "Allarme Classifica Globale impostato su questo canale.",
"both_set": "Allarmi Registri e Punti per {squadron} impostati su questo canale.{premium_note}",
"alarm_set": "Allarme {alarm_type} per {squadron} impostato su questo canale.{premium_note}",
"weekly_br_wildcard_set": "Report BR Settimanale (top 20 squadroni) configurato per questo canale. Inviato alla fine di ogni rotazione BR.",
"weekly_br_squadron_set": "Report BR Settimanale per {squadron} (top 15 giocatori) configurato per questo canale. Inviato alla fine di ogni rotazione BR."
},
"diagnostics": {
"title": "Diagnostica autolog",
"channel_permissions_header": "**Permessi Canale** (<#{channel_id}>)",
"perms_needed": " ^ L'autologging necessita di tutti i permessi sopra per inviare le classifiche.",
"server_squadron_header": "**Squadriglia del Server** (`/set-squadron`)",
"server_squadron_short": " Breve: `{short}`",
"server_squadron_long": " Esteso: `{long}`",
"server_squadron_not_set": " Non impostato (il colore della barra della classifica mostrerà 'not_set')",
"autolog_prefs_header": "**Preferenze Autolog** (`/quick-log`)",
"autolog_none_configured": " ❌ NESSUNO configurato - l'autologging NON invierà nulla a questo server.",
"autolog_setup_hint": " Usa `/quick-log <squadron_short> Logs` nel canale di destinazione per configurarlo.",
"autolog_no_logs_channels": " ❌ Nessun canale Registri configurato. Trovati solo Punti/Classifica.",
"autolog_enable_hint": " Usa `/quick-log <squadron_short> Logs` per abilitare l'autologging.",
"selected_channel_tag": " **(canale selezionato)**",
"missing_send_attach": " (permessi di invio/allegato mancanti)",
"channel_not_found": " (canale non trovato)",
"invalid_channel_id": " (ID canale non valido)",
"premium_status_header": "**Stato Premium** (`/unlock`)",
"premium_active": " ✅ Questo server ha un abbonamento Premium attivo.",
"premium_not_subscribed": " ❌ Questo server **non** ha un abbonamento Premium.",
"premium_autolog_required": " L'autologging richiede Premium. Usa `/unlock` per abbonarti.",
"premium_not_subscribed_free": " ⚪ Non abbonato — usa `/unlock` per abbonarti ($2.99/mese).",
"premium_free_note": " *(Gli autolog sono gratuiti per tutti i server al momento.)*"
},
"sq_info": {
"title": "Info squadriglia: {squadron}",
"placement_field": "Posizione",
"total_points_field": "Punti totali",
"total_members_field": "Membri totali",
"members_field": "Membri",
"fetch_failed": "Impossibile recuperare le informazioni della Squadriglia."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Stagione {season})",
"embed_title": "{squadron} — Composizione del roster",
"embed_desc": "Stagione **{season}** · Mediana partite: **{median}** · Nucleo: **{core}** · Attivi: **{active}** · Deboli: **{weak}**\nBarre ordinate per partite desc; altezza = tasso di vittoria. Nucleo = top 30 % di TV e partite ≥ mediana. Attivi = top 3045 % di TV e partite ≈ mediana. Deboli = tutti gli altri.",
"core_threshold_line": "NUCLEO ≥ {wr} %",
"weak_threshold_line": "DEBOLI < {wr} %",
"y_label": "Tasso di vittoria",
"core_header": "NUCLEO — {count} · TV {avg}%",
"active_header": "ATTIVI — {count} · TV {avg}%",
"weak_header": "DEBOLI — {count} · TV {avg}%",
"no_active_season": "Nessuna stagione attiva trovata. Riprova quando inizia la prossima.",
"no_members": "Nessun membro attuale trovato per {squadron}."
},
"recap_card": {
"unknown_season": "Stagione sconosciuta: `{season}`.",
"no_clan_id": "Impossibile risolvere l'ID della squadriglia `{squadron}`.",
"render_failed": "Impossibile generare la card di riepilogo stagione. Riprova più tardi."
},
"sq_stats": {
"no_data_title": "Nessun dato",
"no_data_desc": "Nessun dato storico trovato per la Squadriglia: {squadron}",
"title": "{squadron} // SQUADRIGLIA",
"desc": "Andamento punteggio totale (ultimi {count} dati)",
"previous_score_field": "Punteggio precedente",
"current_score_field": "Punteggio attuale",
"change_field": "Variazione",
"player_title": "{squadron} // GIOCATORI",
"player_desc": "Andamento punti individuali dei giocatori",
"comparison_title": "{squadron} // CONFRONTO CLASSIFICA",
"comparison_desc": "Confronto con Squadriglie classificate {range}",
"current_position_field": "Posizione attuale",
"squadrons_shown_field": "Squadriglie mostrate",
"squadron_not_found_error": "Squadriglia non trovata nella classifica",
"no_nearby_error": "Nessuna Squadriglia vicina trovata",
"no_historical_error": "Nessun dato storico trovato per le Squadriglie vicine",
"comparison_chart_failed": "Impossibile generare il grafico di confronto",
"select_players_placeholder": "Seleziona giocatori (Pagina {page})"
},
"loss_calc": {
"title": "Perdita punti — {squadron}",
"players_leaving_field": "Giocatori in partenza",
"share_of_total_field": "% del totale",
"points_lost_real_field": "Punti persi (reali)",
"points_lost_raw_field": "Punti persi (grezzi)",
"squadron_rating_field": "Valutazione squadriglia",
"squadron_position_field": "Posizione squadriglia",
"positions_lost_field": "Posizioni perse",
"not_found_footer": "Non trovato nella Squadriglia: {players}",
"fetch_failed": "Impossibile recuperare i dati della Squadriglia: {error}",
"no_point_data": "Nessun dato punti disponibile per questa Squadriglia.",
"no_matching_players": "Nessun giocatore corrispondente trovato in **{squadron}**."
},
"player": {
"select_player_placeholder": "Seleziona un giocatore",
"no_stats_found": "❌ Nessuna statistica trovata per UID: {uid}",
"no_vehicle_stats": "❌ Nessuna statistica veicolo trovata per questo giocatore.",
"vehicles_found": "Trovati **{count}** veicoli per **{nick}**\nSeleziona un veicolo per visualizzare le statistiche dettagliate:",
"vehicle_select_placeholder": "Seleziona un veicolo (Pagina {page}/{total})",
"combat_stats_header": "**__STATISTICHE COMBATTIMENTO__**",
"ground_kills_label": "**Eliminazioni terrestri:** {value}",
"air_kills_label": "**Eliminazioni aeree:** {value}",
"total_kills_label": "**Eliminazioni totali:** {value}",
"assists_label": "**Assistenze:** {value}",
"deaths_label": "**Morti:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Catture:** {value}",
"battle_record_header": "**__STORICO BATTAGLIE__**",
"total_battles_label": "**Battaglie totali:** {value}",
"wins_label": "**Vittorie:** {value}",
"losses_label": "**Sconfitte:** {value}",
"win_rate_label": "**Percentuale vittorie:** {value}%",
"stats_desc": "Statistiche per **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Giocatore non trovato",
"not_found_desc": "Nessuna cronologia di gioco trovata per `{player}`.",
"no_players_found": "Nessun giocatore trovato corrispondente a **{username}**\nProva a usare `/website` per cercare sul sito.",
"multiple_matches": "Trovate più corrispondenze, scegli quella corretta qui sotto:",
"must_provide_input": "Devi fornire almeno un UID o un nome utente."
},
"player_games": {
"no_recent_title": "Nessuna partita recente",
"no_recent_desc": "Nessuna partita trovata per **{player}** nelle ultime 8 ore.",
"squadron_label": "**Squadriglia:** {squadron}",
"record_label": "**V:** {wins} **S:** {losses} **%V:** {wr}%",
"comps_played_header": "\n\n**Composizioni giocate**"
},
"match": {
"missing_input_title": "Input mancante",
"missing_input_desc": "Fornisci un `match_id` o un `player_name`.",
"not_found_title": "Partita non trovata",
"not_found_desc": "Impossibile trovare una partita con ID `{match_id}`.",
"invalid_data_title": "Dati partita non validi",
"invalid_data_desc": "I dati del replay non possono essere elaborati.",
"scoreboard_error_title": "Errore classifica",
"scoreboard_error_desc": "Impossibile generare l'immagine della classifica.",
"no_games_title": "Nessuna partita trovata",
"no_games_desc": "Nessuna cronologia di gioco trovata per **{player}**.",
"recent_matches_title": "Partite recenti per {player}",
"recent_matches_desc": "Mostra fino a {count} partite recenti. Selezionane una per visualizzare la classifica completa.",
"select_match_placeholder": "Seleziona una partita da visualizzare..."
},
"compare": {
"no_players_found": "Nessun giocatore trovato corrispondente a **{name}**.",
"multiple_matches": "Più corrispondenze per **{name}**: {matches}\nUsa un nome più specifico (i suggerimenti di completamento automatico sono esatti).",
"could_not_resolve": "Impossibile trovare i giocatori.",
"could_not_fetch": "❌ Impossibile recuperare le statistiche per **{name}**.",
"no_graph_data": "Nessun dato disponibile per gli ultimi 90 giorni.",
"no_squadron_points_data": "Nessun dato punti Squadriglia per {names} (giocatore non trovato nella cronologia Squadriglia tracciata).",
"graph_title": "Punti giocatore — ultimi 90 giorni",
"battles_label": "Battaglie",
"wins_label": "Vittorie",
"losses_label": "Sconfitte",
"win_rate_label": "Percentuale vittorie",
"ground_kills_label": "Eliminazioni terrestri",
"air_kills_label": "Eliminazioni aeree",
"total_kills_label": "Eliminazioni totali",
"assists_label": "Assistenze",
"deaths_label": "Morti",
"kd_label": "K/D",
"captures_label": "Catture"
},
"squadron": {
"not_found_desc": "Squadriglia `{squadron}` non trovata.",
"set_title": "✅ Squadriglia impostata",
"set_desc": "La squadriglia **{squadron}** è stata impostata per questo server.",
"short_name_field": "Nome breve",
"long_name_field": "Nome esteso",
"swap_title": "✅ Squadriglia cambiata",
"swap_desc": "**{old}** sostituito con **{new}** per questo server.",
"already_set_title": "⚠️ Squadriglia già impostata",
"already_set_desc": "Questo server è attualmente impostato su **{old}**.\nCambiarlo con **{new}**?",
"swap_cancelled": "❌ Cambio squadriglia annullato."
},
"setup": {
"step1_title": "Configurazione Server — Passaggio 1 di 3",
"step1_desc": "Questa procedura guidata ti aiuterà a configurare il bot per il tuo server.\n\n**Passaggio 1** — Imposta la tua squadriglia\n**Passaggio 2** — Scegli un canale registri\n**Passaggio 3** — Scegli un canale punti\n",
"step1_current_sq": "\nSquadriglia attualmente configurata: **[{short}] {long}**",
"step2_title": "Configurazione Server — Passaggio 2 di 3",
"step2_desc": "Squadriglia impostata su **[{short}] {long}**.\n\nDove devono essere pubblicati i **registri di battaglia**?\nSeleziona un canale di testo qui sotto, oppure salta questo passaggio.",
"step3_title": "Configurazione Server — Passaggio 3 di 3",
"step3_desc": "Dove devono essere pubblicate le **notifiche punti**?\nSeleziona un canale di testo qui sotto, oppure salta questo passaggio.",
"step3_same_as_logs": "\n\nPuoi anche cliccare su \"Come Registri\" per riutilizzare il canale registri.",
"summary_title": "Configurazione Completata",
"summary_desc": "Puoi usare `/autolog-management` per modificare queste impostazioni in seguito.",
"squadron_field": "Squadriglia",
"logs_channel_field": "Canale Registri",
"points_channel_field": "Canale Punti",
"premium_required_field": "⚠️ I Registri di Gioco Richiedono Premium",
"premium_required_value": "Le classifiche automatiche non verranno inviate finché questo server non ha un abbonamento attivo. Esegui `/unlock` per abbonarti ($2.99/mese).",
"modal_title": "Imposta Squadriglia",
"modal_label": "Nome Breve Squadriglia",
"modal_placeholder": "es. AXYS",
"squadron_not_found": "Squadriglia `{squadron}` non trovata. Riprova.",
"logs_channel_placeholder": "Seleziona un canale registri...",
"points_channel_placeholder": "Seleziona un canale punti..."
},
"meta_management": {
"squadron_not_found_title": "❌ Squadriglia Non Trovata",
"squadron_not_found_desc": "Impossibile trovare l'ID clan per la Squadriglia: **{squadron}**",
"access_denied_title": "❌ Accesso Negato",
"access_denied_desc": "Password errata. I dati meta di questa Squadriglia sono protetti.",
"data_locked_title": "🔐 Dati squadriglia vincolati",
"data_locked_desc": "**{squadron}** ha la vincolazione dati attivata e non può essere trasferita a un altro server.\n\nIl proprietario della Squadriglia deve disabilitare **Vincola Dati Squadriglia** prima che possa essere spostata.",
"error_retrieving_settings": "❌ Errore nel recupero delle impostazioni del server dopo il trasferimento. Riprova.",
"error_retrieving_settings_retry": "❌ Errore nel recupero delle impostazioni del server. Prova a eseguire nuovamente il comando.",
"authenticated_title": "✅ Autenticato",
"authenticated_desc": "Password verificata. Gestione impostazioni per **{squadron}**.",
"claimed_title": "✅ Squadriglia Rivendicata",
"claimed_desc": "**{squadron}** è stata rivendicata con successo per questo server!",
"password_requirement_field": "🔒 Requisito password",
"data_lock_field": "🔐 Vincolazione dati squadriglia",
"public_meta_field": "👥 Accesso meta pubblico",
"access_password_field": "🔑 Password di accesso",
"enabled_value": "✅ Attivo",
"disabled_value": "❌ Disattivo",
"settings_title": "🔐 Impostazioni gestione meta",
"settings_desc": "**Squadriglia:** {squadron}\n**ID Clan:** {clan_id}",
"first_time_title": "🔐 Gestione meta - Prima configurazione",
"first_time_owner_desc": "**Squadriglia:** {squadron}\n**ID Clan:** {clan_id}\n\n🔑 La tua password di accesso è stata generata. **Salva questa password** — ti servirà per autenticare l'accesso ai dati meta in futuro.\n\n**Password:** `{password}`",
"first_time_non_owner_desc": "**Squadriglia:** {squadron}\n**ID Clan:** {clan_id}\n\nLa Squadriglia è stata configurata. Chiedi al proprietario del server la password di accesso.",
"settings_field": "Impostazioni",
"settings_hint": "Usa i pulsanti qui sotto per configurare le impostazioni di accesso.",
"password_toggled": "✅ Requisito password: **{state}**",
"lock_toggled": "✅ Vincolazione dati Squadriglia: **{state}**",
"public_meta_toggled": "✅ Accesso meta pubblico: **{state}**\n{detail}",
"public_meta_enabled_detail": "I non-amministratori possono ora usare il comando `/meta`.",
"public_meta_disabled_detail": "Solo gli amministratori possono usare il comando `/meta`.",
"owner_only_password": "❌ Solo il proprietario del server può cambiare la password della Squadriglia.",
"help_title": "📖 Guida Gestione Meta",
"help_desc": "Spiegazione di ogni impostazione e funzione:",
"help_password_field": "🔑 Password di Accesso",
"help_password_value": "La password di accesso della tua Squadriglia. Solo il **proprietario del server** può vedere la password nel pannello impostazioni. Chiunque conosca la password può rivendicare i dati meta della tua Squadriglia sul proprio server, quindi tienila al sicuro.",
"help_require_field": "🔒 Richiedi Password",
"help_require_value": "Quando attivo, anche gli amministratori su questo server devono inserire la password della Squadriglia per accedere a `/meta-management`. Aggiunge un ulteriore livello di sicurezza per prevenire modifiche accidentali.",
"help_lock_field": "🔐 Vincola Dati Squadriglia",
"help_lock_value": "Quando attivo, impedisce il trasferimento della Squadriglia ad altri server, anche con la password corretta. Deve essere disabilitato prima che la Squadriglia possa essere trasferita.",
"help_public_field": "👥 Consenti Meta Pubblico",
"help_public_value": "Quando attivo, consente ai membri non-amministratori di usare il comando `/meta` per cercare i veicoli della Squadriglia. Quando disabilitato, solo gli amministratori del server possono usare `/meta`.",
"help_accounts_field": "📋 Aggiorna Account Meta",
"help_accounts_value": "Apre il gestore del registro giocatori dove puoi aggiungere o rimuovere giocatori dal registro meta della tua Squadriglia. Usa **Aggiorna Tutti i Membri** per sincronizzare l'intera Squadriglia in una volta sola.",
"help_change_pw_field": "🔑 Cambia Password",
"help_change_pw_value": "**Solo proprietario del server.** Cambia la password di accesso della Squadriglia e imposta un suggerimento opzionale. Il suggerimento viene mostrato nella richiesta di password per aiutare a ricordarla.",
"password_modal_title": "Password di Accesso Squadriglia",
"password_modal_label": "Inserisci la Password della Squadriglia",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Cambia Password Squadriglia",
"current_password_label": "Password Attuale",
"current_password_placeholder": "Inserisci la tua password attuale",
"new_password_label": "Nuova Password",
"new_password_placeholder": "Inserisci la tua nuova password",
"confirm_password_label": "Conferma Nuova Password",
"confirm_password_placeholder": "Reinserisci la tua nuova password",
"hint_label": "Suggerimento Password (Opzionale)",
"hint_placeholder": "Un suggerimento per ricordare la password",
"pw_incorrect": "❌ La password attuale non è corretta.",
"pw_mismatch": "❌ Le nuove password non corrispondono. Riprova.",
"pw_empty": "❌ La nuova password non può essere vuota.",
"pw_changed": "✅ Password aggiornata con successo per **{squadron}**.\n**Nuova Password:** `{password}`",
"pw_changed_hint": "\n**Suggerimento:** {hint}",
"player_add_modal_title": "Aggiungi Giocatore al Registro Meta",
"player_add_label": "UID o Nome del Giocatore",
"player_add_placeholder": "Inserisci l'UID del giocatore (es. 12345678) o il nickname",
"player_not_found": "❌ Giocatore `{player}` non trovato nel database Players_Global.\n",
"roster_title": "📋 Gestione Registro Meta - {squadron}",
"roster_desc": "**ID Clan Squadriglia:** {clan_id}\n**Giocatori Totali:** {count}",
"roster_page_field": "Giocatori (Pagina {page}/{total})",
"no_players_field": "Nessun Giocatore",
"no_players_hint": "Nessun giocatore aggiunto al registro meta. Clicca **Aggiungi Giocatore** per iniziare.",
"remove_player_placeholder": "Seleziona giocatore da rimuovere...",
"fetch_members_failed": "❌ Impossibile recuperare i membri della Squadriglia: {error}",
"no_members_found": "❌ Nessun membro trovato nella Squadriglia o chiamata API fallita.",
"roster_synced": "✅ Registro sincronizzato con la Squadriglia.",
"roster_added": "**+{count}** aggiunti",
"roster_removed": "**-{count}** rimossi (hanno lasciato la Squadriglia)",
"roster_up_to_date": "**{count}** già aggiornati",
"refreshing_vehicles": "Aggiornamento dati veicoli in background..."
},
"meta": {
"not_configured": "❌ Dati meta non configurati per questo server. Esegui prima `/meta-management`.",
"no_permission": "❌ Hai bisogno dei permessi di amministratore per usare questo comando.\nGli amministratori possono abilitare l'accesso pubblico tramite `/meta-management`.",
"no_results": "❌ Nessun giocatore nel registro della tua Squadriglia possiede **{vehicle}**.",
"no_results_admin_hint": "\n*Ti aspetti che qualcuno lo abbia? Clicca il pulsante di aggiornamento membri in `/meta-management` e ricontrolla.*",
"search_title": "🔍 Risultati Ricerca - {vehicle}",
"matches_found": "**Corrispondenze trovate:** {count} giocatore/i",
"spawns_label": "Spawn",
"deaths_label": "Morti",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "Punti",
"kdr_label": "KDR",
"games_label": "Partite",
"no_points": "—"
},
"top": {
"title": "**Top 20 Squadriglie**",
"rating_label": "**Valutazione:** {value}",
"air_kills_label": "**Eliminazioni aeree:** {value}",
"ground_kills_label": "**Eliminazioni terrestri:** {value}",
"deaths_label": "**Morti:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Percentuale vittorie:** {value}",
"playtime_label": "**Tempo di gioco:** {value}",
"fetch_failed": "Impossibile recuperare i dati della Squadriglia."
},
"analytics": {
"no_data_title": "Nessun dato",
"no_matches_desc": "Nessuna partita trovata.",
"no_comp_desc": "Nessun dato composizione trovato.",
"no_consistency_desc": "Dati giocatore insufficienti (minimo 50 partite).",
"no_time_desc": "Nessun dato orario trovato.",
"unknown_view": "Vista sconosciuta.",
"map_title": "Percentuali vittorie per mappa: {squadron}",
"comp_title": "Composizioni di squadra: {squadron}",
"consistency_title": "Consistenza giocatori: {squadron}",
"consistency_desc": "Ordinato per rapporto K/D",
"time_title": "Prestazioni per fascia oraria: {squadron}",
"eu_timeslot": "\n**Fascia Europea**",
"na_timeslot": "\n**Fascia Nord Americana**",
"off_peak": "\n**Fuori orario di punta**",
"matchups_title": "📜 {squadron} — Storico Scontri",
"matchups_won_field": "🏆 Più Vittorie Contro",
"matchups_lost_field": "💀 Più Sconfitte Contro",
"no_matchups_desc": "Nessuna partita registrata contro altri squadroni."
},
"recent": {
"title": "Partite recenti: {squadron}",
"no_matches_desc": "Nessuna partita trovata per questa Squadriglia."
},
"h2h": {
"two_required_title": "Due squadriglie richieste",
"two_required_desc": "Fornisci almeno una Squadriglia, oppure usa `/set-squadron` e fornisci l'avversario.",
"provide_a_desc": "Fornisci `squadron_a` oppure usa prima `/set-squadron`.",
"provide_b_desc": "Fornisci `squadron_b` oppure usa prima `/set-squadron`.",
"squadron_not_found_title": "Squadriglia non trovata",
"same_squadron_title": "Stessa squadriglia",
"same_squadron_desc": "Non puoi controllare il testa a testa contro te stesso.",
"record_desc": "**Record:** {a_wins}V - {b_wins}S ({total} partite)",
"no_matches_desc": "Nessuna partita registrata tra **{a}** e **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Attivo — l'autologging è abilitato per questo server.",
"premium_not_subscribed_line": "❌ **Premium:** Non abbonato — usa `/unlock` per abilitare l'autologging.",
"premium_free_line": "⚪ **Premium:** Non abbonato — usa `/unlock` per abbonarti ($2.99/mese). *(Gli autolog sono gratuiti per tutti i server al momento.)*",
"what_to_do": "\n\nCosa vorresti fare?",
"select_notif_type": "Seleziona il tipo di notifica da gestire:",
"select_notif_placeholder": "Seleziona tipo di notifica",
"logs_option": "Registri",
"logs_option_desc": "Gestisci le notifiche Registri",
"points_option": "Punti",
"points_option_desc": "Gestisci le notifiche Punti",
"leaderboard_option": "Classifica",
"leaderboard_option_desc": "Gestisci le notifiche Classifica",
"selected_type": "Selezionato **{type}**. Ora scegli la Squadriglia da gestire:",
"select_squadron_placeholder": "Seleziona una Squadriglia",
"select_squadron_page_placeholder": "Seleziona una Squadriglia (Pagina {page})",
"no_squadrons_available": "Nessuna Squadriglia disponibile per questo tipo di notifica.",
"managing_global": "Gestione **{type}** (globale) nel canale **{channel}**.",
"managing_squadron": "Gestione **{type}** per la Squadriglia **{squadron}** nel canale **{channel}**.",
"select_channel": "Seleziona un nuovo canale:",
"select_channel_placeholder": "Seleziona un canale",
"select_channel_page_placeholder": "Seleziona un canale (Pagina {page})",
"global_toggled": "{type} (globale) è ora {state}.",
"squadron_toggled": "{type} per **{squadron}** è ora {state}.",
"channel_updated_global": "Aggiornato {type} (globale) a {channel}",
"channel_updated_squadron": "Aggiornato {type} per **{squadron}** a {channel}",
"diagnose_channel_placeholder": "Seleziona un canale da diagnosticare...",
"select_channel_diagnose": "Seleziona il canale da diagnosticare:",
"game_not_logged_title": "Partita non registrata",
"game_not_logged_desc": "Usa `/unlock` per sottoscrivere il piano **Standard** (o superiore) e ricevere le classifiche automatiche.",
"server_not_upgraded_title": "⚠️ Server non aggiornato",
"server_not_upgraded_autolog_desc": "Questo server non ha un abbonamento Premium attivo.\n\n**Le classifiche automatiche smetteranno di essere inviate ai server non aggiornati dopo <t:{deadline}:D>.**\n\nUsa `/unlock` per abbonarti e continuare a ricevere i registri di gioco automatici.",
"replay_not_available": "I dati del replay non sono ancora disponibili — aspetta un po' e riprova!",
"too_many_videos": "Troppi video in rendering al momento — riprova tra un attimo.",
"video_gen_failed": "Errore nella generazione del video: `{error}`",
"video_missing": "Impossibile generare il video del replay - file di output mancante o vuoto.",
"video_too_large": "Video replay troppo grande da caricare ({file_mb:.1f} MB). Il limite del server è {limit_mb:.0f} MB.",
"video_web_fallback": "Puoi anche visualizzare questa partita su {url}",
"video_upload_failed": "Video troppo grande da caricare — visualizzalo sul sito:\n{url}",
"video_unexpected_error": "Errore imprevisto nella generazione del video replay: `{error}`",
"replay_not_found": "Dati replay non trovati per la sessione `{session_id}` su disco.",
"chat_log_title": "**Registro Chat della Partita [{session_id}]({url})**",
"chat_log_part_title": "**Registro Chat della Partita [{session_id}]({url}) (Parte {part}/{total})**",
"chat_log_part_only": "**Registro Chat (Parte {part}/{total})**",
"no_chat_log": "Nessun registro chat trovato per la sessione `{session_id}`.",
"chat_log_error": "Errore imprevisto nel caricamento del registro chat: `{error}`",
"battle_log_title": "**Registro Battaglia della Partita [{session_id}]({url})**",
"battle_log_part_title": "**Registro Battaglia della Partita [{session_id}]({url}) (Parte {part}/{total})**",
"battle_log_part_only": "**Registro Battaglia (Parte {part}/{total})**",
"no_battle_log": "Nessun evento di combattimento trovato per la sessione `{session_id}`.",
"battle_log_error": "Errore imprevisto nel caricamento del registro battaglia: `{error}`",
"points_update_title": "**{squadron} {region} Aggiornamento Punti**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Variazioni giocatori:**",
"points_table_header": "Nome Variaz. Ora\n",
"wl_line": "\n**{squadron}** è andata **{wins}V-{losses}S** in questa sessione",
"placement_rose": "\n**{squadron}** è salito al **{new_place}** dal **{old_place}**",
"placement_fell": "\n**{squadron}** è sceso al **{new_place}** dal **{old_place}**",
"points_not_logged_title": "Punti non registrati",
"points_not_logged_desc": "Usa `/unlock` per sottoscrivere il piano **Standard** (o superiore) e ricevere gli aggiornamenti automatici dei punti.",
"server_not_upgraded_points_desc": "Questo server non ha un abbonamento Premium attivo.\n\n**Gli aggiornamenti automatici smetteranno di essere inviati ai server non aggiornati dopo <t:{deadline}:D>.**\n\nUsa `/unlock` per abbonarti e continuare a ricevere gli aggiornamenti automatici.",
"leave_title": "⚠️ Giocatore Ha Lasciato {squadron}",
"leave_desc": "**{nick}** ({uid}) ha lasciato la Squadriglia.\n\nUltimi punti registrati: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Squadrone oltre il limite del tuo piano",
"over_cap_desc": "Il tuo server è sul piano **{tier}**, che consente **{cap} {notif}** squadroni. Lo squadrone **{squadron}** supera il limite e non viene registrato. Passa a un piano superiore per ripristinarlo.",
"over_cap_footer": "Aggiorna su srebot-meow.ing/premium o con /unlock",
"wildcard_blocked_title": "Il wildcard richiede un piano superiore",
"wildcard_blocked_desc": "Gli squadroni wildcard (*, all, everything) sono disponibili solo su Pro o Max. Il tuo server è su **{tier}** per {notif}. Aggiorna per abilitarli.",
"cap_header": "{used}/{cap} {notif} attivati — piano {tier}"
},
"track": {
"squadron_not_found": "Squadriglia non trovata.",
"fetch_failed": "Impossibile recuperare le informazioni della Squadriglia."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Sblocca le funzioni premium per questo server.**\n\nPremium include:\n> • Post automatici della classifica\n> • Registri chat e battaglia\n> • Ricerche replay\n> • Ricerche /comp illimitate\n> • Supporto prioritario\n\n**$2.99 / mese · per server · cancella quando vuoi**\n\n⚠️ La fatturazione Discord è disponibile solo in alcuni paesi. Se il pulsante qui sotto mostra **\"Prodotto Non Disponibile\"**, potrebbe essere a causa di un paese non supportato o dell'uso di un **dispositivo mobile**. Usa il pulsante **Abbonati tramite Sito Web** invece.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Questo server è già abbonato!**",
"manage_discord_field": "Gestisci Abbonamento",
"manage_discord_value": "Il tuo abbonamento è tramite **Discord**.\nPer annullare, vai su **Impostazioni Utente → Abbonamenti** in Discord.",
"manage_website_field": "Gestisci Abbonamento",
"manage_website_value": "Il tuo abbonamento è tramite il **sito web**.\nGestiscilo su [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Prossimamente",
"coming_soon_value": "Gli abbonamenti Premium non sono ancora disponibili. Ricontrolla presto!",
"current_tier": "Sei sul piano **{tier}**.",
"upgrade_to": "Passa a {tier}",
"upgrade_to_value": "Più squadroni e funzioni passando a **{tier}**."
},
"language": {
"prompt": "Seleziona la lingua del tuo server:",
"select_placeholder": "Scegli la lingua del tuo server",
"language_set": "Lingua impostata su {language}.",
"translate_prompt": "Seleziona una lingua di destinazione qui sotto 👇",
"translate_placeholder": "Scegli una lingua di destinazione…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Traduzione non disponibile (DeepL non configurato)",
"translation_failed": "Traduzione fallita"
},
"misc": {
"credits_title": "Crediti",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Lead Developer, Bot Manager, Community Manager\n> **Z3R0** - Developer, Optimization Developer, Database Engineer\n> **Clippii (Heidi)** - Developer, Website Developer, Community Manager\n> **LivingTheDagor** - Developer, Parser Developer, Consultant\n> **Lux_** - API Engineer, Spectra Developer\n> **Konigallerwaffen** - Consulente Feedback e Funzionalità\n> **Žralok Tonda** - Traduttore Ceco\n> **Styevy**, **Lopais** - Traduttori Tedeschi\n> **Susogus**, **playforfun698** - Traduttori Polacchi\n> **Bobr** - Traduttore Russo\n\n\n[Vuoi unirti a noi?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "PROGRAMMA DI STAGIONE",
"schedule_not_found_title": "Programma non trovato",
"schedule_not_found_desc": "Nessun dato del programma disponibile ancora.",
"news_no_news_title": "Nessuna notizia",
"news_no_news_desc": "Non ci sono annunci al momento. Ricontrolla più tardi!",
"news_footer": "Grazie per il tuo supporto! ᖙᘘᗢ",
"help_title": "Guida al Bot",
"donate_title": "Supporta SRE Bot",
"donate_desc": "Se ti piace usare SRE Bot e vuoi supportare il suo sviluppo, considera di offrirmi un caffè!\n\n**[Dona su Ko-fi](https://ko-fi.com/notsotoothless)**\n\nOgni contributo aiuta a mantenere il bot attivo e supporta nuove funzioni. Grazie!",
"status_title": "Stato del bot",
"status_last_received": "Ultima partita ricevuta",
"status_avg_ttl": "TTL medio (ultime 30)",
"status_no_data": "Nessun dato ancora",
"status_gaijin_slow": "⚠️ Server Gaijin lenti",
"help_commands_header": "**Panoramica comandi**",
"help_links": "Per i dettagli, leggi la documentazione [qui]({docs}) o chiedi supporto [qui]({support}).",
"help_terms": "[Termini di servizio]({terms}) • [Privacy Policy]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Classifica Squadriglie",
"top15_desc": "Top 15 Squadriglie con statistiche, inviato 35 minuti dopo la chiusura della fascia oraria.\nInviato <t:{timestamp}:R>.",
"top30_desc": "Squadriglie dalla 16ª alla 30ª con statistiche.",
"not_logged_title": "Classifica Non Registrata",
"not_logged_desc": "Usa `/unlock` per sottoscrivere il piano **Standard** (o superiore) e ricevere gli aggiornamenti automatici della classifica.",
"server_not_upgraded_title": "⚠️ Server non aggiornato",
"server_not_upgraded_desc": "Questo server non ha un abbonamento Premium attivo.\n\n**Gli aggiornamenti automatici smetteranno di essere inviati ai server non aggiornati dopo <t:{deadline}:D>.**\n\nUsa `/unlock` per abbonarti e continuare a ricevere gli aggiornamenti automatici."
},
"stacks": {
"stack_title": "Stack di {leader}",
"stack_named_title": "{name}",
"no_members": "Nessun membro ancora.",
"members_field": "Membri ({count}/{max})",
"queue_field": "Coda ({count}/{max})",
"manage_title": "Gestisci Stack",
"no_pending_requests": "Nessuna richiesta in sospeso.",
"disbanded_title": "Stack [Sciolto]",
"disbanded_desc": "Questo stack è stato sciolto dal leader.",
"expired_title": "Stack [Scaduto]",
"expired_desc": "Questo stack è scaduto.",
"join_modal_title": "Richiedi di unirti allo stack",
"join_vehicle_label": "Con cosa giocherai?",
"join_vehicle_placeholder": "es. F-16C, WZ305...",
"ping_modal_title": "Messaggio di notifica",
"ping_message_label": "Messaggio personalizzato (opzionale)",
"ping_message_placeholder": "es. Venite ora! Lo stack sta iniziando!",
"rename_modal_title": "Rinomina stack",
"rename_label": "Nome dello stack",
"rename_placeholder": "es. Gufi Notturni, Squadra Alfa...",
"select_new_leader": "Seleziona nuovo leader…",
"select_applicants": "Seleziona candidati…",
"no_pending_applications": "Nessuna candidatura in sospeso.",
"select_to_remove": "Seleziona persone da rimuovere…",
"no_members_or_applicants": "Nessun membro o candidato.",
"select_to_ping": "Seleziona persone da notificare individualmente…",
"stack_not_found": "❌ Stack non trovato.",
"no_longer_exists": "❌ Questo stack non esiste più.",
"member_not_exists": "❌ Quel membro non esiste più.",
"already_has_stack": "❌ Quel giocatore ha già uno stack attivo.",
"already_member": "❌ Sei già membro di questo stack.",
"already_applied": "❌ Hai già una candidatura in sospeso per questo stack.",
"queue_full": "❌ La coda è piena ({max}/{max}). Riprova più tardi.",
"application_sent": "✅ Candidatura inviata! Il leader dello stack la esaminerà.",
"stack_disbanded": "✅ Stack sciolto.",
"cancelled": "Annullato.",
"select_member_transfer": "❌ Seleziona un membro a cui trasferire la leadership.",
"ownership_transferred": "✅ Leadership trasferita a {nick}. Hai lasciato lo stack.",
"select_applicant_first": "❌ Seleziona almeno un candidato prima.",
"stack_full": "❌ Lo stack è già pieno ({max}/{max} membri).",
"select_person_first": "❌ Seleziona almeno una persona prima.",
"no_one_to_ping": "❌ Nessuno da notificare.",
"ping_footer": "Notificato da {leader} per {stack}.",
"pinged": "✅ Notificato!",
"select_from_dropdown": "❌ Seleziona almeno una persona dal menu a tendina prima.",
"stack_renamed": "✅ Stack rinominato in **{name}**.",
"only_member_use_disband": "❌ Sei l'unico membro. Usa **Sciogli stack** per terminare.",
"select_transfer_prompt": "Seleziona un membro a cui trasferire la leadership prima di uscire:",
"left_stack": "✅ Hai lasciato lo stack.",
"application_withdrawn": "✅ La tua candidatura è stata ritirata.",
"not_member_or_applicant": "❌ Non sei membro né candidato di questo stack.",
"leader_only_manage": "❌ Solo il leader dello stack può gestirlo.",
"leader_only_disband": "❌ Solo il leader dello stack può scioglierlo.",
"confirm_disband": "Sei sicuro di voler sciogliere questo stack? Questa azione non può essere annullata.",
"already_active_stack": "⚠️ Hai già uno stack attivo. Se il messaggio originale è scomparso (es. dopo riavvio del bot), puoi forzare lo scioglimento e ricominciare.",
"force_created": "✅ Stack precedente sciolto. Nuovo stack creato.",
"no_active_stack": "❌ Non hai uno stack attivo. Usa `/stack-create` per crearne uno.",
"could_not_parse_channel": "⚠️ Impossibile elaborare l'ID del canale memorizzato."
},
"commands": {
"common": {
"season": "La stagione per generare la card",
"theme": "Tema colore della card",
"squadron_short": "Nome breve dello squadrone",
"player_username": "Nome del giocatore",
"choice_dark": "Scuro",
"choice_light": "Chiaro"
},
"comp": {
"description": "Trova le ultime comp note di un team",
"squadron_short": "Nome breve del team nemico"
},
"quick_log": {
"description": "Imposta un allarme per questo squadrone in questo canale",
"squadron_name": "Nome BREVE dello squadrone da monitorare",
"type": "Scegli Log, Punti, Classifica, BR Settimanale o Entrambi",
"choice_logs": "Logs",
"choice_points": "Punti",
"choice_leaderboard": "Classifica",
"choice_both": "Entrambi (Logs + Punti)",
"choice_weekly_br": "BR Settimanale"
},
"sq_info": {
"description": "Mostra informazioni su uno squadrone"
},
"sq_info_graph": {
"description": "Mostra un grafico della composizione del roster per attività e tasso di vittoria (stagione corrente)"
},
"sq_card": {
"description": "Genera una card stagionale per uno squadrone",
"squadron": "Nome breve dello squadrone"
},
"sq_stats": {
"description": "Mostra i punti di uno squadrone nel tempo"
},
"loss_calculator": {
"description": "Calcola la perdita di punti se dei giocatori lasciano lo squadrone",
"player1": "Giocatore in uscita",
"player_optional": "Giocatore in uscita (opzionale)"
},
"website": {
"description": "Ottieni un link al sito di SRE Bot"
},
"card": {
"description": "Genera una card stagionale per un giocatore"
},
"player_stats": {
"description": "Vedi statistiche veicolo dettagliate di un giocatore",
"username": "Username WT per la richiesta stats",
"uid": "UID WT per la richiesta stats"
},
"view_player_games": {
"description": "Vedi le ultime 20 partite di un giocatore"
},
"view_match": {
"description": "Vedi scoreboard partita per ID o giocatore",
"match_id": "ID sessione esadecimale della partita",
"player_name": "Giocatore per sfogliare partite recenti"
},
"compare": {
"description": "Confronta statistiche SQB aggregate tra giocatori",
"player1": "Primo username",
"player2": "Secondo username",
"player_optional": "Username aggiuntivo (opzionale)"
},
"leaderboard": {
"description": "Ottieni la classifica globale di SRE Bot"
},
"set_squadron": {
"description": "Imposta il tag squadrone di questo server",
"abbreviated_name": "Nome breve dello squadrone da impostare"
},
"setup": {
"description": "Configura il bot per questo server"
},
"meta_management": {
"description": "Gestisci accesso ai dati meta per questo server"
},
"meta": {
"description": "Cerca il roster meta per nome veicolo",
"vehicle": "Nome veicolo da cercare"
},
"top": {
"description": "Vedi i top 20 squadroni con stats dettagliate"
},
"language": {
"description": "Cambia la lingua del bot."
},
"translate_message": {
"name": "Traduci messaggio"
},
"sq_track": {
"description": "Traccia uno squadrone e confronta dall'ultimo controllo",
"squadron_short_name": "Nome breve dello squadrone da tracciare"
},
"analytics": {
"description": "Vedi analisi SQB avanzate per uno squadrone",
"view": "Vista analisi da mostrare",
"choice_maps": "Win rate mappe",
"choice_comps": "Composizioni team",
"choice_consistency": "Costanza giocatori",
"choice_time": "Ora del giorno",
"choice_matchups": "Storico match-up"
},
"recent": {
"description": "Mostra battaglie recenti di uno squadrone",
"length": "Numero di partite da mostrare"
},
"vs": {
"description": "Record testa a testa tra due squadroni",
"squadron_a": "Primo squadrone",
"squadron_b": "Secondo squadrone"
},
"autolog_management": {
"description": "Gestisci notifiche autolog e diagnostica permessi"
},
"diagnose_perms": {
"description": "Diagnostica permessi autolog per questo canale"
},
"unlock": {
"description": "Sblocca funzionalità Premium per questo server"
},
"credits": {
"description": "Vedi il team accreditato per questo progetto"
},
"schedule": {
"description": "Vedi programma BR della stagione attuale"
},
"news": {
"description": "Vedi ultime news e annunci di SRE Bot"
},
"help": {
"description": "Vedi guida, ToS e link supporto"
},
"donate": {
"description": "Supporta lo sviluppo di SRE Bot"
},
"stack_create": {
"description": "Crea uno stack di giocatori",
"vehicle": "Con quale veicolo inizierai?"
},
"stack_manage": {
"description": "Ripubblica lo stack attivo in questo canale"
},
"bot_status": {
"description": "Visualizza lo stato del bot: ultima partita ricevuta e TTL medio"
}
},
"permission": {
"blacklisted_title": "❌ In blacklist",
"blacklisted_desc": "Non puoi usare questo comando perché sei in blacklist.",
"reason_line": "**Motivo:** {reason}",
"access_denied_title": "⛔ Accesso negato",
"no_permission_desc": "Non hai il permesso di usare questo comando.",
"unexpected_error_title": "❗ Errore, segnalalo...."
},
"weekly_br": {
"title_wildcard": "Report BR Settimanale — {br} BR",
"title_squadron": "Report BR Settimanale — [{tag}] {long} • {br} BR",
"window_label": "Periodo: {start} → {end}",
"wildcard_desc_first": "Top {count} squadroni per ELO • Posizioni {low}{high}",
"wildcard_desc_second": "Top {count} squadroni per ELO • Posizioni {low}{high}",
"squadron_stats_line": "- {games} partite • K/D {kdr} • Vittorie {wr}%",
"top_players_inline_header": "🥇 Migliori giocatori:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}p)",
"top_players_header": "**Top {count} giocatori per ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} partite • K/D {kdr}",
"squadron_header_line": "ELO squadrone: {score} • {games} partite • Vittorie {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO squadrone: troppa poca attività di squadra questa settimana.",
"no_data": "Nessuna partita registrata per [{tag}] in questa rotazione BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Błąd",
"no_data_title": "Brak danych",
"access_denied_title": "Odmowa dostępu",
"access_denied_desc": "Ten serwer został zablokowany.",
"no_players_selected": "Nie wybrano graczy. Wybierz przynajmniej jednego gracza.",
"must_use_in_server": "To polecenie musi być użyte na serwerze.",
"could_not_resolve_channel": "Nie udało się znaleźć wybranego kanału.",
"failed_update_setting": "❌ Nie udało się zaktualizować ustawienia.",
"configuration_not_found": "Nie znaleziono konfiguracji.",
"no_channel_selected": "Nie wybrano kanału.",
"no_selection_received": "Nie otrzymano wyboru.",
"database_error": "❌ Błąd bazy danych: {error}",
"enabled": "Włączone",
"disabled": "Wyłączone",
"not_configured": "Nieskonfigurowane",
"unknown": "Nieznane",
"rating_field": "Ranking",
"battles_field": "Bitwy",
"wins_field": "Zwycięstwa",
"losses_field": "Porażki",
"win_rate_field": "Wskaźnik zwycięstw",
"kills_field": "Zabójstwa",
"deaths_field": "Śmierci",
"kd_field": "K/D",
"members_field": "Członkowie",
"placement_field": "Miejsce",
"points_field": "Punkty",
"ground_kills_field": "Zabójstwa naziemne",
"air_kills_field": "Zabójstwa powietrzne",
"total_kills_field": "Łączne zabójstwa",
"assists_field": "Asysty",
"captures_field": "Przejęcia",
"none_option": "Brak"
},
"buttons": {
"skip": "Pomiń",
"previous": "Poprzedni",
"next": "Następny",
"prev": "Poprz.",
"prev_arrow": "◀ Poprzedni",
"next_arrow": "Następny ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Generuj wykres",
"show_graph": "Pokaż wykres",
"view_player_stats": "📊 Zobacz statystyki graczy",
"compare_nearby": "📈 Porównaj pobliskie dywizjony",
"confirm_swap": "Tak, zmień",
"cancel_swap": "Nie, zostaw starą",
"set_squadron": "Ustaw dywizjon",
"same_as_logs": "Tak samo jak logi",
"require_password": "🔒 Wymagaj Hasła",
"password_required": "🔒 Hasło Wymagane",
"lock_data": "🔐 Powiąż Dane Dywizjonu",
"data_locked": "🔐 Dane Powiązane z Serwerem",
"allow_public": "👥 Zezwól na Publiczne Meta",
"public_enabled": "👥 Publiczne Meta Włączone",
"update_accounts": "📋 Zaktualizuj Konta Meta",
"change_password": "🔑 Zmień Hasło",
"help": "❓ Pomoc",
"add_player": " Dodaj Gracza",
"update_all": "🔄 Zaktualizuj Wszystkich Członków",
"back_to_settings": "⬅ Powrót do Ustawień",
"manage_notifications": "Zarządzaj Powiadomieniami",
"diagnose_permissions": "Diagnozuj Uprawnienia",
"enable": "Włącz",
"disable": "Wyłącz",
"change_channel": "Zmień Kanał",
"view_replay": "Zobacz Powtórkę",
"view_website": "Zobacz na Stronie",
"view_video": "Zobacz Wideo",
"view_log": "Zobacz Log",
"view_chat": "Zobacz Czat",
"subscribe_website": "Subskrybuj przez Stronę",
"yes_disband": "Tak, rozwiąż",
"cancel": "Anuluj",
"transfer_leave": "Przekaż i odejdź",
"accept_selected": "Przyjmij wybranych",
"accept_all": "Przyjmij wszystkich",
"decline_selected": "Odrzuć wybranych",
"back": "Wróć",
"remove_all": "Usuń wszystkich",
"remove_active": "Usuń aktywnych",
"remove_queued": "Usuń oczekujących",
"remove_selected": "Usuń wybranych",
"ping_all": "Pinguj wszystkich",
"ping_active": "Pinguj aktywnych",
"ping_queued": "Pinguj oczekujących",
"ping_selected": "Pinguj wybranych",
"accept_members": "Przyjmij członków",
"remove_members": "Usuń członków",
"ping_members": "Pinguj członków",
"rename_stack": "Zmień nazwę stacka",
"request_to_join": "Poproś o dołączenie",
"leave_withdraw": "Odejdź / Wycofaj się",
"manage_stack": "Zarządzaj stackiem ⚙️",
"disband_stack": "Rozwiąż stack",
"force_disband_create": "Wymuś rozwiązanie i utwórz nowy"
},
"events": {
"guild_join_title": "Dziękuję za dodanie mnie!",
"guild_join_desc": "Uruchom `/setup`, aby skonfigurować bota dla tego serwera."
},
"comp": {
"not_found_title": "Nie znaleziono składów",
"not_found_desc": "Brak danych dla **{squadron}**, spróbuj ponownie później.",
"error_loading_title": "Błąd ładowania składów",
"error_loading_desc": "Nie udało się załadować danych składów: {error}",
"title": "Składy dla {squadron}",
"desc": "Składy widziane w ciągu ostatnich {minutes} minut",
"no_recent_title": "Brak ostatnich składów",
"no_recent_desc": "Brak składów w ciągu ostatnich {minutes} minut.",
"comp_title": "SKŁAD {index}",
"last_seen_label": "**Ostatnio widziany**: {timestamp}{warning}",
"comp_label": "**Skład**: {notation}",
"no_players_recorded": "Brak zarejestrowanych graczy.",
"limit_reached_title": "Limit składów osiągnięty",
"limit_reached_desc": "Ten serwer wykorzystał wszystkie {limit} wyszukiwań składów w tym slocie czasowym. Subskrybuj (za pomocą /unlock) aby uzyskać nieograniczony dostęp lub poczekaj na następny slot.",
"remaining_footer": "{remaining}/{limit} wyszukiwań składów pozostało w tym slocie czasowym"
},
"quick_log": {
"invalid_type": "Typ można ustawić tylko na Logi, Punkty, Tabela liderów, Tygodniowy BR lub Oba.",
"squadron_required": "Musisz podać nazwę dywizjonu dla alarmów Logów, Punktów lub Obu.",
"wildcard_logs_only": "Tylko Logi można ustawić na dywizjon z symbolem wieloznacznym.",
"squadron_not_resolved": "Nie udało się rozwiązać dywizjonu `{squadron}`.",
"save_failed": "Nie udało się zapisać preferencji. Spróbuj ponownie później.",
"premium_warning": "\n\n> ⚠️ **Logi gry wymagają Premium.** Uruchom `/unlock`, aby subskrybować ($2.99/mies.) — logi nie będą wysyłane do tego czasu.",
"leaderboard_set": "Alarm globalnej tabeli wyników ustawiony na tym kanale.",
"both_set": "Alarmy Logów i Punktów dla {squadron} ustawione na tym kanale.{premium_note}",
"alarm_set": "Alarm {alarm_type} dla {squadron} ustawiony na tym kanale.{premium_note}",
"weekly_br_wildcard_set": "Tygodniowy raport BR (top 20 szwadronów) skonfigurowany dla tego kanału. Wysyłany na koniec każdej rotacji BR.",
"weekly_br_squadron_set": "Tygodniowy raport BR dla {squadron} (top 15 graczy) skonfigurowany dla tego kanału. Wysyłany na koniec każdej rotacji BR."
},
"diagnostics": {
"title": "Diagnostyka autologu",
"channel_permissions_header": "**Uprawnienia kanału** (<#{channel_id}>)",
"perms_needed": " ^ Autologowanie wymaga wszystkich powyższych uprawnień do wysyłania tablic wyników.",
"server_squadron_header": "**Dywizjon serwera** (`/set-squadron`)",
"server_squadron_short": " Skrót: `{short}`",
"server_squadron_long": " Pełna nazwa: `{long}`",
"server_squadron_not_set": " Nie ustawiony (kolor paska tablicy wyników będzie wyświetlany jako 'not_set')",
"autolog_prefs_header": "**Preferencje autologu** (`/quick-log`)",
"autolog_none_configured": " ❌ BRAK konfiguracji - autologowanie NIE będzie wysyłać niczego na ten serwer.",
"autolog_setup_hint": " Użyj `/quick-log <squadron_short> Logs` na docelowym kanale, aby skonfigurować.",
"autolog_no_logs_channels": " ❌ Brak skonfigurowanych kanałów Logów. Znaleziono tylko Punkty/Tabelę wyników.",
"autolog_enable_hint": " Użyj `/quick-log <squadron_short> Logs`, aby włączyć autologowanie.",
"selected_channel_tag": " **(wybrany kanał)**",
"missing_send_attach": " (brak uprawnień do wysyłania/załączania)",
"channel_not_found": " (kanał nie znaleziony)",
"invalid_channel_id": " (nieprawidłowe ID kanału)",
"premium_status_header": "**Status Premium** (`/unlock`)",
"premium_active": " ✅ Ten serwer ma aktywną subskrypcję Premium.",
"premium_not_subscribed": " ❌ Ten serwer **nie** posiada subskrypcji Premium.",
"premium_autolog_required": " Autologowanie wymaga Premium. Użyj `/unlock`, aby subskrybować.",
"premium_not_subscribed_free": " ⚪ Brak subskrypcji — użyj `/unlock`, aby subskrybować ($2.99/mies.).",
"premium_free_note": " *(Autologi są teraz darmowe dla wszystkich serwerów.)*"
},
"sq_info": {
"title": "Informacje o dywizjonie: {squadron}",
"placement_field": "Miejsce",
"total_points_field": "Łączne punkty",
"total_members_field": "Łączna liczba członków",
"members_field": "Członkowie",
"fetch_failed": "Nie udało się pobrać informacji o dywizjonie."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Sezon {season})",
"embed_title": "{squadron} — Skład dywizjonu",
"embed_desc": "Sezon **{season}** · Mediana meczów: **{median}** · Trzon: **{core}** · Aktywni: **{active}** · Słabi: **{weak}**\nSłupki posortowane wg meczów malejąco; wysokość = wsp. wygranych. Trzon = top 30 % WR i mecze ≥ mediana. Aktywni = top 3045 % WR i mecze ≈ mediana. Słabi = pozostali.",
"core_threshold_line": "TRZON ≥ {wr} %",
"weak_threshold_line": "SŁABI < {wr} %",
"y_label": "Współczynnik wygranych",
"core_header": "TRZON — {count} · WR {avg}%",
"active_header": "AKTYWNI — {count} · WR {avg}%",
"weak_header": "SŁABI — {count} · WR {avg}%",
"no_active_season": "Nie znaleziono aktywnego sezonu. Spróbuj ponownie po rozpoczęciu następnego.",
"no_members": "Nie znaleziono aktualnych członków dla {squadron}."
},
"recap_card": {
"unknown_season": "Nieznany sezon: `{season}`.",
"no_clan_id": "Nie udało się ustalić ID dywizjonu `{squadron}`.",
"render_failed": "Nie udało się wygenerować karty podsumowania sezonu. Spróbuj ponownie później."
},
"sq_stats": {
"no_data_title": "Brak danych",
"no_data_desc": "Nie znaleziono danych historycznych dla dywizjonu: {squadron}",
"title": "{squadron} // DYWIZJON",
"desc": "Trend łącznego wyniku (ostatnie {count} punktów danych)",
"previous_score_field": "Poprzedni wynik",
"current_score_field": "Bieżący wynik",
"change_field": "Zmiana",
"player_title": "{squadron} // GRACZE",
"player_desc": "Trendy punktowe poszczególnych graczy",
"comparison_title": "{squadron} // PORÓWNANIE TABELI WYNIKÓW",
"comparison_desc": "Porównanie z dywizjonami z rankingu {range}",
"current_position_field": "Aktualna pozycja",
"squadrons_shown_field": "Wyświetlone dywizjony",
"squadron_not_found_error": "Nie znaleziono dywizjonu w tabeli wyników",
"no_nearby_error": "Nie znaleziono pobliskich dywizjonów",
"no_historical_error": "Nie znaleziono danych historycznych dla pobliskich dywizjonów",
"comparison_chart_failed": "Nie udało się wygenerować wykresu porównawczego",
"select_players_placeholder": "Wybierz graczy (Strona {page})"
},
"loss_calc": {
"title": "Utrata punktów — {squadron}",
"players_leaving_field": "Opuszczający gracze",
"share_of_total_field": "% udział w całości",
"points_lost_real_field": "Utracone punkty (rzeczywiste)",
"points_lost_raw_field": "Utracone punkty (surowe)",
"squadron_rating_field": "Ranking dywizjonu",
"squadron_position_field": "Pozycja dywizjonu",
"positions_lost_field": "Utracone pozycje",
"not_found_footer": "Nie znaleziono w dywizjonie: {players}",
"fetch_failed": "Nie udało się pobrać danych dywizjonu: {error}",
"no_point_data": "Brak danych punktowych dla tego dywizjonu.",
"no_matching_players": "Nie znaleziono pasujących graczy w **{squadron}**."
},
"player": {
"select_player_placeholder": "Wybierz gracza",
"no_stats_found": "❌ Nie znaleziono statystyk dla UID: {uid}",
"no_vehicle_stats": "❌ Nie znaleziono statystyk pojazdów dla tego gracza.",
"vehicles_found": "Znaleziono **{count}** pojazdów dla **{nick}**\nWybierz pojazd, aby zobaczyć szczegółowe statystyki:",
"vehicle_select_placeholder": "Wybierz pojazd (Strona {page}/{total})",
"combat_stats_header": "**__STATYSTYKI BOJOWE__**",
"ground_kills_label": "**Zabójstwa naziemne:** {value}",
"air_kills_label": "**Zabójstwa powietrzne:** {value}",
"total_kills_label": "**Łączne zabójstwa:** {value}",
"assists_label": "**Asysty:** {value}",
"deaths_label": "**Śmierci:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Przejęcia:** {value}",
"battle_record_header": "**__WYNIKI BITEW__**",
"total_battles_label": "**Łączne bitwy:** {value}",
"wins_label": "**Zwycięstwa:** {value}",
"losses_label": "**Porażki:** {value}",
"win_rate_label": "**Wskaźnik zwycięstw:** {value}%",
"stats_desc": "Statystyki dla **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Nie znaleziono gracza",
"not_found_desc": "Nie znaleziono historii gier dla `{player}`.",
"no_players_found": "Nie znaleziono graczy pasujących do **{username}**\nSpróbuj użyć `/website`, aby wyszukać na stronie.",
"multiple_matches": "Znaleziono wiele dopasowań, wybierz właściwe poniżej:",
"must_provide_input": "Musisz podać przynajmniej UID lub nazwę użytkownika."
},
"player_games": {
"no_recent_title": "Brak ostatnich gier",
"no_recent_desc": "Nie znaleziono gier dla **{player}** w ciągu ostatnich 8 godzin.",
"squadron_label": "**Dywizjon:** {squadron}",
"record_label": "**Z:** {wins} **P:** {losses} **WS:** {wr}%",
"comps_played_header": "\n\n**Grane składy**"
},
"match": {
"missing_input_title": "Brakujące dane",
"missing_input_desc": "Podaj `match_id` lub `player_name`.",
"not_found_title": "Nie znaleziono meczu",
"not_found_desc": "Nie udało się znaleźć meczu o ID `{match_id}`.",
"invalid_data_title": "Nieprawidłowe dane meczu",
"invalid_data_desc": "Nie udało się przetworzyć danych powtórki.",
"scoreboard_error_title": "Błąd tablicy wyników",
"scoreboard_error_desc": "Nie udało się wygenerować obrazu tablicy wyników.",
"no_games_title": "Nie znaleziono gier",
"no_games_desc": "Nie znaleziono historii gier dla **{player}**.",
"recent_matches_title": "Ostatnie mecze dla {player}",
"recent_matches_desc": "Wyświetlanie do {count} ostatnich gier. Wybierz jedną, aby zobaczyć pełną tablicę wyników.",
"select_match_placeholder": "Wybierz mecz do wyświetlenia..."
},
"compare": {
"no_players_found": "Nie znaleziono graczy pasujących do **{name}**.",
"multiple_matches": "Wiele dopasowań dla **{name}**: {matches}\nProszę użyć bardziej szczegółowej nazwy (sugestie autouzupełniania są dokładne).",
"could_not_resolve": "Nie udało się rozwiązać graczy.",
"could_not_fetch": "❌ Nie udało się pobrać statystyk dla **{name}**.",
"no_graph_data": "Brak danych za ostatnie 90 dni.",
"no_squadron_points_data": "Brak danych punktowych dywizjonu dla {names} (gracz nie znaleziony w śledzonej historii dywizjonu).",
"graph_title": "Punkty gracza — ostatnie 90 dni",
"battles_label": "Bitwy",
"wins_label": "Zwycięstwa",
"losses_label": "Porażki",
"win_rate_label": "Wskaźnik zwycięstw",
"ground_kills_label": "Zabójstwa naziemne",
"air_kills_label": "Zabójstwa powietrzne",
"total_kills_label": "Łączne zabójstwa",
"assists_label": "Asysty",
"deaths_label": "Śmierci",
"kd_label": "K/D",
"captures_label": "Przejęcia"
},
"squadron": {
"not_found_desc": "Nie znaleziono dywizjonu `{squadron}`.",
"set_title": "✅ Dywizjon ustawiony",
"set_desc": "Dywizjon **{squadron}** został ustawiony dla tego serwera.",
"short_name_field": "Skrócona nazwa",
"long_name_field": "Pełna nazwa",
"swap_title": "✅ Dywizjon zmieniony",
"swap_desc": "Zastąpiono **{old}** przez **{new}** dla tego serwera.",
"already_set_title": "⚠️ Dywizjon już ustawiony",
"already_set_desc": "Ten serwer jest aktualnie ustawiony na **{old}**.\nZmienić na **{new}**?",
"swap_cancelled": "❌ Zmiana dywizjonu anulowana."
},
"setup": {
"step1_title": "Konfiguracja serwera — Krok 1 z 3",
"step1_desc": "Ten kreator przeprowadzi Cię przez konfigurację bota dla Twojego serwera.\n\n**Krok 1** — Ustaw dywizjon\n**Krok 2** — Wybierz kanał logów\n**Krok 3** — Wybierz kanał punktów\n",
"step1_current_sq": "\nAktualnie skonfigurowany dywizjon: **[{short}] {long}**",
"step2_title": "Konfiguracja serwera — Krok 2 z 3",
"step2_desc": "Dywizjon ustawiony na **[{short}] {long}**.\n\nGdzie powinny być wysyłane **logi bitew**?\nWybierz kanał tekstowy poniżej lub pomiń ten krok.",
"step3_title": "Konfiguracja serwera — Krok 3 z 3",
"step3_desc": "Gdzie powinny być wysyłane **powiadomienia o punktach**?\nWybierz kanał tekstowy poniżej lub pomiń ten krok.",
"step3_same_as_logs": "\n\nMożesz też kliknąć \"Tak samo jak Logi\", aby ponownie użyć kanału logów.",
"summary_title": "Konfiguracja zakończona",
"summary_desc": "Możesz użyć `/autolog-management`, aby zmienić te ustawienia później.",
"squadron_field": "Dywizjon",
"logs_channel_field": "Kanał logów",
"points_channel_field": "Kanał punktów",
"premium_required_field": "⚠️ Logi gry wymagają Premium",
"premium_required_value": "Automatyczne tablice wyników nie będą wysyłane, dopóki ten serwer nie będzie miał aktywnej subskrypcji. Uruchom `/unlock`, aby subskrybować ($2.99/mies.).",
"modal_title": "Ustaw dywizjon",
"modal_label": "Skrócona nazwa dywizjonu",
"modal_placeholder": "np. AXYS",
"squadron_not_found": "Nie znaleziono dywizjonu `{squadron}`. Spróbuj ponownie.",
"logs_channel_placeholder": "Wybierz kanał logów...",
"points_channel_placeholder": "Wybierz kanał punktów..."
},
"meta_management": {
"squadron_not_found_title": "❌ Nie znaleziono dywizjonu",
"squadron_not_found_desc": "Nie udało się znaleźć ID klanu dla dywizjonu: **{squadron}**",
"access_denied_title": "❌ Odmowa dostępu",
"access_denied_desc": "Nieprawidłowe hasło. Metadane tego dywizjonu są chronione.",
"data_locked_title": "🔐 Dane dywizjonu powiązane",
"data_locked_desc": "**{squadron}** ma włączone powiązanie danych i nie może zostać przeniesiony na inny serwer.\n\nWłaściciel dywizjonu musi wyłączyć **Powiązanie Danych Dywizjonu** przed jego przeniesieniem.",
"error_retrieving_settings": "❌ Błąd pobierania ustawień serwera po przeniesieniu. Spróbuj ponownie.",
"error_retrieving_settings_retry": "❌ Błąd pobierania ustawień serwera. Spróbuj uruchomić polecenie ponownie.",
"authenticated_title": "✅ Uwierzytelniono",
"authenticated_desc": "Hasło zweryfikowane. Zarządzanie ustawieniami dla **{squadron}**.",
"claimed_title": "✅ Dywizjon Przejęty",
"claimed_desc": "**{squadron}** został pomyślnie przejęty dla tego serwera!",
"password_requirement_field": "🔒 Wymaganie Hasła",
"data_lock_field": "🔐 Powiązanie Danych Dywizjonu",
"public_meta_field": "👥 Publiczny Dostęp do Meta",
"access_password_field": "🔑 Hasło Dostępu",
"enabled_value": "✅ Włączone",
"disabled_value": "❌ Wyłączone",
"settings_title": "🔐 Ustawienia Zarządzania Meta",
"settings_desc": "**Dywizjon:** {squadron}\n**ID Klanu:** {clan_id}",
"first_time_title": "🔐 Zarządzanie Meta - Pierwsze Uruchomienie",
"first_time_owner_desc": "**Dywizjon:** {squadron}\n**ID Klanu:** {clan_id}\n\n🔑 Twoje hasło dostępu zostało wygenerowane. **Zapisz to hasło** — będzie Ci potrzebne do uwierzytelniania dostępu do metadanych w przyszłości.\n\n**Hasło:** `{password}`",
"first_time_non_owner_desc": "**Dywizjon:** {squadron}\n**ID Klanu:** {clan_id}\n\nDywizjon został skonfigurowany. Zapytaj właściciela serwera o hasło dostępu.",
"settings_field": "Ustawienia",
"settings_hint": "Użyj przycisków poniżej, aby skonfigurować ustawienia dostępu.",
"password_toggled": "✅ Wymaganie hasła: **{state}**",
"lock_toggled": "✅ Powiązanie danych dywizjonu: **{state}**",
"public_meta_toggled": "✅ Publiczny dostęp do meta: **{state}**\n{detail}",
"public_meta_enabled_detail": "Osoby niebędące administratorami mogą teraz używać polecenia `/meta`.",
"public_meta_disabled_detail": "Tylko administratorzy mogą używać polecenia `/meta`.",
"owner_only_password": "❌ Tylko właściciel serwera może zmienić hasło dywizjonu.",
"help_title": "📖 Pomoc Zarządzania Meta",
"help_desc": "Wyjaśnienie każdego ustawienia i funkcji:",
"help_password_field": "🔑 Hasło Dostępu",
"help_password_value": "Hasło dostępu Twojego dywizjonu. Tylko **właściciel serwera** może zobaczyć hasło w panelu ustawień. Każdy, kto ma hasło, może przejąć metadane dywizjonu na swoim serwerze, więc przechowuj je bezpiecznie.",
"help_require_field": "🔒 Wymagaj Hasła",
"help_require_value": "Po włączeniu, nawet administratorzy na tym serwerze muszą wprowadzić hasło dywizjonu, aby uzyskać dostęp do `/meta-management`. Dodaje dodatkową warstwę zabezpieczeń przed przypadkowymi zmianami.",
"help_lock_field": "🔐 Powiąż Dane Dywizjonu",
"help_lock_value": "Po włączeniu, wiąże dane dywizjonu z tym serwerem, uniemożliwiając przeniesienie nawet przy poprawnym haśle. Musi być wyłączone przed przeniesieniem dywizjonu.",
"help_public_field": "👥 Zezwól na Publiczne Meta",
"help_public_value": "Po włączeniu, pozwala osobom niebędącym administratorami używać polecenia `/meta` do wyszukiwania pojazdów dywizjonu. Po wyłączeniu, tylko administratorzy serwera mogą używać `/meta`.",
"help_accounts_field": "📋 Zaktualizuj Konta Meta",
"help_accounts_value": "Otwiera menedżera składu graczy, gdzie możesz dodawać lub usuwać graczy z listy meta dywizjonu. Użyj **Zaktualizuj Wszystkich Członków**, aby zsynchronizować cały dywizjon naraz.",
"help_change_pw_field": "🔑 Zmień Hasło",
"help_change_pw_value": "**Tylko właściciel serwera.** Zmień hasło dostępu dywizjonu i ustaw opcjonalną podpowiedź. Podpowiedź jest wyświetlana w oknie hasła, aby pomóc je zapamiętać.",
"password_modal_title": "Hasło Dostępu Dywizjonu",
"password_modal_label": "Wprowadź Hasło Dywizjonu",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Zmień Hasło Dywizjonu",
"current_password_label": "Bieżące Hasło",
"current_password_placeholder": "Wprowadź swoje bieżące hasło",
"new_password_label": "Nowe Hasło",
"new_password_placeholder": "Wprowadź nowe hasło",
"confirm_password_label": "Potwierdź Nowe Hasło",
"confirm_password_placeholder": "Wprowadź ponownie nowe hasło",
"hint_label": "Podpowiedź do Hasła (Opcjonalne)",
"hint_placeholder": "Podpowiedź pomagająca zapamiętać hasło",
"pw_incorrect": "❌ Bieżące hasło jest nieprawidłowe.",
"pw_mismatch": "❌ Nowe hasła nie są zgodne. Spróbuj ponownie.",
"pw_empty": "❌ Nowe hasło nie może być puste.",
"pw_changed": "✅ Hasło zaktualizowane pomyślnie dla **{squadron}**.\n**Nowe Hasło:** `{password}`",
"pw_changed_hint": "\n**Podpowiedź:** {hint}",
"player_add_modal_title": "Dodaj Gracza do Listy Meta",
"player_add_label": "UID lub Pseudonim Gracza",
"player_add_placeholder": "Wprowadź UID gracza (np. 12345678) lub pseudonim",
"player_not_found": "❌ Gracz `{player}` nie znaleziony w bazie danych Players_Global.\n",
"roster_title": "📋 Zarządzanie Listą Meta - {squadron}",
"roster_desc": "**ID Klanu Dywizjonu:** {clan_id}\n**Łączna Liczba Graczy:** {count}",
"roster_page_field": "Gracze (Strona {page}/{total})",
"no_players_field": "Brak Graczy",
"no_players_hint": "Nie dodano jeszcze żadnych graczy do listy meta. Kliknij **Dodaj Gracza**, aby rozpocząć.",
"remove_player_placeholder": "Wybierz gracza do usunięcia...",
"fetch_members_failed": "❌ Nie udało się pobrać członków dywizjonu: {error}",
"no_members_found": "❌ Nie znaleziono członków w dywizjonie lub wywołanie API nie powiodło się.",
"roster_synced": "✅ Lista zsynchronizowana z dywizjonem.",
"roster_added": "**+{count}** dodanych",
"roster_removed": "**-{count}** usuniętych (opuścili dywizjon)",
"roster_up_to_date": "**{count}** już aktualnych",
"refreshing_vehicles": "Odświeżanie danych pojazdów w tle..."
},
"meta": {
"not_configured": "❌ Metadane nie skonfigurowane dla tego serwera. Najpierw uruchom `/meta-management`.",
"no_permission": "❌ Potrzebujesz uprawnień administratora, aby użyć tego polecenia.\nAdministratorzy mogą włączyć dostęp publiczny przez `/meta-management`.",
"no_results": "❌ Żaden gracz z listy dywizjonu nie posiada **{vehicle}**.",
"no_results_admin_hint": "\n*Spodziewasz się, że ktoś powinien to mieć? Kliknij przycisk aktualizacji członków w `/meta-management` i sprawdź ponownie.*",
"search_title": "🔍 Wyniki Wyszukiwania - {vehicle}",
"matches_found": "**Znalezione Dopasowania:** {count} gracz(y)",
"spawns_label": "Spawny",
"deaths_label": "Śmierci",
"gk_label": "ZN",
"ak_label": "ZP",
"points_label": "Punkty",
"kdr_label": "KDR",
"games_label": "Gry",
"no_points": "—"
},
"top": {
"title": "**Top 20 Dywizjonów**",
"rating_label": "**Ranking:** {value}",
"air_kills_label": "**Zabójstwa powietrzne:** {value}",
"ground_kills_label": "**Zabójstwa naziemne:** {value}",
"deaths_label": "**Śmierci:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Wskaźnik zwycięstw:** {value}",
"playtime_label": "**Czas Gry:** {value}",
"fetch_failed": "Nie udało się pobrać danych dywizjonu."
},
"analytics": {
"no_data_title": "Brak danych",
"no_matches_desc": "Nie znaleziono meczów.",
"no_comp_desc": "Nie znaleziono danych składu.",
"no_consistency_desc": "Niewystarczające dane graczy (minimum 50 meczów).",
"no_time_desc": "Nie znaleziono danych czasowych.",
"unknown_view": "Nieznany widok.",
"map_title": "Wskaźniki Zwycięstw na Mapach: {squadron}",
"comp_title": "Składy Drużyny: {squadron}",
"consistency_title": "Stałość graczy: {squadron}",
"consistency_desc": "Posortowane według wskaźnika K/D",
"time_title": "Wyniki w Zależności od Pory Dnia: {squadron}",
"eu_timeslot": "\n**Slot EU**",
"na_timeslot": "\n**Slot NA**",
"off_peak": "\n**Poza Szczytem**",
"matchups_title": "📜 {squadron} — Historia Starć",
"matchups_won_field": "🏆 Najwięcej Wygranych Przeciw",
"matchups_lost_field": "💀 Najwięcej Przegranych Z",
"no_matchups_desc": "Brak zarejestrowanych meczów przeciwko innym klanom."
},
"recent": {
"title": "Ostatnie Mecze: {squadron}",
"no_matches_desc": "Nie znaleziono meczów dla tego dywizjonu."
},
"h2h": {
"two_required_title": "Wymagane Dwa Dywizjony",
"two_required_desc": "Podaj przynajmniej jeden dywizjon lub użyj `/set-squadron` i podaj przeciwnika.",
"provide_a_desc": "Podaj `squadron_a` lub najpierw użyj `/set-squadron`.",
"provide_b_desc": "Podaj `squadron_b` lub najpierw użyj `/set-squadron`.",
"squadron_not_found_title": "Nie Znaleziono Dywizjonu",
"same_squadron_title": "Ten Sam Dywizjon",
"same_squadron_desc": "Nie możesz sprawdzić bezpośredniej konfrontacji z samym sobą.",
"record_desc": "**Wynik:** {a_wins}Z - {b_wins}P ({total} gier)",
"no_matches_desc": "Brak zarejestrowanych meczów między **{a}** a **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Aktywne — autologowanie jest włączone dla tego serwera.",
"premium_not_subscribed_line": "❌ **Premium:** Brak subskrypcji — użyj `/unlock`, aby włączyć autologowanie.",
"premium_free_line": "⚪ **Premium:** Brak subskrypcji — użyj `/unlock`, aby subskrybować ($2.99/mies.). *(Autologi są teraz darmowe dla wszystkich serwerów.)*",
"what_to_do": "\n\nCo chcesz zrobić?",
"select_notif_type": "Wybierz typ powiadomienia do zarządzania:",
"select_notif_placeholder": "Wybierz typ powiadomienia",
"logs_option": "Logi",
"logs_option_desc": "Zarządzaj powiadomieniami Logów",
"points_option": "Punkty",
"points_option_desc": "Zarządzaj powiadomieniami Punktów",
"leaderboard_option": "Tabela Wyników",
"leaderboard_option_desc": "Zarządzaj powiadomieniami Tabeli Wyników",
"selected_type": "Wybrano **{type}**. Teraz wybierz dywizjon do zarządzania:",
"select_squadron_placeholder": "Wybierz dywizjon",
"select_squadron_page_placeholder": "Wybierz dywizjon (Strona {page})",
"no_squadrons_available": "Brak dostępnych dywizjonów dla tego typu powiadomień.",
"managing_global": "Zarządzanie **{type}** (globalne) na kanale **{channel}**.",
"managing_squadron": "Zarządzanie **{type}** dla dywizjonu **{squadron}** na kanale **{channel}**.",
"select_channel": "Wybierz nowy kanał:",
"select_channel_placeholder": "Wybierz kanał",
"select_channel_page_placeholder": "Wybierz kanał (Strona {page})",
"global_toggled": "{type} (globalne) jest teraz {state}.",
"squadron_toggled": "{type} dla **{squadron}** jest teraz {state}.",
"channel_updated_global": "Zaktualizowano {type} (globalne) na {channel}",
"channel_updated_squadron": "Zaktualizowano {type} dla **{squadron}** na {channel}",
"diagnose_channel_placeholder": "Wybierz kanał do diagnostyki...",
"select_channel_diagnose": "Wybierz kanał do diagnostyki:",
"game_not_logged_title": "Gra niezapisana",
"game_not_logged_desc": "Użyj `/unlock`, aby wykupić plan **Standard** (lub wyższy) i otrzymywać automatyczne tablice wyników.",
"server_not_upgraded_title": "⚠️ Serwer Niezaktualizowany",
"server_not_upgraded_autolog_desc": "Ten serwer nie ma aktywnej subskrypcji Premium.\n\n**Automatyczne tablice wyników gier przestaną być wysyłane na serwery bez aktualizacji po <t:{deadline}:D>.**\n\nUżyj `/unlock`, aby subskrybować i nadal otrzymywać automatyczne logi gier.",
"replay_not_available": "Dane powtórki nie są jeszcze dostępne — poczekaj chwilę i spróbuj ponownie!",
"too_many_videos": "Zbyt wiele filmów jest teraz renderowanych — spróbuj ponownie za chwilę.",
"video_gen_failed": "Błąd generowania wideo: `{error}`",
"video_missing": "Nie udało się wygenerować wideo powtórki - brak pliku wyjściowego lub jest on pusty.",
"video_too_large": "Wideo powtórki jest zbyt duże do przesłania ({file_mb:.1f} MB). Limit serwera wynosi {limit_mb:.0f} MB.",
"video_web_fallback": "Możesz też obejrzeć ten mecz na {url}",
"video_upload_failed": "Wideo zbyt duże do przesłania — obejrzyj je na stronie:\n{url}",
"video_unexpected_error": "Nieoczekiwany błąd podczas generowania wideo powtórki: `{error}`",
"replay_not_found": "Nie znaleziono danych powtórki dla sesji `{session_id}` na dysku.",
"chat_log_title": "**Log Czatu dla Gry [{session_id}]({url})**",
"chat_log_part_title": "**Log Czatu dla Gry [{session_id}]({url}) (Część {part}/{total})**",
"chat_log_part_only": "**Log Czatu (Część {part}/{total})**",
"no_chat_log": "Nie znaleziono logu czatu dla sesji `{session_id}`.",
"chat_log_error": "Nieoczekiwany błąd podczas ładowania logu czatu: `{error}`",
"battle_log_title": "**Log Bitwy dla Gry [{session_id}]({url})**",
"battle_log_part_title": "**Log Bitwy dla Gry [{session_id}]({url}) (Część {part}/{total})**",
"battle_log_part_only": "**Log Bitwy (Część {part}/{total})**",
"no_battle_log": "Nie znaleziono zdarzeń bojowych dla sesji `{session_id}`.",
"battle_log_error": "Nieoczekiwany błąd podczas ładowania logu bitwy: `{error}`",
"points_update_title": "**{squadron} {region} Aktualizacja Punktów**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Zmiany Graczy:**",
"points_table_header": "Nazwa Zmiana Teraz\n",
"wl_line": "\n**{squadron}** osiągnął **{wins}Z-{losses}P** w tej sesji",
"placement_rose": "\n**{squadron}** awansował na **{new_place}** z **{old_place}**",
"placement_fell": "\n**{squadron}** spadł na **{new_place}** z **{old_place}**",
"points_not_logged_title": "Punkty niezapisane",
"points_not_logged_desc": "Użyj `/unlock`, aby wykupić plan **Standard** (lub wyższy) i otrzymywać automatyczne aktualizacje punktów.",
"server_not_upgraded_points_desc": "Ten serwer nie ma aktywnej subskrypcji Premium.\n\n**Automatyczne aktualizacje przestaną być wysyłane na serwery bez aktualizacji po <t:{deadline}:D>.**\n\nUżyj `/unlock`, aby subskrybować i nadal otrzymywać automatyczne aktualizacje.",
"leave_title": "⚠️ Gracz Opuścił {squadron}",
"leave_desc": "**{nick}** ({uid}) opuścił dywizjon.\n\nOstatnie zarejestrowane punkty: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Klan powyżej limitu Twojego planu",
"over_cap_desc": "Twój serwer ma plan **{tier}**, który pozwala na **{cap} {notif}** klanów. Klan **{squadron}** jest powyżej limitu i nie jest rejestrowany. Zmień plan, aby przywrócić.",
"over_cap_footer": "Zmień plan na srebot-meow.ing/premium lub /unlock",
"wildcard_blocked_title": "Wildcard wymaga wyższego planu",
"wildcard_blocked_desc": "Wpisy wildcard (*, all, everything) są dostępne tylko w planach Pro i Max. Twój serwer ma **{tier}** dla {notif}. Zaktualizuj, aby włączyć.",
"cap_header": "{used}/{cap} {notif} włączonych — plan {tier}"
},
"track": {
"squadron_not_found": "Nie znaleziono dywizjonu.",
"fetch_failed": "Nie udało się pobrać informacji o dywizjonie."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Odblokuj funkcje premium dla tego serwera.**\n\nPremium zawiera:\n> • Automatyczne posty z tablicami wyników\n> • Logi czatu i bitwy\n> • Wyszukiwanie powtórek\n> • Nieograniczone wyszukiwania /comp\n> • Priorytetowe wsparcie\n\n**$2.99 / miesiąc · na serwer · anuluj w dowolnym momencie**\n\n⚠️ Płatności Discord są dostępne tylko w wybranych krajach. Jeśli przycisk poniżej pokazuje **\"Produkt Niedostępny\"**, może to być spowodowane nieobsługiwanym krajem lub użyciem **urządzenia mobilnego**. Zamiast tego użyj przycisku **Subskrybuj przez Stronę**.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Ten serwer jest już subskrybowany!**",
"manage_discord_field": "Zarządzaj Subskrypcją",
"manage_discord_value": "Twoja subskrypcja jest przez **Discord**.\nAby anulować, przejdź do **Ustawień Użytkownika → Subskrypcje** w Discord.",
"manage_website_field": "Zarządzaj Subskrypcją",
"manage_website_value": "Twoja subskrypcja jest przez **stronę**.\nZarządzaj nią na [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Już Wkrótce",
"coming_soon_value": "Subskrypcje Premium nie są jeszcze dostępne. Sprawdź wkrótce!",
"current_tier": "Masz plan **{tier}**.",
"upgrade_to": "Przejdź na {tier}",
"upgrade_to_value": "Więcej klanów i funkcji po przejściu na **{tier}**."
},
"language": {
"prompt": "Proszę wybrać język serwera:",
"select_placeholder": "Wybierz język serwera",
"language_set": "Język ustawiony na {language}.",
"translate_prompt": "Wybierz język docelowy poniżej 👇",
"translate_placeholder": "Wybierz język docelowy…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Tłumaczenie niedostępne (DeepL nieskonfigurowany)",
"translation_failed": "Tłumaczenie nie powiodło się"
},
"misc": {
"credits_title": "Twórcy",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Główny Programista, Menedżer Bota, Menedżer Społeczności\n> **Z3R0** - Programista, Programista Optymalizacji, Inżynier Baz Danych\n> **Clippii (Heidi)i (Heidi)** - Programista, Programista Strony, Menedżer Społeczności\n> **LivingTheDagor** - Programista, Programista Parsera, Konsultant\n> **Lux_** - Inżynier API, Programista Spectra\n> **Konigallerwaffen** - Konsultant ds. opinii i funkcji\n> **Žralok Tonda** - Tłumacz Czeski\n> **Styevy**, **Lopais** - Tłumacze Niemieccy\n> **Susogus**, **playforfun698** - Tłumacze Polscy\n> **Bobr** - Tłumacz Rosyjski\n\n\n[Chcesz do nas dołączyć?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "HARMONOGRAM SEZONU",
"schedule_not_found_title": "Nie Znaleziono Harmonogramu",
"schedule_not_found_desc": "Dane harmonogramu nie są jeszcze dostępne.",
"news_no_news_title": "Brak Wiadomości",
"news_no_news_desc": "Nie ma teraz żadnych ogłoszeń. Sprawdź ponownie później!",
"news_footer": "Dziękujemy za wsparcie! ᕙᘘᗢ",
"help_title": "Przewodnik po Bocie",
"donate_title": "Wesprzyj SRE Bot",
"donate_desc": "Jeśli lubisz używać SRE Bot i chcesz wesprzeć jego rozwój, rozważ kupienie mi kawy!\n\n**[Przekaż darowiznę na Ko-fi](https://ko-fi.com/notsotoothless)**\n\nKażda wpłata pomaga utrzymać bota i wspiera nowe funkcje. Dziękuję!",
"status_title": "Status bota",
"status_last_received": "Ostatnia odebrana gra",
"status_avg_ttl": "Średni TTL (ostatnie 30)",
"status_no_data": "Brak danych",
"status_gaijin_slow": "⚠️ Serwery Gaijin są wolne",
"help_commands_header": "**Przegląd komend**",
"help_links": "Szczegóły znajdziesz w dokumentacji [tutaj]({docs}) albo na supporcie [tutaj]({support}).",
"help_terms": "[Regulamin]({terms}) • [Polityka prywatności]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Tabela Wyników Dywizjonów",
"top15_desc": "Top 15 dywizjonów ze statystykami, wysyłane 35 minut po zamknięciu slotu czasowego.\nWysłana <t:{timestamp}:R>.",
"top30_desc": "Dywizjony 16-30 ze statystykami.",
"not_logged_title": "Tabela Wyników Niezalogowana",
"not_logged_desc": "Użyj `/unlock`, aby wykupić plan **Standard** (lub wyższy) i otrzymywać automatyczne aktualizacje tabeli wyników.",
"server_not_upgraded_title": "⚠️ Serwer Niezaktualizowany",
"server_not_upgraded_desc": "Ten serwer nie ma aktywnej subskrypcji Premium.\n\n**Automatyczne aktualizacje przestaną być wysyłane na serwery bez aktualizacji po <t:{deadline}:D>.**\n\nUżyj `/unlock`, aby subskrybować i nadal otrzymywać automatyczne aktualizacje."
},
"stacks": {
"stack_title": "Stack gracza {leader}",
"stack_named_title": "{name}",
"no_members": "Brak członków.",
"members_field": "Członkowie ({count}/{max})",
"queue_field": "Kolejka ({count}/{max})",
"manage_title": "Zarządzaj stackiem",
"no_pending_requests": "Brak oczekujących próśb.",
"disbanded_title": "Stack [Rozwiązany]",
"disbanded_desc": "Ten stack został rozwiązany przez lidera.",
"expired_title": "Stack [Wygasły]",
"expired_desc": "Ten stack wygasł.",
"join_modal_title": "Prośba o dołączenie do stacka",
"join_vehicle_label": "Czym będziesz grać?",
"join_vehicle_placeholder": "np. F-16C, WZ305...",
"ping_modal_title": "Wiadomość pingu",
"ping_message_label": "Własna wiadomość (opcjonalnie)",
"ping_message_placeholder": "np. Chodźcie! Stack startuje!",
"rename_modal_title": "Zmień nazwę stacka",
"rename_label": "Nazwa stacka",
"rename_placeholder": "np. Nocne Sowy, Drużyna Alfa...",
"select_new_leader": "Wybierz nowego lidera…",
"select_applicants": "Wybierz kandydatów…",
"no_pending_applications": "Brak oczekujących aplikacji.",
"select_to_remove": "Wybierz osoby do usunięcia…",
"no_members_or_applicants": "Brak członków lub kandydatów.",
"select_to_ping": "Wybierz osoby do indywidualnego pingu…",
"stack_not_found": "❌ Stack nie znaleziony.",
"no_longer_exists": "❌ Ten stack już nie istnieje.",
"member_not_exists": "❌ Ten członek już nie istnieje.",
"already_has_stack": "❌ Ten gracz już ma aktywny stack.",
"already_member": "❌ Już jesteś członkiem tego stacka.",
"already_applied": "❌ Już masz oczekującą aplikację do tego stacka.",
"queue_full": "❌ Kolejka jest pełna ({max}/{max}). Spróbuj później.",
"application_sent": "✅ Aplikacja wysłana! Lider stacka ją rozpatrzy.",
"stack_disbanded": "✅ Stack rozwiązany.",
"cancelled": "Anulowano.",
"select_member_transfer": "❌ Wybierz członka, na którego chcesz przekazać prowadzenie.",
"ownership_transferred": "✅ Prowadzenie przekazane do {nick}. Opuściłeś stack.",
"select_applicant_first": "❌ Najpierw wybierz co najmniej jednego kandydata.",
"stack_full": "❌ Stack jest już pełny ({max}/{max} członków).",
"select_person_first": "❌ Najpierw wybierz co najmniej jedną osobę.",
"no_one_to_ping": "❌ Nie ma kogo pingować.",
"ping_footer": "Pingowane przez {leader} dla {stack}.",
"pinged": "✅ Pingowano!",
"select_from_dropdown": "❌ Najpierw wybierz co najmniej jedną osobę z listy rozwijanej.",
"stack_renamed": "✅ Stack zmieniony na **{name}**.",
"only_member_use_disband": "❌ Jesteś jedynym członkiem. Użyj **Rozwiąż stack** aby zakończyć.",
"select_transfer_prompt": "Wybierz członka, na którego chcesz przekazać prowadzenie przed odejściem:",
"left_stack": "✅ Opuściłeś stack.",
"application_withdrawn": "✅ Twoja aplikacja została wycofana.",
"not_member_or_applicant": "❌ Nie jesteś członkiem ani kandydatem tego stacka.",
"leader_only_manage": "❌ Tylko lider stacka może nim zarządzać.",
"leader_only_disband": "❌ Tylko lider stacka może go rozwiązać.",
"confirm_disband": "Czy na pewno chcesz rozwiązać ten stack? Tej akcji nie można cofnąć.",
"already_active_stack": "⚠️ Masz już aktywny stack. Jeśli oryginalna wiadomość zniknęła (np. po restarcie bota), możesz wymusić rozwiązanie i zacząć od nowa.",
"force_created": "✅ Poprzedni stack rozwiązany. Nowy stack utworzony.",
"no_active_stack": "❌ Nie masz aktywnego stacka. Użyj `/stack-create` aby utworzyć.",
"could_not_parse_channel": "⚠️ Nie można przetworzyć zapisanego ID kanału."
},
"commands": {
"common": {
"season": "Sezon do wygenerowania karty",
"theme": "Motyw kolorystyczny karty",
"squadron_short": "Krótka nazwa dywizjonu",
"player_username": "Nazwa gracza",
"choice_dark": "Ciemny",
"choice_light": "Jasny"
},
"comp": {
"description": "Znajdź ostatnie znane składy drużyny",
"squadron_short": "Krótka nazwa wrogiej drużyny"
},
"quick_log": {
"description": "Ustaw alarm dla tego dywizjonu w tym kanale",
"squadron_name": "KRÓTKA nazwa dywizjonu do monitorowania",
"type": "Wybierz Logi, Punkty, Tabela liderów, Tygodniowy BR lub Oba",
"choice_logs": "Logi",
"choice_points": "Punkty",
"choice_leaderboard": "Ranking",
"choice_both": "Oba (Logi + Punkty)",
"choice_weekly_br": "Tygodniowy BR"
},
"sq_info": {
"description": "Pobierz informacje o dywizjonie"
},
"sq_info_graph": {
"description": "Pokaż wykres składu dywizjonu według aktywności i współczynnika wygranych (bieżący sezon)"
},
"sq_card": {
"description": "Wygeneruj kartę sezonu dla dywizjonu",
"squadron": "Krótka nazwa dywizjonu"
},
"sq_stats": {
"description": "Pokaż punkty dywizjonu w czasie"
},
"loss_calculator": {
"description": "Oblicz stratę punktów, jeśli gracze opuszczą dywizjon",
"player1": "Gracz odchodzący",
"player_optional": "Gracz odchodzący (opcjonalnie)"
},
"website": {
"description": "Pobierz link do strony SRE Bot"
},
"card": {
"description": "Wygeneruj kartę sezonu dla gracza"
},
"player_stats": {
"description": "Zobacz szczegółowe statystyki pojazdów gracza",
"username": "Nazwa WT do statystyk",
"uid": "UID WT do statystyk"
},
"view_player_games": {
"description": "Zobacz ostatnie 20 gier gracza"
},
"view_match": {
"description": "Zobacz tabelę meczu po ID lub graczu",
"match_id": "Szesnastkowe ID sesji meczu",
"player_name": "Gracz do przeglądania ostatnich meczów"
},
"compare": {
"description": "Porównaj łączne statystyki SQB graczy",
"player1": "Pierwszy gracz",
"player2": "Drugi gracz",
"player_optional": "Dodatkowy gracz (opcjonalnie)"
},
"leaderboard": {
"description": "Otwórz globalny ranking SRE Bot"
},
"set_squadron": {
"description": "Ustaw tag dywizjonu dla tego serwera",
"abbreviated_name": "Krótka nazwa dywizjonu do ustawienia"
},
"setup": {
"description": "Skonfiguruj bota dla tego serwera"
},
"meta_management": {
"description": "Zarządzaj dostępem do danych meta dla tego serwera"
},
"meta": {
"description": "Szukaj w rosterze meta po nazwie pojazdu",
"vehicle": "Nazwa pojazdu do wyszukania"
},
"top": {
"description": "Pokaż top 20 dywizjonów ze szczegółowymi statystykami"
},
"language": {
"description": "Zmień język bota."
},
"translate_message": {
"name": "Przetłumacz wiadomość"
},
"sq_track": {
"description": "Śledź dywizjon i porównaj z ostatnim sprawdzeniem",
"squadron_short_name": "Krótka nazwa dywizjonu do śledzenia"
},
"analytics": {
"description": "Zobacz zaawansowane analizy SQB dywizjonu",
"view": "Widok analizy do pokazania",
"choice_maps": "Win rate map",
"choice_comps": "Składy drużyny",
"choice_consistency": "Regularność graczy",
"choice_time": "Pora dnia",
"choice_matchups": "Historia pojedynków"
},
"recent": {
"description": "Pokaż ostatnie bitwy dywizjonu",
"length": "Liczba meczów do pokazania"
},
"vs": {
"description": "Bilans bezpośredni dwóch dywizjonów",
"squadron_a": "Pierwszy dywizjon",
"squadron_b": "Drugi dywizjon"
},
"autolog_management": {
"description": "Zarządzaj powiadomieniami autolog i diagnozuj uprawnienia"
},
"diagnose_perms": {
"description": "Diagnozuj uprawnienia autolog w tym kanale"
},
"unlock": {
"description": "Odblokuj funkcje Premium dla tego serwera"
},
"credits": {
"description": "Zobacz zespół stojący za tym projektem"
},
"schedule": {
"description": "Zobacz obecny harmonogram BR sezonu"
},
"news": {
"description": "Zobacz najnowsze newsy i ogłoszenia SRE Bot"
},
"help": {
"description": "Zobacz poradnik, ToS i linki wsparcia"
},
"donate": {
"description": "Wesprzyj rozwój SRE Bot"
},
"stack_create": {
"description": "Utwórz stack graczy",
"vehicle": "Jakim pojazdem zaczniesz?"
},
"stack_manage": {
"description": "Opublikuj ponownie aktywny stack w tym kanale"
},
"bot_status": {
"description": "Pokaż status bota: ostatnia odebrana gra i średni TTL"
}
},
"permission": {
"blacklisted_title": "❌ Zablokowano",
"blacklisted_desc": "Nie możesz używać tej komendy, bo jesteś na czarnej liście.",
"reason_line": "**Powód:** {reason}",
"access_denied_title": "⛔ Odmowa dostępu",
"no_permission_desc": "Nie masz uprawnień do użycia tej komendy.",
"unexpected_error_title": "❗ Błąd, zgłoś to...."
},
"weekly_br": {
"title_wildcard": "Tygodniowy raport BR — {br} BR",
"title_squadron": "Tygodniowy raport BR — [{tag}] {long} • {br} BR",
"window_label": "Okres: {start} → {end}",
"wildcard_desc_first": "Top {count} szwadronów wg ELO • Miejsca {low}{high}",
"wildcard_desc_second": "Top {count} szwadronów wg ELO • Miejsca {low}{high}",
"squadron_stats_line": "- {games} bitew • K/D {kdr} • Zwycięstw {wr}%",
"top_players_inline_header": "🥇 Najlepsi gracze:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}b)",
"top_players_header": "**Top {count} graczy wg ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} bitew • K/D {kdr}",
"squadron_header_line": "ELO szwadronu: {score} • {games} bitew • Zwycięstw {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO szwadronu: zbyt mała aktywność drużyny w tym tygodniu.",
"no_data": "Brak meczów dla [{tag}] w tej rotacji BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Erro",
"no_data_title": "Sem dados",
"access_denied_title": "Acesso negado",
"access_denied_desc": "Este servidor foi bloqueado.",
"no_players_selected": "Nenhum jogador selecionado. Selecione pelo menos um jogador.",
"must_use_in_server": "Este comando deve ser usado em um servidor.",
"could_not_resolve_channel": "Não foi possível resolver o canal selecionado.",
"failed_update_setting": "❌ Falha ao atualizar configuração.",
"configuration_not_found": "Configuração não encontrada.",
"no_channel_selected": "Nenhum canal selecionado.",
"no_selection_received": "Nenhuma seleção recebida.",
"database_error": "❌ Erro no banco de dados: {error}",
"enabled": "Ativado",
"disabled": "Desativado",
"not_configured": "Não configurado",
"unknown": "Desconhecido",
"rating_field": "Classificação",
"battles_field": "Batalhas",
"wins_field": "Vitórias",
"losses_field": "Derrotas",
"win_rate_field": "Taxa de Vitórias",
"kills_field": "Abates",
"deaths_field": "Mortes",
"kd_field": "K/D",
"members_field": "Membros",
"placement_field": "Colocação",
"points_field": "Pontos",
"ground_kills_field": "Abates terrestres",
"air_kills_field": "Abates aéreos",
"total_kills_field": "Total de Abates",
"assists_field": "Assistências",
"captures_field": "Capturas",
"none_option": "Nenhum"
},
"buttons": {
"skip": "Pular",
"previous": "Anterior",
"next": "Próximo",
"prev": "Ant.",
"prev_arrow": "◀ Anterior",
"next_arrow": "Próximo ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Gerar Gráfico",
"show_graph": "Mostrar gráfico",
"view_player_stats": "📊 Ver Estatísticas dos Jogadores",
"compare_nearby": "📈 Comparar Esquadrões Próximos",
"confirm_swap": "Sim, trocar",
"cancel_swap": "Não, manter o anterior",
"set_squadron": "Definir esquadrão",
"same_as_logs": "Mesmo que Logs",
"require_password": "🔒 Exigir senha",
"password_required": "🔒 Senha obrigatória",
"lock_data": "🔐 Vincular dados do esquadrão",
"data_locked": "🔐 Dados vinculados ao servidor",
"allow_public": "👥 Permitir meta público",
"public_enabled": "👥 Meta público ativado",
"update_accounts": "📋 Atualizar contas meta",
"change_password": "🔑 Alterar senha",
"help": "❓ Ajuda",
"add_player": " Adicionar jogador",
"update_all": "🔄 Atualizar todos os membros",
"back_to_settings": "⬅ Voltar às configurações",
"manage_notifications": "Gerenciar notificações",
"diagnose_permissions": "Diagnosticar permissões",
"enable": "Ativar",
"disable": "Desativar",
"change_channel": "Alterar canal",
"view_replay": "Ver replay",
"view_website": "Ver no Site",
"view_video": "Ver vídeo",
"view_log": "Ver log",
"view_chat": "Ver chat",
"subscribe_website": "Assinar pelo Site",
"yes_disband": "Sim, dissolver",
"cancel": "Cancelar",
"transfer_leave": "Transferir e sair",
"accept_selected": "Aceitar selecionados",
"accept_all": "Aceitar todos",
"decline_selected": "Recusar selecionados",
"back": "Voltar",
"remove_all": "Remover todos",
"remove_active": "Remover ativos",
"remove_queued": "Remover em espera",
"remove_selected": "Remover selecionados",
"ping_all": "Notificar todos",
"ping_active": "Notificar ativos",
"ping_queued": "Notificar em espera",
"ping_selected": "Notificar selecionados",
"accept_members": "Aceitar membros",
"remove_members": "Remover membros",
"ping_members": "Notificar membros",
"rename_stack": "Renomear stack",
"request_to_join": "Pedir para entrar",
"leave_withdraw": "Sair / Retirar",
"manage_stack": "Gerenciar stack ⚙️",
"disband_stack": "Dissolver stack",
"force_disband_create": "Forçar dissolução e criar novo"
},
"events": {
"guild_join_title": "Obrigado por me adicionar!",
"guild_join_desc": "Execute `/setup` para configurar o bot neste servidor."
},
"comp": {
"not_found_title": "Composições não encontradas",
"not_found_desc": "Sem dados para **{squadron}**, tente novamente mais tarde.",
"error_loading_title": "Erro ao carregar composições",
"error_loading_desc": "Falha ao carregar dados de composição: {error}",
"title": "Composições de {squadron}",
"desc": "Composições vistas nos últimos {minutes} minutos",
"no_recent_title": "Sem composições recentes",
"no_recent_desc": "Nenhuma composição nos últimos {minutes} minutos.",
"comp_title": "COMP {index}",
"last_seen_label": "**Visto pela última vez**: {timestamp}{warning}",
"comp_label": "**Comp**: {notation}",
"no_players_recorded": "Nenhum jogador registrado.",
"limit_reached_title": "Limite de composições atingido",
"limit_reached_desc": "Este servidor usou todas as {limit} consultas de composições para este horário. Assine (com /unlock) para acesso ilimitado ou aguarde o próximo horário.",
"remaining_footer": "{remaining}/{limit} consultas de composições restantes neste horário"
},
"quick_log": {
"invalid_type": "O tipo só pode ser definido como Logs, Pontos, Classificação, BR Semanal ou Ambos.",
"squadron_required": "Você deve fornecer um nome de esquadrão para alarmes de Logs, Pontos ou Ambos.",
"wildcard_logs_only": "Apenas Logs podem ser configurados para esquadrão curinga.",
"squadron_not_resolved": "O esquadrão `{squadron}` não pôde ser resolvido.",
"save_failed": "Falha ao salvar preferências. Tente novamente mais tarde.",
"premium_warning": "\n\n> ⚠️ **Logs de partidas requerem Premium.** Execute `/unlock` para assinar ($2.99/mês) — os logs não serão postados até então.",
"leaderboard_set": "Alarme do Placar Global definido para este canal.",
"both_set": "Alarmes de Logs e Pontos para {squadron} definidos para este canal.{premium_note}",
"alarm_set": "Alarme de {alarm_type} para {squadron} definido para este canal.{premium_note}",
"weekly_br_wildcard_set": "Relatório BR Semanal (top 20 esquadrões) configurado para este canal. Envia ao final de cada rotação de BR.",
"weekly_br_squadron_set": "Relatório BR Semanal para {squadron} (top 15 jogadores) configurado para este canal. Envia ao final de cada rotação de BR."
},
"diagnostics": {
"title": "Diagnóstico do Autolog",
"channel_permissions_header": "**Permissões do Canal** (<#{channel_id}>)",
"perms_needed": " ^ O autolog precisa de todas as permissões acima para enviar placares.",
"server_squadron_header": "**Esquadrão do Servidor** (`/set-squadron`)",
"server_squadron_short": " Curto: `{short}`",
"server_squadron_long": " Longo: `{long}`",
"server_squadron_not_set": " Não definido (a cor da barra do placar aparecerá como 'not_set')",
"autolog_prefs_header": "**Preferências do Autolog** (`/quick-log`)",
"autolog_none_configured": " ❌ NENHUM configurado — o autolog NÃO enviará nada para este servidor.",
"autolog_setup_hint": " Use `/quick-log <squadron_short> Logs` no canal desejado para configurar.",
"autolog_no_logs_channels": " ❌ Nenhum canal de Logs configurado. Apenas Pontos/Placar encontrados.",
"autolog_enable_hint": " Use `/quick-log <squadron_short> Logs` para ativar o autolog.",
"selected_channel_tag": " **(canal selecionado)**",
"missing_send_attach": " (falta permissão de envio/anexo)",
"channel_not_found": " (canal não encontrado)",
"invalid_channel_id": " (ID de canal inválido)",
"premium_status_header": "**Status Premium** (`/unlock`)",
"premium_active": " ✅ Este servidor possui uma assinatura Premium ativa.",
"premium_not_subscribed": " ❌ Este servidor **não** possui uma assinatura Premium.",
"premium_autolog_required": " O autolog requer Premium. Use `/unlock` para assinar.",
"premium_not_subscribed_free": " ⚪ Não assinado — use `/unlock` para assinar ($2.99/mês).",
"premium_free_note": " *(Os autologs estão gratuitos para todos os servidores por enquanto.)*"
},
"sq_info": {
"title": "Informações do Esquadrão: {squadron}",
"placement_field": "Colocação",
"total_points_field": "Total de Pontos",
"total_members_field": "Total de Membros",
"members_field": "Membros",
"fetch_failed": "Falha ao buscar informações do esquadrão."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Temporada {season})",
"embed_title": "{squadron} — Composição do plantel",
"embed_desc": "Temporada **{season}** · Mediana de partidas: **{median}** · Núcleo: **{core}** · Ativos: **{active}** · Fracos: **{weak}**\nBarras ordenadas por partidas desc; altura = taxa de vitória. Núcleo = ≥ mediana e TV ≥ 1,5× TV do esquadrão. Fracos = abaixo da mediana ou TV < TV do esquadrão ÷ 2. Ativos = os demais.",
"core_threshold_line": "NÚCLEO ≥ {wr} %",
"weak_threshold_line": "FRACOS < {wr} %",
"y_label": "Taxa de vitória",
"core_header": "NÚCLEO — {count} · TV {avg}%",
"active_header": "ATIVOS — {count} · TV {avg}%",
"weak_header": "FRACOS — {count} · TV {avg}%",
"no_active_season": "Nenhuma temporada ativa encontrada. Tente novamente quando a próxima começar.",
"no_members": "Nenhum membro atual encontrado para {squadron}."
},
"recap_card": {
"unknown_season": "Temporada desconhecida: `{season}`.",
"no_clan_id": "Não foi possível resolver o ID do esquadrão `{squadron}`.",
"render_failed": "Falha ao gerar o card de resumo da temporada. Tente novamente mais tarde."
},
"sq_stats": {
"no_data_title": "Sem dados",
"no_data_desc": "Nenhum dado histórico encontrado para o esquadrão: {squadron}",
"title": "{squadron} // ESQUADRÃO",
"desc": "Tendência de Pontuação Total (Últimos {count} pontos de dados)",
"previous_score_field": "Pontuação anterior",
"current_score_field": "Pontuação atual",
"change_field": "Variação",
"player_title": "{squadron} // JOGADORES",
"player_desc": "Tendências individuais de pontos dos jogadores",
"comparison_title": "{squadron} // COMPARAÇÃO NO PLACAR",
"comparison_desc": "Comparando com esquadrões classificados {range}",
"current_position_field": "Posição atual",
"squadrons_shown_field": "Esquadrões exibidos",
"squadron_not_found_error": "Esquadrão não encontrado no placar",
"no_nearby_error": "Nenhum esquadrão próximo encontrado",
"no_historical_error": "Nenhum dado histórico encontrado para esquadrões próximos",
"comparison_chart_failed": "Falha ao gerar gráfico de comparação",
"select_players_placeholder": "Selecionar jogadores (Página {page})"
},
"loss_calc": {
"title": "Perda de Pontos — {squadron}",
"players_leaving_field": "Jogadores saindo",
"share_of_total_field": "% do Total",
"points_lost_real_field": "Pontos perdidos (real)",
"points_lost_raw_field": "Pontos perdidos (bruto)",
"squadron_rating_field": "Classificação do Esquadrão",
"squadron_position_field": "Posição do Esquadrão",
"positions_lost_field": "Posições perdidas",
"not_found_footer": "Não encontrado no esquadrão: {players}",
"fetch_failed": "Falha ao buscar dados do esquadrão: {error}",
"no_point_data": "Nenhum dado de pontos disponível para este esquadrão.",
"no_matching_players": "Nenhum jogador correspondente encontrado em **{squadron}**."
},
"player": {
"select_player_placeholder": "Selecionar um jogador",
"no_stats_found": "❌ Nenhuma estatística encontrada para o UID: {uid}",
"no_vehicle_stats": "❌ Nenhuma estatística de veículo encontrada para este jogador.",
"vehicles_found": "Encontrados **{count}** veículos para **{nick}**\nSelecione um veículo para ver as estatísticas detalhadas:",
"vehicle_select_placeholder": "Selecionar um veículo (Página {page}/{total})",
"combat_stats_header": "**__ESTATÍSTICAS DE COMBATE__**",
"ground_kills_label": "**Abates Terrestres:** {value}",
"air_kills_label": "**Abates Aéreos:** {value}",
"total_kills_label": "**Total de Abates:** {value}",
"assists_label": "**Assistências:** {value}",
"deaths_label": "**Mortes:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Capturas:** {value}",
"battle_record_header": "**__HISTÓRICO DE BATALHAS__**",
"total_battles_label": "**Total de Batalhas:** {value}",
"wins_label": "**Vitórias:** {value}",
"losses_label": "**Derrotas:** {value}",
"win_rate_label": "**Taxa de Vitórias:** {value}%",
"stats_desc": "Estatísticas de **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Jogador não encontrado",
"not_found_desc": "Nenhum histórico de partidas encontrado para `{player}`.",
"no_players_found": "Nenhum jogador encontrado correspondendo a **{username}**\nTente usar `/website` para pesquisar no site.",
"multiple_matches": "Múltiplas correspondências encontradas, escolha a correta abaixo:",
"must_provide_input": "Você deve fornecer pelo menos um UID ou nome de usuário."
},
"player_games": {
"no_recent_title": "Sem partidas recentes",
"no_recent_desc": "Nenhuma partida encontrada para **{player}** nas últimas 8 horas.",
"squadron_label": "**Esquadrão:** {squadron}",
"record_label": "**V:** {wins} **D:** {losses} **TV:** {wr}%",
"comps_played_header": "\n\n**Composições Jogadas**"
},
"match": {
"missing_input_title": "Entrada ausente",
"missing_input_desc": "Forneça um `match_id` ou um `player_name`.",
"not_found_title": "Partida não encontrada",
"not_found_desc": "Não foi possível encontrar uma partida com o ID `{match_id}`.",
"invalid_data_title": "Dados de Partida Inválidos",
"invalid_data_desc": "Os dados do replay não puderam ser processados.",
"scoreboard_error_title": "Erro no Placar",
"scoreboard_error_desc": "Falha ao gerar a imagem do placar.",
"no_games_title": "Nenhuma partida encontrada",
"no_games_desc": "Nenhum histórico de partidas encontrado para **{player}**.",
"recent_matches_title": "Partidas recentes de {player}",
"recent_matches_desc": "Exibindo até {count} partidas recentes. Selecione uma para ver o placar completo.",
"select_match_placeholder": "Selecione uma partida para visualizar..."
},
"compare": {
"no_players_found": "Nenhum jogador encontrado correspondendo a **{name}**.",
"multiple_matches": "Múltiplas correspondências para **{name}**: {matches}\nPor favor, use um nome mais específico (as sugestões de autocompletar são exatas).",
"could_not_resolve": "Não foi possível resolver os jogadores.",
"could_not_fetch": "❌ Não foi possível buscar estatísticas de **{name}**.",
"no_graph_data": "Nenhum dado disponível para os últimos 90 dias.",
"no_squadron_points_data": "Nenhum dado de pontos do esquadrão para {names} (jogador não encontrado no histórico de esquadrão rastreado).",
"graph_title": "Pontos dos Jogadores — Últimos 90 Dias",
"battles_label": "Batalhas",
"wins_label": "Vitórias",
"losses_label": "Derrotas",
"win_rate_label": "Taxa de Vitórias",
"ground_kills_label": "Abates terrestres",
"air_kills_label": "Abates aéreos",
"total_kills_label": "Total de Abates",
"assists_label": "Assistências",
"deaths_label": "Mortes",
"kd_label": "K/D",
"captures_label": "Capturas"
},
"squadron": {
"not_found_desc": "Esquadrão `{squadron}` não encontrado.",
"set_title": "✅ Esquadrão definido",
"set_desc": "O esquadrão **{squadron}** foi definido para este servidor.",
"short_name_field": "Nome curto",
"long_name_field": "Nome completo",
"swap_title": "✅ Esquadrão trocado",
"swap_desc": "Substituído **{old}** por **{new}** neste servidor.",
"already_set_title": "⚠️ Esquadrão já definido",
"already_set_desc": "Este servidor está atualmente configurado para **{old}**.\nTrocar para **{new}**?",
"swap_cancelled": "❌ Alteração de esquadrão cancelada."
},
"setup": {
"step1_title": "Configuração do Servidor — Passo 1 de 3",
"step1_desc": "Este assistente vai guiá-lo pela configuração do bot para o seu servidor.\n\n**Passo 1** — Defina seu esquadrão\n**Passo 2** — Escolha um canal de logs\n**Passo 3** — Escolha um canal de pontos\n",
"step1_current_sq": "\nEsquadrão configurado atualmente: **[{short}] {long}**",
"step2_title": "Configuração do Servidor — Passo 2 de 3",
"step2_desc": "Esquadrão definido como **[{short}] {long}**.\n\nOnde os **logs de batalha** devem ser postados?\nSelecione um canal de texto abaixo ou pule esta etapa.",
"step3_title": "Configuração do Servidor — Passo 3 de 3",
"step3_desc": "Onde as **notificações de pontos** devem ser postadas?\nSelecione um canal de texto abaixo ou pule esta etapa.",
"step3_same_as_logs": "\n\nVocê também pode clicar em \"Mesmo que Logs\" para reutilizar o canal de logs.",
"summary_title": "Configuração concluída",
"summary_desc": "Você pode usar `/autolog-management` para alterar essas configurações mais tarde.",
"squadron_field": "Esquadrão",
"logs_channel_field": "Canal de Logs",
"points_channel_field": "Canal de Pontos",
"premium_required_field": "⚠️ Logs de Partidas Requerem Premium",
"premium_required_value": "Os placares automáticos de partidas não serão postados até que este servidor tenha uma assinatura ativa. Execute `/unlock` para assinar ($2.99/mês).",
"modal_title": "Definir esquadrão",
"modal_label": "Nome curto do esquadrão",
"modal_placeholder": "ex. AXYS",
"squadron_not_found": "Esquadrão `{squadron}` não encontrado. Por favor, tente novamente.",
"logs_channel_placeholder": "Selecione um canal de logs...",
"points_channel_placeholder": "Selecione um canal de pontos..."
},
"meta_management": {
"squadron_not_found_title": "❌ Esquadrão não encontrado",
"squadron_not_found_desc": "Não foi possível encontrar o ID do clã para o esquadrão: **{squadron}**",
"access_denied_title": "❌ Acesso negado",
"access_denied_desc": "Senha incorreta. Os dados meta deste esquadrão estão protegidos.",
"data_locked_title": "🔐 Dados do esquadrão vinculados",
"data_locked_desc": "**{squadron}** tem a vinculação de dados ativada e não pode ser transferido para outro servidor.\n\nO dono do esquadrão deve desativar **Vincular Dados do Esquadrão** antes de movê-lo.",
"error_retrieving_settings": "❌ Erro ao recuperar as configurações do servidor após a transferência. Por favor, tente novamente.",
"error_retrieving_settings_retry": "❌ Erro ao recuperar as configurações do servidor. Por favor, execute o comando novamente.",
"authenticated_title": "✅ Autenticado",
"authenticated_desc": "Senha verificada. Gerenciando configurações para **{squadron}**.",
"claimed_title": "✅ Esquadrão reivindicado",
"claimed_desc": "**{squadron}** foi reivindicado com sucesso para este servidor!",
"password_requirement_field": "🔒 Requisito de senha",
"data_lock_field": "🔐 Vinculação de dados do esquadrão",
"public_meta_field": "👥 Acesso meta público",
"access_password_field": "🔑 Senha de acesso",
"enabled_value": "✅ Ativado",
"disabled_value": "❌ Desativado",
"settings_title": "🔐 Configurações de gerenciamento meta",
"settings_desc": "**Esquadrão:** {squadron}\n**ID do Clã:** {clan_id}",
"first_time_title": "🔐 Gerenciamento meta - Configuração inicial",
"first_time_owner_desc": "**Esquadrão:** {squadron}\n**ID do Clã:** {clan_id}\n\n🔑 Sua senha de acesso foi gerada. **Salve esta senha** — você precisará dela para autenticar o acesso a dados meta no futuro.\n\n**Senha:** `{password}`",
"first_time_non_owner_desc": "**Esquadrão:** {squadron}\n**ID do Clã:** {clan_id}\n\nO esquadrão foi configurado. Peça a senha de acesso ao dono do servidor.",
"settings_field": "Configurações",
"settings_hint": "Use os botões abaixo para definir as configurações de acesso.",
"password_toggled": "✅ Requisito de senha: **{state}**",
"lock_toggled": "✅ Vinculação de dados do esquadrão: **{state}**",
"public_meta_toggled": "✅ Acesso meta público: **{state}**\n{detail}",
"public_meta_enabled_detail": "Não-administradores agora podem usar o comando `/meta`.",
"public_meta_disabled_detail": "Apenas administradores podem usar o comando `/meta`.",
"owner_only_password": "❌ Apenas o dono do servidor pode alterar a senha do esquadrão.",
"help_title": "📖 Ajuda do gerenciamento meta",
"help_desc": "Explicação de cada configuração e funcionalidade:",
"help_password_field": "🔑 Senha de acesso",
"help_password_value": "A senha de acesso do seu esquadrão. Apenas o **dono do servidor** pode ver a senha no painel de configurações. Qualquer pessoa com a senha pode reivindicar os dados meta do seu esquadrão em outro servidor, portanto mantenha-a segura.",
"help_require_field": "🔒 Exigir senha",
"help_require_value": "Quando ativado, mesmo os administradores deste servidor devem inserir a senha do esquadrão para acessar `/meta-management`. Adiciona uma camada extra de segurança para evitar alterações acidentais.",
"help_lock_field": "🔐 Vincular dados do esquadrão",
"help_lock_value": "Quando ativado, impede que o esquadrão seja transferido para outros servidores, mesmo com a senha correta. Deve ser desativado antes que o esquadrão possa ser transferido.",
"help_public_field": "👥 Permitir meta público",
"help_public_value": "Quando ativado, permite que membros não-administradores usem o comando `/meta` para pesquisar veículos do esquadrão. Quando desativado, apenas administradores do servidor podem usar `/meta`.",
"help_accounts_field": "📋 Atualizar contas meta",
"help_accounts_value": "Abre o gerenciador de lista de jogadores onde você pode adicionar ou remover jogadores da lista meta do seu esquadrão. Use **Atualizar Todos os Membros** para sincronizar todo o seu esquadrão de uma vez.",
"help_change_pw_field": "🔑 Alterar senha",
"help_change_pw_value": "**Apenas o dono do servidor.** Altere a senha de acesso do esquadrão e defina uma dica opcional. A dica é exibida no prompt de senha para ajudar a lembrá-la.",
"password_modal_title": "Senha de acesso do esquadrão",
"password_modal_label": "Inserir senha do esquadrão",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Alterar senha do esquadrão",
"current_password_label": "Senha atual",
"current_password_placeholder": "Digite sua senha atual",
"new_password_label": "Nova senha",
"new_password_placeholder": "Digite sua nova senha",
"confirm_password_label": "Confirmar nova senha",
"confirm_password_placeholder": "Re-insira sua nova senha",
"hint_label": "Dica de senha (opcional)",
"hint_placeholder": "Uma dica para ajudar a lembrar a senha",
"pw_incorrect": "❌ A senha atual está incorreta.",
"pw_mismatch": "❌ As novas senhas não coincidem. Por favor, tente novamente.",
"pw_empty": "❌ A nova senha não pode ser vazia.",
"pw_changed": "✅ Senha atualizada com sucesso para **{squadron}**.\n**Nova senha:** `{password}`",
"pw_changed_hint": "\n**Dica:** {hint}",
"player_add_modal_title": "Adicionar jogador à lista meta",
"player_add_label": "UID ou apelido do jogador",
"player_add_placeholder": "Insira o UID do jogador (ex., 12345678) ou apelido",
"player_not_found": "❌ Jogador `{player}` não encontrado no banco de dados Players_Global.\n",
"roster_title": "📋 Gerenciamento da lista meta - {squadron}",
"roster_desc": "**ID do Clã do Esquadrão:** {clan_id}\n**Total de Jogadores:** {count}",
"roster_page_field": "Jogadores (Página {page}/{total})",
"no_players_field": "Sem jogadores",
"no_players_hint": "Nenhum jogador adicionado à lista meta ainda. Clique em **Adicionar jogador** para começar.",
"remove_player_placeholder": "Selecionar jogador para remover...",
"fetch_members_failed": "❌ Falha ao buscar membros do esquadrão: {error}",
"no_members_found": "❌ Nenhum membro encontrado no esquadrão ou a chamada à API falhou.",
"roster_synced": "✅ Lista sincronizada com o esquadrão.",
"roster_added": "**+{count}** adicionado(s)",
"roster_removed": "**-{count}** removido(s) (saiu do esquadrão)",
"roster_up_to_date": "**{count}** já atualizados",
"refreshing_vehicles": "Atualizando dados de veículos em segundo plano..."
},
"meta": {
"not_configured": "❌ Dados meta não configurados para este servidor. Execute `/meta-management` primeiro.",
"no_permission": "❌ Você precisa de permissões de administrador para usar este comando.\nAdmins podem ativar o acesso público via `/meta-management`.",
"no_results": "❌ Nenhum jogador na lista do seu esquadrão possui **{vehicle}**.",
"no_results_admin_hint": "\n*Esperando que alguém tenha isso? Clique no botão de atualizar membros em `/meta-management` e verifique novamente.*",
"search_title": "🔍 Resultados da Busca - {vehicle}",
"matches_found": "**Correspondências Encontradas:** {count} jogador(es)",
"spawns_label": "Spawns",
"deaths_label": "Mortes",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "Pontos",
"kdr_label": "KDR",
"games_label": "Partidas",
"no_points": "—"
},
"top": {
"title": "**Top 20 Esquadrões**",
"rating_label": "**Classificação:** {value}",
"air_kills_label": "**Abates Aéreos:** {value}",
"ground_kills_label": "**Abates Terrestres:** {value}",
"deaths_label": "**Mortes:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Taxa de Vitórias:** {value}",
"playtime_label": "**Tempo de Jogo:** {value}",
"fetch_failed": "Falha ao recuperar dados do esquadrão."
},
"analytics": {
"no_data_title": "Sem dados",
"no_matches_desc": "Nenhuma partida encontrada.",
"no_comp_desc": "Nenhum dado de composição encontrado.",
"no_consistency_desc": "Dados de jogadores insuficientes (mínimo de 50 partidas).",
"no_time_desc": "Nenhum dado de tempo encontrado.",
"unknown_view": "Visualização desconhecida.",
"map_title": "Taxas de Vitória por Mapa: {squadron}",
"comp_title": "Composições de Time: {squadron}",
"consistency_title": "Consistência dos Jogadores: {squadron}",
"consistency_desc": "Ordenado por proporção K/D",
"time_title": "Desempenho por Hora do Dia: {squadron}",
"eu_timeslot": "\n**Horário EU**",
"na_timeslot": "\n**Horário NA**",
"off_peak": "\n**Fora do Pico**",
"matchups_title": "📜 {squadron} — Histórico de Confrontos",
"matchups_won_field": "🏆 Mais Vitórias Contra",
"matchups_lost_field": "💀 Mais Derrotas Contra",
"no_matchups_desc": "Sem partidas registadas contra outros esquadrões."
},
"recent": {
"title": "Partidas recentes: {squadron}",
"no_matches_desc": "Nenhuma partida encontrada para este esquadrão."
},
"h2h": {
"two_required_title": "Dois esquadrões necessários",
"two_required_desc": "Forneça pelo menos um esquadrão, ou use `/set-squadron` e forneça o adversário.",
"provide_a_desc": "Forneça `squadron_a` ou use `/set-squadron` primeiro.",
"provide_b_desc": "Forneça `squadron_b` ou use `/set-squadron` primeiro.",
"squadron_not_found_title": "Esquadrão não encontrado",
"same_squadron_title": "Mesmo esquadrão",
"same_squadron_desc": "Você não pode verificar o confronto direto contra si mesmo.",
"record_desc": "**Confronto:** {a_wins}V - {b_wins}D ({total} partidas)",
"no_matches_desc": "Nenhuma partida registrada entre **{a}** e **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Ativo — o autolog está ativado para este servidor.",
"premium_not_subscribed_line": "❌ **Premium:** Não assinado — use `/unlock` para ativar o autolog.",
"premium_free_line": "⚪ **Premium:** Não assinado — use `/unlock` para assinar ($2.99/mês). *(Os autologs estão gratuitos para todos os servidores por enquanto.)*",
"what_to_do": "\n\nO que você gostaria de fazer?",
"select_notif_type": "Selecione o tipo de notificação a gerenciar:",
"select_notif_placeholder": "Selecionar tipo de notificação",
"logs_option": "Logs",
"logs_option_desc": "Gerenciar notificações de Logs",
"points_option": "Pontos",
"points_option_desc": "Gerenciar notificações de Pontos",
"leaderboard_option": "Placar",
"leaderboard_option_desc": "Gerenciar notificações do Placar",
"selected_type": "Selecionado **{type}**. Agora escolha o esquadrão a gerenciar:",
"select_squadron_placeholder": "Selecionar um esquadrão",
"select_squadron_page_placeholder": "Selecionar um esquadrão (Página {page})",
"no_squadrons_available": "Nenhum esquadrão disponível para este tipo de notificação.",
"managing_global": "Gerenciando **{type}** (global) no canal **{channel}**.",
"managing_squadron": "Gerenciando **{type}** para o esquadrão **{squadron}** no canal **{channel}**.",
"select_channel": "Selecione um novo canal:",
"select_channel_placeholder": "Selecionar um canal",
"select_channel_page_placeholder": "Selecionar um canal (Página {page})",
"global_toggled": "{type} (global) agora está {state}.",
"squadron_toggled": "{type} para **{squadron}** agora está {state}.",
"channel_updated_global": "{type} (global) atualizado para {channel}",
"channel_updated_squadron": "{type} para **{squadron}** atualizado para {channel}",
"diagnose_channel_placeholder": "Selecionar um canal para diagnosticar...",
"select_channel_diagnose": "Selecione o canal para diagnosticar:",
"game_not_logged_title": "Partida não registrada",
"game_not_logged_desc": "Use `/unlock` para assinar o plano **Standard** (ou superior) e receber placares automáticos de partidas.",
"server_not_upgraded_title": "⚠️ Servidor não atualizado",
"server_not_upgraded_autolog_desc": "Este servidor não possui uma assinatura Premium ativa.\n\n**Os placares automáticos de partidas deixarão de ser enviados para servidores não atualizados após <t:{deadline}:D>.**\n\nUse `/unlock` para assinar e continuar recebendo logs automáticos de partidas.",
"replay_not_available": "Os dados do replay ainda não estão disponíveis — aguarde um momento e tente novamente!",
"too_many_videos": "Muitos vídeos sendo gerados agora — por favor, tente novamente em instantes.",
"video_gen_failed": "Erro ao gerar vídeo: `{error}`",
"video_missing": "Falha ao gerar o vídeo do replay - arquivo de saída ausente ou vazio.",
"video_too_large": "Vídeo do replay muito grande para enviar ({file_mb:.1f} MB). O limite do servidor é {limit_mb:.0f} MB.",
"video_web_fallback": "Você também pode ver esta partida em {url}",
"video_upload_failed": "Vídeo muito grande para enviar — veja-o no site:\n{url}",
"video_unexpected_error": "Erro inesperado ao gerar o vídeo do replay: `{error}`",
"replay_not_found": "Dados do replay não encontrados para a sessão `{session_id}` no disco.",
"chat_log_title": "**Log de Chat da Partida [{session_id}]({url})**",
"chat_log_part_title": "**Log de Chat da Partida [{session_id}]({url}) (Parte {part}/{total})**",
"chat_log_part_only": "**Log de Chat (Parte {part}/{total})**",
"no_chat_log": "Nenhum log de chat encontrado para a sessão `{session_id}`.",
"chat_log_error": "Erro inesperado ao carregar o log de chat: `{error}`",
"battle_log_title": "**Log de Batalha da Partida [{session_id}]({url})**",
"battle_log_part_title": "**Log de Batalha da Partida [{session_id}]({url}) (Parte {part}/{total})**",
"battle_log_part_only": "**Log de Batalha (Parte {part}/{total})**",
"no_battle_log": "Nenhum evento de combate encontrado para a sessão `{session_id}`.",
"battle_log_error": "Erro inesperado ao carregar o log de batalha: `{error}`",
"points_update_title": "**{squadron} {region} Atualização de Pontos**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Alterações dos Jogadores:**",
"points_table_header": "Nome Alteração Agora\n",
"wl_line": "\n**{squadron}** foi **{wins}V-{losses}D** nesta sessão",
"placement_rose": "\n**{squadron}** subiu para o **{new_place}** do **{old_place}**",
"placement_fell": "\n**{squadron}** caiu para o **{new_place}** do **{old_place}**",
"points_not_logged_title": "Pontos não registrados",
"points_not_logged_desc": "Use `/unlock` para assinar o plano **Standard** (ou superior) e receber atualizações automáticas de pontos.",
"server_not_upgraded_points_desc": "Este servidor não possui uma assinatura Premium ativa.\n\n**As atualizações automáticas deixarão de ser enviadas para servidores não atualizados após <t:{deadline}:D>.**\n\nUse `/unlock` para assinar e continuar recebendo atualizações automáticas.",
"leave_title": "⚠️ Jogador Saiu de {squadron}",
"leave_desc": "**{nick}** ({uid}) saiu do esquadrão.\n\nÚltimos pontos registrados: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Esquadrão acima do limite do seu plano",
"over_cap_desc": "Seu servidor está no plano **{tier}**, que permite **{cap} {notif}** esquadrões. O esquadrão **{squadron}** está acima do limite e não está sendo registrado. Atualize para um plano maior.",
"over_cap_footer": "Atualize em srebot-meow.ing/premium ou via /unlock",
"wildcard_blocked_title": "Wildcard requer um plano superior",
"wildcard_blocked_desc": "Entradas wildcard (*, all, everything) só estão disponíveis nos planos Pro ou Max. Seu servidor está em **{tier}** para {notif}. Atualize para habilitar.",
"cap_header": "{used}/{cap} {notif} ativos — plano {tier}"
},
"track": {
"squadron_not_found": "Esquadrão não encontrado.",
"fetch_failed": "Falha ao buscar informações do esquadrão."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Desbloqueie recursos premium para este servidor.**\n\nO Premium inclui:\n> • Publicações automáticas de placar\n> • Logs de chat e batalha\n> • Consultas de replay\n> • Consultas /comp ilimitadas\n> • Suporte prioritário\n\n**$2.99 / mês · por servidor · cancele quando quiser**\n\n⚠️ A cobrança pelo Discord está disponível apenas em países selecionados. Se o botão abaixo exibir **\"Produto Indisponível\"**, pode ser devido a um país não suportado ou ao uso de um **dispositivo móvel**. Use o botão **Assinar pelo Site** em vez disso.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Este servidor já está inscrito!**",
"manage_discord_field": "Gerenciar assinatura",
"manage_discord_value": "Sua assinatura é pelo **Discord**.\nPara cancelar, vá em **Configurações do Usuário → Assinaturas** no Discord.",
"manage_website_field": "Gerenciar assinatura",
"manage_website_value": "Sua assinatura é pelo **site**.\nGerencie-a em [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Em breve",
"coming_soon_value": "As assinaturas Premium ainda não estão disponíveis. Volte em breve!",
"current_tier": "Você está no plano **{tier}**.",
"upgrade_to": "Atualizar para {tier}",
"upgrade_to_value": "Mais esquadrões e recursos atualizando para **{tier}**."
},
"language": {
"prompt": "Por favor, selecione o idioma do seu servidor:",
"select_placeholder": "Escolha o idioma do seu servidor",
"language_set": "Idioma definido para {language}.",
"translate_prompt": "Selecione abaixo o idioma de destino 👇",
"translate_placeholder": "Escolha um idioma de destino…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Tradução indisponível (DeepL não configurado)",
"translation_failed": "Falha na tradução"
},
"misc": {
"credits_title": "Créditos",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Desenvolvedor Principal, Gerente do Bot, Gerente da Comunidade\n> **Z3R0** - Desenvolvedor, Desenvolvedor de Otimização, Engenheiro de Banco de Dados\n> **Clippii (Heidi)** - Desenvolvedor, Desenvolvedor do Site, Gerente da Comunidade\n> **LivingTheDagor** - Desenvolvedor, Desenvolvedor de Parser, Consultor\n> **Lux_** - Engenheiro de API, Desenvolvedor Spectra\n> **Konigallerwaffen** - Consultor de Feedback e Funcionalidades\n> **Žralok Tonda** - Tradutor Tcheco\n> **Styevy**, **Lopais** - Tradutores Alemães\n> **Susogus**, **playforfun698** - Tradutores Poloneses\n> **Bobr** - Tradutor Russo\n\n\n[Quer se juntar a nós?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "CALENDÁRIO DA TEMPORADA",
"schedule_not_found_title": "Calendário não encontrado",
"schedule_not_found_desc": "Nenhum dado de calendário disponível ainda.",
"news_no_news_title": "Sem notícias",
"news_no_news_desc": "Não há anúncios no momento. Volte mais tarde!",
"news_footer": "Obrigado pelo seu apoio! ᕙᘘᗢ",
"help_title": "Guia do Bot",
"donate_title": "Apoie o SRE Bot",
"donate_desc": "Se você gosta de usar o SRE Bot e quer apoiar seu desenvolvimento, considere me pagar um café!\n\n**[Doar no Ko-fi](https://ko-fi.com/notsotoothless)**\n\nCada contribuição ajuda a manter o bot funcionando e apoia novos recursos. Obrigado!",
"status_title": "Status do bot",
"status_last_received": "Última partida recebida",
"status_avg_ttl": "TTL médio (últimas 30)",
"status_no_data": "Sem dados ainda",
"status_gaijin_slow": "⚠️ Servidores da Gaijin lentos",
"help_commands_header": "**Visão geral dos comandos**",
"help_links": "Para detalhes, leia a documentação [aqui]({docs}) ou peça suporte [aqui]({support}).",
"help_terms": "[Termos de Serviço]({terms}) • [Política de Privacidade]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Placar do Esquadrão",
"top15_desc": "Top 15 esquadrões com estatísticas, enviado 35 minutos após o fechamento do horário.\nEste foi enviado <t:{timestamp}:R>.",
"top30_desc": "Esquadrões 16-30 com estatísticas.",
"not_logged_title": "Placar não registrado",
"not_logged_desc": "Use `/unlock` para assinar o plano **Standard** (ou superior) e receber atualizações automáticas do placar.",
"server_not_upgraded_title": "⚠️ Servidor não atualizado",
"server_not_upgraded_desc": "Este servidor não possui uma assinatura Premium ativa.\n\n**As atualizações automáticas deixarão de ser enviadas para servidores não atualizados após <t:{deadline}:D>.**\n\nUse `/unlock` para assinar e continuar recebendo atualizações automáticas."
},
"stacks": {
"stack_title": "Stack de {leader}",
"stack_named_title": "{name}",
"no_members": "Sem membros ainda.",
"members_field": "Membros ({count}/{max})",
"queue_field": "Fila ({count}/{max})",
"manage_title": "Gerenciar Stack",
"no_pending_requests": "Sem pedidos pendentes.",
"disbanded_title": "Stack [Dissolvido]",
"disbanded_desc": "Este stack foi dissolvido pelo líder.",
"expired_title": "Stack [Expirado]",
"expired_desc": "Este stack expirou.",
"join_modal_title": "Pedir para entrar no stack",
"join_vehicle_label": "Com o que vai jogar?",
"join_vehicle_placeholder": "ex. F-16C, WZ305...",
"ping_modal_title": "Mensagem de notificação",
"ping_message_label": "Mensagem personalizada (opcional)",
"ping_message_placeholder": "ex. Venham agora! O stack está começando!",
"rename_modal_title": "Renomear stack",
"rename_label": "Nome do stack",
"rename_placeholder": "ex. Corujas Noturnas, Esquadrão Alfa...",
"select_new_leader": "Selecionar novo líder…",
"select_applicants": "Selecionar candidatos…",
"no_pending_applications": "Sem candidaturas pendentes.",
"select_to_remove": "Selecionar pessoas para remover…",
"no_members_or_applicants": "Sem membros ou candidatos.",
"select_to_ping": "Selecionar pessoas para notificar individualmente…",
"stack_not_found": "❌ Stack não encontrado.",
"no_longer_exists": "❌ Este stack não existe mais.",
"member_not_exists": "❌ Esse membro não existe mais.",
"already_has_stack": "❌ Esse jogador já tem um stack ativo.",
"already_member": "❌ Você já é membro deste stack.",
"already_applied": "❌ Você já tem uma candidatura pendente para este stack.",
"queue_full": "❌ A fila está cheia ({max}/{max}). Tente novamente mais tarde.",
"application_sent": "✅ Candidatura enviada! O líder do stack irá analisá-la.",
"stack_disbanded": "✅ Stack dissolvido.",
"cancelled": "Cancelado.",
"select_member_transfer": "❌ Selecione um membro para transferir a liderança.",
"ownership_transferred": "✅ Liderança transferida para {nick}. Você saiu do stack.",
"select_applicant_first": "❌ Selecione pelo menos um candidato primeiro.",
"stack_full": "❌ O stack está cheio ({max}/{max} membros).",
"select_person_first": "❌ Selecione pelo menos uma pessoa primeiro.",
"no_one_to_ping": "❌ Ninguém para notificar.",
"ping_footer": "Notificado por {leader} para {stack}.",
"pinged": "✅ Notificado!",
"select_from_dropdown": "❌ Selecione pelo menos uma pessoa do menu suspenso primeiro.",
"stack_renamed": "✅ Stack renomeado para **{name}**.",
"only_member_use_disband": "❌ Você é o único membro. Use **Dissolver stack** para encerrar.",
"select_transfer_prompt": "Selecione um membro para transferir a liderança antes de sair:",
"left_stack": "✅ Você saiu do stack.",
"application_withdrawn": "✅ Sua candidatura foi retirada.",
"not_member_or_applicant": "❌ Você não é membro nem candidato deste stack.",
"leader_only_manage": "❌ Apenas o líder do stack pode gerenciá-lo.",
"leader_only_disband": "❌ Apenas o líder do stack pode dissolvê-lo.",
"confirm_disband": "Tem certeza de que deseja dissolver este stack? Esta ação não pode ser desfeita.",
"already_active_stack": "⚠️ Você já tem um stack ativo. Se a mensagem original desapareceu (ex. após reinício do bot), você pode forçar a dissolução e começar de novo.",
"force_created": "✅ Stack anterior dissolvido. Novo stack criado.",
"no_active_stack": "❌ Você não tem um stack ativo. Use `/stack-create` para criar um.",
"could_not_parse_channel": "⚠️ Não foi possível processar o ID do canal armazenado."
},
"commands": {
"common": {
"season": "A temporada para gerar o cartão",
"theme": "Tema de cor do cartão",
"squadron_short": "Nome curto do esquadrão",
"player_username": "Nome do jogador",
"choice_dark": "Escuro",
"choice_light": "Claro"
},
"comp": {
"description": "Encontrar as últimas composições conhecidas de uma equipe",
"squadron_short": "Nome curto da equipe inimiga"
},
"quick_log": {
"description": "Configurar um alarme para este esquadrão neste canal",
"squadron_name": "Nome CURTO do esquadrão a monitorar",
"type": "Escolha Logs, Pontos, Classificação, BR Semanal ou Ambos",
"choice_logs": "Logs",
"choice_points": "Pontos",
"choice_leaderboard": "Ranking",
"choice_both": "Ambos (Logs + Pontos)",
"choice_weekly_br": "BR Semanal"
},
"sq_info": {
"description": "Buscar informações de um esquadrão"
},
"sq_info_graph": {
"description": "Mostrar um gráfico da composição do plantel por atividade e taxa de vitória (temporada atual)"
},
"sq_card": {
"description": "Gerar cartão de temporada para um esquadrão",
"squadron": "Nome curto do esquadrão"
},
"sq_stats": {
"description": "Mostrar pontos de um esquadrão ao longo do tempo"
},
"loss_calculator": {
"description": "Calcular perda de pontos se jogadores saírem do esquadrão",
"player1": "Jogador saindo",
"player_optional": "Jogador saindo (opcional)"
},
"website": {
"description": "Obter link do site do SRE Bot"
},
"card": {
"description": "Gerar cartão de temporada para um jogador"
},
"player_stats": {
"description": "Ver estatísticas detalhadas de veículos de um jogador",
"username": "Nome WT para solicitar stats",
"uid": "UID WT para solicitar stats"
},
"view_player_games": {
"description": "Ver os últimos 20 jogos de um jogador"
},
"view_match": {
"description": "Ver placar de partida por ID ou jogador",
"match_id": "ID hex da sessão da partida",
"player_name": "Jogador para navegar partidas recentes"
},
"compare": {
"description": "Comparar stats SQB agregadas entre jogadores",
"player1": "Primeiro jogador",
"player2": "Segundo jogador",
"player_optional": "Jogador adicional (opcional)"
},
"leaderboard": {
"description": "Obter ranking global do SRE Bot"
},
"set_squadron": {
"description": "Definir tag de esquadrão deste servidor",
"abbreviated_name": "Nome curto do esquadrão a definir"
},
"setup": {
"description": "Configurar o bot para este servidor"
},
"meta_management": {
"description": "Gerenciar acesso aos dados meta deste servidor"
},
"meta": {
"description": "Pesquisar roster meta por nome de veículo",
"vehicle": "Nome do veículo a pesquisar"
},
"top": {
"description": "Ver top 20 esquadrões com stats detalhadas"
},
"language": {
"description": "Alterar o idioma do bot."
},
"translate_message": {
"name": "Traduzir mensagem"
},
"sq_track": {
"description": "Acompanhar um esquadrão e comparar desde a última verificação",
"squadron_short_name": "Nome curto do esquadrão a acompanhar"
},
"analytics": {
"description": "Ver análises SQB avançadas de um esquadrão",
"view": "Qual análise mostrar",
"choice_maps": "Taxa de vitória por mapa",
"choice_comps": "Composições de equipe",
"choice_consistency": "Consistência dos jogadores",
"choice_time": "Hora do dia",
"choice_matchups": "Histórico de confrontos"
},
"recent": {
"description": "Mostrar batalhas recentes de um esquadrão",
"length": "Número de partidas a mostrar"
},
"vs": {
"description": "Histórico direto entre dois esquadrões",
"squadron_a": "Primeiro esquadrão",
"squadron_b": "Segundo esquadrão"
},
"autolog_management": {
"description": "Gerenciar notificações autolog e diagnosticar permissões"
},
"diagnose_perms": {
"description": "Diagnosticar permissões autolog deste canal"
},
"unlock": {
"description": "Desbloquear recursos Premium para este servidor"
},
"credits": {
"description": "Ver a equipe creditada por este projeto"
},
"schedule": {
"description": "Ver o calendário BR da temporada atual"
},
"news": {
"description": "Ver últimas notícias e anúncios do SRE Bot"
},
"help": {
"description": "Ver guia, ToS e links de suporte"
},
"donate": {
"description": "Apoiar o desenvolvimento do SRE Bot"
},
"stack_create": {
"description": "Criar um stack de jogadores",
"vehicle": "Com qual veículo você vai começar?"
},
"stack_manage": {
"description": "Repostar seu stack ativo neste canal"
},
"bot_status": {
"description": "Ver status do bot: última partida recebida e TTL médio"
}
},
"permission": {
"blacklisted_title": "❌ Bloqueado",
"blacklisted_desc": "Você está bloqueado de usar este comando.",
"reason_line": "**Motivo:** {reason}",
"access_denied_title": "⛔ Acesso negado",
"no_permission_desc": "Você não tem permissão para usar este comando.",
"unexpected_error_title": "❗ Erro, reporte isso...."
},
"weekly_br": {
"title_wildcard": "Relatório BR Semanal — {br} BR",
"title_squadron": "Relatório BR Semanal — [{tag}] {long} • {br} BR",
"window_label": "Período: {start} → {end}",
"wildcard_desc_first": "Top {count} esquadrões por ELO • Posições {low}{high}",
"wildcard_desc_second": "Top {count} esquadrões por ELO • Posições {low}{high}",
"squadron_stats_line": "- {games} partidas • K/D {kdr} • Vitórias {wr}%",
"top_players_inline_header": "🥇 Melhores jogadores:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}p)",
"top_players_header": "**Top {count} jogadores por ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} partidas • K/D {kdr}",
"squadron_header_line": "ELO do esquadrão: {score} • {games} partidas • Vitórias {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO do esquadrão: pouca atividade da equipe esta semana.",
"no_data": "Nenhuma partida registrada para [{tag}] nesta rotação de BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Ошибка",
"no_data_title": "Нет данных",
"access_denied_title": "Доступ запрещён",
"access_denied_desc": "Этот сервер заблокирован.",
"no_players_selected": "Игроки не выбраны. Выберите хотя бы одного игрока.",
"must_use_in_server": "Эта команда доступна только на сервере.",
"could_not_resolve_channel": "Не удалось определить выбранный канал.",
"failed_update_setting": "❌ Не удалось обновить настройку.",
"configuration_not_found": "Конфигурация не найдена.",
"no_channel_selected": "Канал не выбран.",
"no_selection_received": "Выбор не получен.",
"database_error": "❌ Ошибка базы данных: {error}",
"enabled": "Включено",
"disabled": "Отключено",
"not_configured": "Не настроено",
"unknown": "Неизвестно",
"rating_field": "Рейтинг",
"battles_field": "Бои",
"wins_field": "Победы",
"losses_field": "Поражения",
"win_rate_field": "Процент побед",
"kills_field": "Уничтожения",
"deaths_field": "Гибели",
"kd_field": "K/D",
"members_field": "Участники",
"placement_field": "Место",
"points_field": "Очки",
"ground_kills_field": "Наземные уничтожения",
"air_kills_field": "Воздушные уничтожения",
"total_kills_field": "Всего уничтожений",
"assists_field": "Помощь",
"captures_field": "Захваты",
"none_option": "Нет"
},
"buttons": {
"skip": "Пропустить",
"previous": "Назад",
"next": "Далее",
"prev": "Назад",
"prev_arrow": "◀ Назад",
"next_arrow": "Далее ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Построить график",
"show_graph": "Показать график",
"view_player_stats": "📊 Статистика игроков",
"compare_nearby": "📈 Сравнить соседние полки",
"confirm_swap": "Да, заменить",
"cancel_swap": "Нет, оставить прежний",
"set_squadron": "Установить полк",
"same_as_logs": "Как в логах",
"require_password": "🔒 Требовать пароль",
"password_required": "🔒 Пароль обязателен",
"lock_data": "🔐 Привязать данные полка",
"data_locked": "🔐 Данные привязаны к серверу",
"allow_public": "👥 Разрешить публичную мету",
"public_enabled": "👥 Публичная мета включена",
"update_accounts": "📋 Обновить мета-аккаунты",
"change_password": "🔑 Изменить пароль",
"help": "❓ Помощь",
"add_player": "➕ Добавить игрока",
"update_all": "🔄 Обновить всех участников",
"back_to_settings": "⬅ К настройкам",
"manage_notifications": "Управление уведомлениями",
"diagnose_permissions": "Диагностика прав",
"enable": "Включить",
"disable": "Отключить",
"change_channel": "Сменить канал",
"view_replay": "Просмотреть реплей",
"view_website": "Открыть на сайте",
"view_video": "Просмотреть видео",
"view_log": "Просмотреть лог",
"view_chat": "Просмотреть чат",
"subscribe_website": "Подписаться через сайт",
"yes_disband": "Да, распустить",
"cancel": "Отмена",
"transfer_leave": "Передать и выйти",
"accept_selected": "Принять выбранных",
"accept_all": "Принять всех",
"decline_selected": "Отклонить выбранных",
"back": "Назад",
"remove_all": "Удалить всех",
"remove_active": "Удалить активных",
"remove_queued": "Удалить в очереди",
"remove_selected": "Удалить выбранных",
"ping_all": "Пинг всех",
"ping_active": "Пинг активных",
"ping_queued": "Пинг в очереди",
"ping_selected": "Пинг выбранных",
"accept_members": "Принять участников",
"remove_members": "Удалить участников",
"ping_members": "Пинг участников",
"rename_stack": "Переименовать стак",
"request_to_join": "Запросить вступление",
"leave_withdraw": "Выйти / Отозвать",
"manage_stack": "Управление стаком ⚙️",
"disband_stack": "Распустить стак",
"force_disband_create": "Принудительно распустить и создать новый"
},
"events": {
"guild_join_title": "Спасибо, что добавили меня!",
"guild_join_desc": "Запустите `/setup`, чтобы настроить бота для этого сервера."
},
"comp": {
"not_found_title": "Составы не найдены",
"not_found_desc": "Нет данных для **{squadron}**, попробуйте позже.",
"error_loading_title": "Ошибка загрузки составов",
"error_loading_desc": "Не удалось загрузить данные составов: {error}",
"title": "Составы полка {squadron}",
"desc": "Составы, замеченные за последние {minutes} минут",
"no_recent_title": "Нет недавних составов",
"no_recent_desc": "Нет составов за последние {minutes} минут.",
"comp_title": "СОСТАВ {index}",
"last_seen_label": "**Последний раз замечен** : {timestamp}{warning}",
"comp_label": "**Состав**: {notation}",
"no_players_recorded": "Нет зафиксированных игроков.",
"limit_reached_title": "Лимит составов достигнут",
"limit_reached_desc": "Этот сервер использовал все {limit} запросов составов для этого таймслота. Подпишитесь (через /unlock) для безлимитного доступа или дождитесь следующего таймслота.",
"remaining_footer": "{remaining}/{limit} запросов составов осталось в этом таймслоте"
},
"quick_log": {
"invalid_type": "Тип может быть только Логи, Очки, Таблица лидеров, Еженедельный BR или Все.",
"squadron_required": "Для типов Логи, Очки или Оба необходимо указать название полка.",
"wildcard_logs_only": "Только тип Логи поддерживает универсальный полк.",
"squadron_not_resolved": "Не удалось определить полк `{squadron}`.",
"save_failed": "Не удалось сохранить настройки. Попробуйте позже.",
"premium_warning": "\n\n> ⚠️ **Игровые логи требуют Premium.** Запустите `/unlock` для подписки ($2.99/мес.) — логи не будут публиковаться до её активации.",
"leaderboard_set": "Оповещение глобального рейтинга установлено для этого канала.",
"both_set": "Оповещения о логах и очках для {squadron} установлены для этого канала.{premium_note}",
"alarm_set": "Оповещение {alarm_type} для {squadron} установлено для этого канала.{premium_note}",
"weekly_br_wildcard_set": "Еженедельный отчёт BR (топ-20 полков) настроен на этот канал. Отправляется в конце каждой ротации BR.",
"weekly_br_squadron_set": "Еженедельный отчёт BR для {squadron} (топ-15 игроков) настроен на этот канал. Отправляется в конце каждой ротации BR."
},
"diagnostics": {
"title": "Диагностика автологирования",
"channel_permissions_header": "**Права канала** (<#{channel_id}>)",
"perms_needed": " ^ Автологирование требует всех указанных прав для отправки таблиц результатов.",
"server_squadron_header": "**Полк сервера** (`/set-squadron`)",
"server_squadron_short": " Короткое: `{short}`",
"server_squadron_long": " Длинное: `{long}`",
"server_squadron_not_set": " Не задано (цвет полосы таблицы результатов будет 'not_set')",
"autolog_prefs_header": "**Настройки автологирования** (`/quick-log`)",
"autolog_none_configured": " ❌ Ничего НЕ настроено — автологирование НЕ будет отправлять ничего на этот сервер.",
"autolog_setup_hint": " Используйте `/quick-log <squadron_short> Logs` в нужном канале для настройки.",
"autolog_no_logs_channels": " ❌ Каналы для логов не настроены. Найдены только каналы для Очков/Рейтинга.",
"autolog_enable_hint": " Используйте `/quick-log <squadron_short> Logs` для включения автологирования.",
"selected_channel_tag": " **(выбранный канал)**",
"missing_send_attach": " (отсутствуют права отправки/прикрепления)",
"channel_not_found": " (канал не найден)",
"invalid_channel_id": " (некорректный ID канала)",
"premium_status_header": "**Статус Premium** (`/unlock`)",
"premium_active": " ✅ На этом сервере активна подписка Premium.",
"premium_not_subscribed": " ❌ На этом сервере **нет** подписки Premium.",
"premium_autolog_required": " Автологирование требует Premium. Используйте `/unlock` для подписки.",
"premium_not_subscribed_free": " ⚪ Подписка отсутствует — используйте `/unlock` для подписки ($2.99/мес.).",
"premium_free_note": " *(Автологи сейчас бесплатны для всех серверов.)*"
},
"sq_info": {
"title": "Информация о полке: {squadron}",
"placement_field": "Место",
"total_points_field": "Всего очков",
"total_members_field": "Всего участников",
"members_field": "Участники",
"fetch_failed": "Не удалось получить информацию о полке."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Сезон {season})",
"embed_title": "{squadron} — Состав полка",
"embed_desc": "Сезон **{season}** · Медиана боёв: **{median}** · Костяк: **{core}** · Активные: **{active}** · Слабые: **{weak}**\nСтолбцы по убыванию боёв; высота = винрейт. Костяк = ≥ медианы и WR ≥ 1,5× WR полка. Слабые = меньше медианы или WR < WR полка ÷ 2. Активные = все остальные.",
"core_threshold_line": "КОСТЯК ≥ {wr} %",
"weak_threshold_line": "СЛАБЫЕ < {wr} %",
"y_label": "Винрейт",
"core_header": "КОСТЯК — {count} · WR {avg}%",
"active_header": "АКТИВНЫЕ — {count} · WR {avg}%",
"weak_header": "СЛАБЫЕ — {count} · WR {avg}%",
"no_active_season": "Активный сезон не найден. Попробуйте позже, когда начнётся следующий.",
"no_members": "Текущие участники для {squadron} не найдены."
},
"recap_card": {
"unknown_season": "Неизвестный сезон: `{season}`.",
"no_clan_id": "Не удалось определить ID полка `{squadron}`.",
"render_failed": "Не удалось сгенерировать сезонную карточку. Попробуйте позже."
},
"sq_stats": {
"no_data_title": "Нет данных",
"no_data_desc": "Исторические данные для полка {squadron} не найдены",
"title": "{squadron} // ПОЛК",
"desc": "Тренд общего счёта (последние {count} точек данных)",
"previous_score_field": "Предыдущий счёт",
"current_score_field": "Текущий счёт",
"change_field": "Изменение",
"player_title": "{squadron} // ИГРОКИ",
"player_desc": "Тренды очков отдельных игроков",
"comparison_title": "{squadron} // СРАВНЕНИЕ РЕЙТИНГА",
"comparison_desc": "Сравнение с полками на позициях {range}",
"current_position_field": "Текущая позиция",
"squadrons_shown_field": "Полков отображено",
"squadron_not_found_error": "Полк не найден в рейтинге",
"no_nearby_error": "Соседние полки не найдены",
"no_historical_error": "Исторические данные для соседних полков не найдены",
"comparison_chart_failed": "Не удалось построить сравнительный график",
"select_players_placeholder": "Выберите игроков (Стр. {page})"
},
"loss_calc": {
"title": "Потеря очков — {squadron}",
"players_leaving_field": "Уходящие игроки",
"share_of_total_field": "% от общего",
"points_lost_real_field": "Потеряно очков (реальных)",
"points_lost_raw_field": "Потеряно очков (сырых)",
"squadron_rating_field": "Рейтинг полка",
"squadron_position_field": "Позиция полка",
"positions_lost_field": "Потеряно позиций",
"not_found_footer": "Не найдено в полке: {players}",
"fetch_failed": "Не удалось получить данные полка: {error}",
"no_point_data": "Данные об очках для этого полка недоступны.",
"no_matching_players": "В полке **{squadron}** не найдено подходящих игроков."
},
"player": {
"select_player_placeholder": "Выберите игрока",
"no_stats_found": "❌ Статистика не найдена для UID: {uid}",
"no_vehicle_stats": "❌ Статистика техники для этого игрока не найдена.",
"vehicles_found": "Найдено **{count}** единиц техники для **{nick}**\nВыберите технику для просмотра подробной статистики:",
"vehicle_select_placeholder": "Выберите технику (Стр. {page}/{total})",
"combat_stats_header": "**__БОЕВАЯ СТАТИСТИКА__**",
"ground_kills_label": "**Наземные уничтожения:** {value}",
"air_kills_label": "**Воздушные уничтожения:** {value}",
"total_kills_label": "**Всего уничтожений:** {value}",
"assists_label": "**Помощь:** {value}",
"deaths_label": "**Гибели:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Захваты:** {value}",
"battle_record_header": "**__БОЕВОЙ СЧЁТ__**",
"total_battles_label": "**Всего боёв:** {value}",
"wins_label": "**Победы:** {value}",
"losses_label": "**Поражения:** {value}",
"win_rate_label": "**Процент побед:** {value}%",
"stats_desc": "Статистика **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Игрок не найден",
"not_found_desc": "История игр для `{player}` не найдена.",
"no_players_found": "Игроки, соответствующие **{username}**, не найдены\nПопробуйте использовать `/website` для поиска на сайте.",
"multiple_matches": "Найдено несколько совпадений, выберите нужное ниже:",
"must_provide_input": "Необходимо указать хотя бы UID или никнейм."
},
"player_games": {
"no_recent_title": "Нет недавних игр",
"no_recent_desc": "Игры для **{player}** за последние 8 часов не найдены.",
"squadron_label": "**Полк:** {squadron}",
"record_label": "**П:** {wins} **Пор:** {losses} **ПП:** {wr}%",
"comps_played_header": "\n\n**Сыгранные составы**"
},
"match": {
"missing_input_title": "Отсутствуют данные",
"missing_input_desc": "Укажите `match_id` или `player_name`.",
"not_found_title": "Матч не найден",
"not_found_desc": "Не удалось найти матч с ID `{match_id}`.",
"invalid_data_title": "Некорректные данные матча",
"invalid_data_desc": "Не удалось обработать данные реплея.",
"scoreboard_error_title": "Ошибка таблицы результатов",
"scoreboard_error_desc": "Не удалось создать изображение таблицы результатов.",
"no_games_title": "Игры не найдены",
"no_games_desc": "История игр для **{player}** не найдена.",
"recent_matches_title": "Недавние матчи: {player}",
"recent_matches_desc": "Отображается до {count} недавних игр. Выберите одну для просмотра полной таблицы результатов.",
"select_match_placeholder": "Выберите матч для просмотра..."
},
"compare": {
"no_players_found": "Игроки, соответствующие **{name}**, не найдены.",
"multiple_matches": "Найдено несколько совпадений для **{name}**: {matches}\nИспользуйте более точное имя (предложения автодополнения точны).",
"could_not_resolve": "Не удалось определить игроков.",
"could_not_fetch": "❌ Не удалось получить статистику для **{name}**.",
"no_graph_data": "Данные за последние 90 дней недоступны.",
"no_squadron_points_data": "Нет данных об очках полка для {names} (игрок не найден в истории отслеживаемых полков).",
"graph_title": "Очки игрока — последние 90 дней",
"battles_label": "Бои",
"wins_label": "Победы",
"losses_label": "Поражения",
"win_rate_label": "Процент побед",
"ground_kills_label": "Наземные уничтожения",
"air_kills_label": "Воздушные уничтожения",
"total_kills_label": "Всего уничтожений",
"assists_label": "Помощь",
"deaths_label": "Гибели",
"kd_label": "K/D",
"captures_label": "Захваты"
},
"squadron": {
"not_found_desc": "Полк `{squadron}` не найден.",
"set_title": "✅ Полк установлен",
"set_desc": "Полк **{squadron}** установлен для этого сервера.",
"short_name_field": "Короткое название",
"long_name_field": "Длинное название",
"swap_title": "✅ Полк заменён",
"swap_desc": "Заменено **{old}** на **{new}** для этого сервера.",
"already_set_title": "⚠️ Полк уже установлен",
"already_set_desc": "На этом сервере сейчас установлено **{old}**.\nЗаменить на **{new}**?",
"swap_cancelled": "❌ Смена полка отменена."
},
"setup": {
"step1_title": "Настройка сервера — Шаг 1 из 3",
"step1_desc": "Этот мастер поможет настроить бота для вашего сервера.\n\n**Шаг 1** — Установите полк\n**Шаг 2** — Выберите канал для логов\n**Шаг 3** — Выберите канал для очков\n",
"step1_current_sq": "\nТекущий настроенный полк: **[{short}] {long}**",
"step2_title": "Настройка сервера — Шаг 2 из 3",
"step2_desc": "Полк установлен: **[{short}] {long}**.\n\nКуда публиковать **боевые логи**?\nВыберите текстовый канал ниже или пропустите этот шаг.",
"step3_title": "Настройка сервера — Шаг 3 из 3",
"step3_desc": "Куда публиковать **уведомления об очках**?\nВыберите текстовый канал ниже или пропустите этот шаг.",
"step3_same_as_logs": "\n\nВы также можете нажать «Как в логах» для использования канала логов.",
"summary_title": "Настройка завершена",
"summary_desc": "Вы можете использовать `/autolog-management` для изменения этих настроек позже.",
"squadron_field": "Полк",
"logs_channel_field": "Канал логов",
"points_channel_field": "Канал очков",
"premium_required_field": "⚠️ Игровые логи требуют Premium",
"premium_required_value": "Автоматические таблицы результатов не будут публиковаться, пока у сервера нет активной подписки. Запустите `/unlock` для подписки ($2.99/мес.).",
"modal_title": "Установить полк",
"modal_label": "Короткое название полка",
"modal_placeholder": "напр. AXYS",
"squadron_not_found": "Полк `{squadron}` не найден. Попробуйте снова.",
"logs_channel_placeholder": "Выберите канал для логов...",
"points_channel_placeholder": "Выберите канал для очков..."
},
"meta_management": {
"squadron_not_found_title": "❌ Полк не найден",
"squadron_not_found_desc": "Не удалось найти ID клана для полка: **{squadron}**",
"access_denied_title": "❌ Доступ запрещён",
"access_denied_desc": "Неверный пароль. Мета-данные этого полка защищены.",
"data_locked_title": "🔐 Данные полка привязаны",
"data_locked_desc": "**{squadron}** имеет включённую привязку данных и не может быть перенесён на другой сервер.\n\nВладелец полка должен отключить **Привязку данных полка** перед переносом.",
"error_retrieving_settings": "❌ Ошибка получения настроек сервера после переноса. Попробуйте снова.",
"error_retrieving_settings_retry": "❌ Ошибка получения настроек сервера. Попробуйте запустить команду снова.",
"authenticated_title": "✅ Аутентификация успешна",
"authenticated_desc": "Пароль подтверждён. Управление настройками **{squadron}**.",
"claimed_title": "✅ Полк закреплён",
"claimed_desc": "**{squadron}** успешно закреплён за этим сервером!",
"password_requirement_field": "🔒 Требование пароля",
"data_lock_field": "🔐 Привязка данных полка",
"public_meta_field": "👥 Публичный доступ к мета",
"access_password_field": "🔑 Пароль доступа",
"enabled_value": "✅ Включено",
"disabled_value": "❌ Отключено",
"settings_title": "🔐 Настройки управления мета",
"settings_desc": "**Полк:** {squadron}\n**ID клана:** {clan_id}",
"first_time_title": "🔐 Управление мета — Первоначальная настройка",
"first_time_owner_desc": "**Полк:** {squadron}\n**ID клана:** {clan_id}\n\n🔑 Ваш пароль доступа сгенерирован. **Сохраните этот пароль** — он потребуется для аутентификации при доступе к мета-данным в будущем.\n\n**Пароль:** `{password}`",
"first_time_non_owner_desc": "**Полк:** {squadron}\n**ID клана:** {clan_id}\n\nПолк настроен. Запросите пароль доступа у владельца сервера.",
"settings_field": "Настройки",
"settings_hint": "Используйте кнопки ниже для настройки прав доступа.",
"password_toggled": "✅ Требование пароля: **{state}**",
"lock_toggled": "✅ Привязка данных полка: **{state}**",
"public_meta_toggled": "✅ Публичный доступ к мета: **{state}**\n{detail}",
"public_meta_enabled_detail": "Теперь не-администраторы могут использовать команду `/meta`.",
"public_meta_disabled_detail": "Команду `/meta` могут использовать только администраторы.",
"owner_only_password": "❌ Только владелец сервера может изменить пароль полка.",
"help_title": "📖 Справка по управлению мета",
"help_desc": "Описание каждой настройки и функции:",
"help_password_field": "🔑 Пароль доступа",
"help_password_value": "Пароль доступа вашего полка. Пароль в панели настроек виден только **владельцу сервера**. Любой, у кого есть пароль, может закрепить мета-данные вашего полка на своём сервере, поэтому храните его в безопасности.",
"help_require_field": "🔒 Требовать пароль",
"help_require_value": "При включении даже администраторы этого сервера должны вводить пароль полка для доступа к `/meta-management`. Добавляет дополнительный уровень защиты от случайных изменений.",
"help_lock_field": "🔐 Привязка данных полка",
"help_lock_value": "При включении привязывает данные полка к этому серверу, предотвращая перенос даже при наличии правильного пароля. Необходимо отключить перед переносом полка.",
"help_public_field": "👥 Разрешить публичную мета",
"help_public_value": "При включении позволяет участникам без прав администратора использовать команду `/meta` для поиска техники полка. При отключении команду `/meta` могут использовать только администраторы сервера.",
"help_accounts_field": "📋 Обновить мета-аккаунты",
"help_accounts_value": "Открывает менеджер ростера игроков, где можно добавлять или удалять игроков из мета-ростера полка. Используйте **Обновить всех участников** для синхронизации всего полка сразу.",
"help_change_pw_field": "🔑 Изменить пароль",
"help_change_pw_value": "**Только для владельца сервера.** Изменить пароль доступа полка и задать необязательную подсказку. Подсказка отображается в запросе пароля для помощи в его запоминании.",
"password_modal_title": "Пароль доступа к полку",
"password_modal_label": "Введите пароль полка",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Изменить пароль полка",
"current_password_label": "Текущий пароль",
"current_password_placeholder": "Введите текущий пароль",
"new_password_label": "Новый пароль",
"new_password_placeholder": "Введите новый пароль",
"confirm_password_label": "Подтвердите новый пароль",
"confirm_password_placeholder": "Повторно введите новый пароль",
"hint_label": "Подсказка к паролю (необязательно)",
"hint_placeholder": "Подсказка для запоминания пароля",
"pw_incorrect": "❌ Текущий пароль неверен.",
"pw_mismatch": "❌ Новые пароли не совпадают. Попробуйте снова.",
"pw_empty": "❌ Новый пароль не может быть пустым.",
"pw_changed": "✅ Пароль для **{squadron}** успешно изменён.\n**Новый пароль:** `{password}`",
"pw_changed_hint": "\n**Подсказка:** {hint}",
"player_add_modal_title": "Добавить игрока в мета-ростер",
"player_add_label": "UID или никнейм игрока",
"player_add_placeholder": "Введите UID (напр., 12345678) или никнейм игрока",
"player_not_found": "❌ Игрок `{player}` не найден в базе данных Players_Global.\n",
"roster_title": "📋 Управление мета-ростером — {squadron}",
"roster_desc": "**ID клана полка:** {clan_id}\n**Всего игроков:** {count}",
"roster_page_field": "Игроки (Стр. {page}/{total})",
"no_players_field": "Нет игроков",
"no_players_hint": "В мета-ростер ещё не добавлено ни одного игрока. Нажмите **Добавить игрока**, чтобы начать.",
"remove_player_placeholder": "Выберите игрока для удаления...",
"fetch_members_failed": "❌ Не удалось получить список участников полка: {error}",
"no_members_found": "❌ Участники полка не найдены или API-запрос не выполнен.",
"roster_synced": "✅ Ростер синхронизирован с полком.",
"roster_added": "**+{count}** добавлено",
"roster_removed": "**-{count}** удалено (покинули полк)",
"roster_up_to_date": "**{count}** уже актуальны",
"refreshing_vehicles": "Обновление данных о технике в фоновом режиме..."
},
"meta": {
"not_configured": "❌ Мета-данные не настроены для этого сервера. Сначала запустите `/meta-management`.",
"no_permission": "❌ Для использования этой команды необходимы права администратора.\nАдминистраторы могут включить публичный доступ через `/meta-management`.",
"no_results": "❌ Ни один игрок в ростере вашего полка не имеет **{vehicle}**.",
"no_results_admin_hint": "\n*Ожидаете, что кто-то должен иметь это? Нажмите кнопку обновления участников в `/meta-management` и проверьте снова.*",
"search_title": "🔍 Результаты поиска — {vehicle}",
"matches_found": "**Найдено совпадений:** {count} игрок(ов)",
"spawns_label": "Появления",
"deaths_label": "Гибели",
"gk_label": "НУ",
"ak_label": "ВУ",
"points_label": "Очки",
"kdr_label": "K/D",
"games_label": "Бои",
"no_points": "—"
},
"top": {
"title": "**Топ 20 полков**",
"rating_label": "**Рейтинг:** {value}",
"air_kills_label": "**Воздушные уничтожения:** {value}",
"ground_kills_label": "**Наземные уничтожения:** {value}",
"deaths_label": "**Гибели:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Процент побед:** {value}",
"playtime_label": "**Время в игре:** {value}",
"fetch_failed": "Не удалось получить данные о полках."
},
"analytics": {
"no_data_title": "Нет данных",
"no_matches_desc": "Матчи не найдены.",
"no_comp_desc": "Данные о составах не найдены.",
"no_consistency_desc": "Недостаточно данных об игроках (минимум 50 матчей).",
"no_time_desc": "Данные о времени не найдены.",
"unknown_view": "Неизвестный вид.",
"map_title": "Процент побед по картам: {squadron}",
"comp_title": "Составы команд: {squadron}",
"consistency_title": "Стабильность игроков: {squadron}",
"consistency_desc": "Отсортировано по K/D",
"time_title": "Результативность по времени суток: {squadron}",
"eu_timeslot": "\n**EU Timeslot**",
"na_timeslot": "\n**NA Timeslot**",
"off_peak": "\n**Off-Peak**",
"matchups_title": "📜 {squadron} — История Противостояний",
"matchups_won_field": "🏆 Чаще Всего Побеждали",
"matchups_lost_field": "💀 Чаще Всего Проигрывали",
"no_matchups_desc": "Нет записанных матчей против других полков."
},
"recent": {
"title": "Недавние матчи: {squadron}",
"no_matches_desc": "Матчи для этого полка не найдены."
},
"h2h": {
"two_required_title": "Необходимо два полка",
"two_required_desc": "Укажите хотя бы один полк или используйте `/set-squadron` и укажите соперника.",
"provide_a_desc": "Укажите `squadron_a` или сначала используйте `/set-squadron`.",
"provide_b_desc": "Укажите `squadron_b` или сначала используйте `/set-squadron`.",
"squadron_not_found_title": "Полк не найден",
"same_squadron_title": "Одинаковый полк",
"same_squadron_desc": "Нельзя проверить статистику встреч против самих себя.",
"record_desc": "**Счёт:** {a_wins}П - {b_wins}Пор ({total} игр)",
"no_matches_desc": "Нет зафиксированных матчей между **{a}** и **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Активно — автологирование включено для этого сервера.",
"premium_not_subscribed_line": "❌ **Premium:** Подписка отсутствует — используйте `/unlock` для включения автологирования.",
"premium_free_line": "⚪ **Premium:** Подписка отсутствует — используйте `/unlock` для подписки ($2.99/мес.). *(Автологи сейчас бесплатны для всех серверов.)*",
"what_to_do": "\n\nЧто вы хотите сделать?",
"select_notif_type": "Выберите тип уведомления для управления:",
"select_notif_placeholder": "Выберите тип уведомления",
"logs_option": "Логи",
"logs_option_desc": "Управление уведомлениями о логах",
"points_option": "Очки",
"points_option_desc": "Управление уведомлениями об очках",
"leaderboard_option": "Рейтинг",
"leaderboard_option_desc": "Управление уведомлениями о рейтинге",
"selected_type": "Выбрано **{type}**. Теперь выберите полк для управления:",
"select_squadron_placeholder": "Выберите полк",
"select_squadron_page_placeholder": "Выберите полк (Стр. {page})",
"no_squadrons_available": "Нет доступных полков для этого типа уведомлений.",
"managing_global": "Управление **{type}** (глобально) в канале **{channel}**.",
"managing_squadron": "Управление **{type}** для полка **{squadron}** в канале **{channel}**.",
"select_channel": "Выберите новый канал:",
"select_channel_placeholder": "Выберите канал",
"select_channel_page_placeholder": "Выберите канал (Стр. {page})",
"global_toggled": "{type} (глобально) теперь {state}.",
"squadron_toggled": "{type} для **{squadron}** теперь {state}.",
"channel_updated_global": "Обновлён {type} (глобально) — канал {channel}",
"channel_updated_squadron": "Обновлён {type} для **{squadron}** — канал {channel}",
"diagnose_channel_placeholder": "Выберите канал для диагностики...",
"select_channel_diagnose": "Выберите канал для диагностики:",
"game_not_logged_title": "Игра не залогирована",
"game_not_logged_desc": "Используйте `/unlock`, чтобы оформить подписку уровня **Standard** (или выше) и получать автоматические таблицы результатов.",
"server_not_upgraded_title": "⚠️ Сервер не обновлён",
"server_not_upgraded_autolog_desc": "На этом сервере нет активной подписки Premium.\n\n**Автоматические таблицы результатов перестанут отправляться на серверы без обновления после <t:{deadline}:D>.**\n\nИспользуйте `/unlock` для подписки и продолжения получения автоматических игровых логов.",
"replay_not_available": "Данные реплея пока недоступны — подождите немного и попробуйте снова!",
"too_many_videos": "Сейчас обрабатывается слишком много видео — попробуйте снова через мгновение.",
"video_gen_failed": "Ошибка создания видео: `{error}`",
"video_missing": "Не удалось создать видео реплея — выходной файл отсутствует или пуст.",
"video_too_large": "Видео реплея слишком большое для загрузки ({file_mb:.1f} МБ). Лимит сервера: {limit_mb:.0f} МБ.",
"video_web_fallback": "Вы также можете просмотреть этот матч на {url}",
"video_upload_failed": "Видео слишком большое для загрузки — просмотрите его на сайте:\n{url}",
"video_unexpected_error": "Неожиданная ошибка при создании видео реплея: `{error}`",
"replay_not_found": "Данные реплея для сессии `{session_id}` не найдены на диске.",
"chat_log_title": "**Лог чата для игры [{session_id}]({url})**",
"chat_log_part_title": "**Лог чата для игры [{session_id}]({url}) (Часть {part}/{total})**",
"chat_log_part_only": "**Лог чата (Часть {part}/{total})**",
"no_chat_log": "Лог чата для сессии `{session_id}` не найден.",
"chat_log_error": "Неожиданная ошибка при загрузке лога чата: `{error}`",
"battle_log_title": "**Боевой лог для игры [{session_id}]({url})**",
"battle_log_part_title": "**Боевой лог для игры [{session_id}]({url}) (Часть {part}/{total})**",
"battle_log_part_only": "**Боевой лог (Часть {part}/{total})**",
"no_battle_log": "Боевые события для сессии `{session_id}` не найдены.",
"battle_log_error": "Неожиданная ошибка при загрузке боевого лога: `{error}`",
"points_update_title": "**{squadron} {region} Обновление очков**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Изменения игроков:**",
"points_table_header": "Имя Изменение Теперь\n",
"wl_line": "\n**{squadron}** сыграл **{wins}П-{losses}Пор** за эту сессию",
"placement_rose": "\n**{squadron}** поднялся на **{new_place}** с **{old_place}**",
"placement_fell": "\n**{squadron}** опустился на **{new_place}** с **{old_place}**",
"points_not_logged_title": "Очки не залогированы",
"points_not_logged_desc": "Используйте `/unlock`, чтобы оформить подписку уровня **Standard** (или выше) и получать автоматические обновления очков.",
"server_not_upgraded_points_desc": "На этом сервере нет активной подписки Premium.\n\n**Автоматические обновления перестанут отправляться на серверы без обновления после <t:{deadline}:D>.**\n\nИспользуйте `/unlock` для подписки и продолжения получения автоматических обновлений.",
"leave_title": "⚠️ Игрок покинул {squadron}",
"leave_desc": "**{nick}** ({uid}) покинул полк.\n\nПоследние зафиксированные очки: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Полк превышает лимит вашего тарифа",
"over_cap_desc": "Ваш сервер на тарифе **{tier}**, который разрешает **{cap}** полков для **{notif}**. Полк **{squadron}** сейчас сверх лимита и не логируется. Перейдите на более высокий тариф, чтобы восстановить.",
"over_cap_footer": "Повысить тариф: srebot-meow.ing/premium или /unlock",
"wildcard_blocked_title": "Для подстановочных символов нужен тариф выше",
"wildcard_blocked_desc": "Подстановочные полки (*, all, everything) доступны только на тарифах Pro и Max. Ваш сервер на **{tier}** для {notif}. Повысьте тариф, чтобы включить их.",
"cap_header": "{used}/{cap} {notif} включено — тариф {tier}"
},
"track": {
"squadron_not_found": "Полк не найден.",
"fetch_failed": "Не удалось получить информацию о полке."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Разблокируйте премиум-функции для этого сервера.**\n\nPremium включает:\n> • Автоматическая публикация таблиц результатов\n> • Логи чата и боя\n> • Просмотр реплеев\n> • Безлимитные запросы /comp\n> • Приоритетная поддержка\n\n**$2.99 / месяц · за сервер · отмена в любое время**\n\n⚠️ Биллинг Discord доступен только в отдельных странах. Если кнопка ниже показывает **«Продукт недоступен»**, это может быть связано с неподдерживаемой страной или использованием **мобильного устройства**. Используйте вместо этого кнопку **Подписаться через сайт**.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Этот сервер уже подписан!**",
"manage_discord_field": "Управление подпиской",
"manage_discord_value": "Ваша подписка оформлена через **Discord**.\nДля отмены перейдите в **Настройки пользователя → Подписки** в Discord.",
"manage_website_field": "Управление подпиской",
"manage_website_value": "Ваша подписка оформлена через **сайт**.\nУправляйте ей на [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Скоро",
"coming_soon_value": "Подписки Premium пока недоступны. Загляните позже!",
"current_tier": "Ваш текущий тариф: **{tier}**.",
"upgrade_to": "Повысить до {tier}",
"upgrade_to_value": "Увеличьте лимит полков и получите больше функций с тарифом **{tier}**."
},
"language": {
"prompt": "Пожалуйста, выберите язык сервера:",
"select_placeholder": "Выберите язык сервера",
"language_set": "Язык установлен: {language}.",
"translate_prompt": "Выберите целевой язык ниже 👇",
"translate_placeholder": "Выберите целевой язык…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Перевод недоступен (DeepL не настроен)",
"translation_failed": "Ошибка перевода"
},
"misc": {
"credits_title": "Авторы",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Ведущий разработчик, менеджер бота, менеджер сообщества\n> **Z3R0** - Разработчик, разработчик оптимизации, инженер баз данных\n> **Clippii (Heidi)** - Разработчик, веб-разработчик, менеджер сообщества\n> **LivingTheDagor** - Разработчик, разработчик парсера, консультант\n> **Lux_** - API-инженер, разработчик Spectra\n> **Konigallerwaffen** - Консультант по обратной связи и функциям\n> **Žralok Tonda** - Чешский переводчик\n> **Styevy**, **Lopais** - Немецкие переводчики\n> **Susogus**, **playforfun698** - Польские переводчики\n> **Bobr** - Русский переводчик\n\n\n[Хотите присоединиться к нам?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "РАСПИСАНИЕ СЕЗОНА",
"schedule_not_found_title": "Расписание не найдено",
"schedule_not_found_desc": "Данные расписания пока недоступны.",
"news_no_news_title": "Нет новостей",
"news_no_news_desc": "Объявлений пока нет. Загляните позже!",
"news_footer": "Спасибо за вашу поддержку! ᖙᘘᗢ",
"help_title": "Руководство по боту",
"donate_title": "Поддержать SRE Bot",
"donate_desc": "Если вам нравится SRE Bot и вы хотите поддержать его развитие, угостите меня кофе!\n\n**[Пожертвовать на Ko-fi](https://ko-fi.com/notsotoothless)**\n\nКаждый взнос помогает поддерживать работу бота и развивать новые функции. Спасибо!",
"status_title": "Статус бота",
"status_last_received": "Последняя полученная игра",
"status_avg_ttl": "Среднее TTL (последние 30)",
"status_no_data": "Пока нет данных",
"status_gaijin_slow": "⚠️ Серверы Gaijin медленные",
"help_commands_header": "**Обзор команд**",
"help_links": "Подробности в документации [здесь]({docs}) или поддержка [здесь]({support}).",
"help_terms": "[Условия использования]({terms}) • [Политика конфиденциальности]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Рейтинг полков",
"top15_desc": "Топ 15 полков со статистикой, публикуется через 35 минут после закрытия таймслота.\nЭто сообщение отправлено <t:{timestamp}:R>.",
"top30_desc": "Полки 16-30 со статистикой.",
"not_logged_title": "Рейтинг не залогирован",
"not_logged_desc": "Используйте `/unlock`, чтобы оформить подписку уровня **Standard** (или выше) и получать автоматические обновления рейтинга.",
"server_not_upgraded_title": "⚠️ Сервер не обновлён",
"server_not_upgraded_desc": "На этом сервере нет активной подписки Premium.\n\n**Автоматические обновления перестанут отправляться на серверы без обновления после <t:{deadline}:D>.**\n\nИспользуйте `/unlock` для подписки и продолжения получения автоматических обновлений."
},
"stacks": {
"stack_title": "Стак {leader}",
"stack_named_title": "{name}",
"no_members": "Пока нет участников.",
"members_field": "Участники ({count}/{max})",
"queue_field": "Очередь ({count}/{max})",
"manage_title": "Управление стаком",
"no_pending_requests": "Нет ожидающих заявок.",
"disbanded_title": "Стак [Распущен]",
"disbanded_desc": "Этот стак был распущен лидером.",
"expired_title": "Стак [Истёк]",
"expired_desc": "Этот стак истёк.",
"join_modal_title": "Заявка на вступление в стак",
"join_vehicle_label": "На чём будете играть?",
"join_vehicle_placeholder": "напр. F-16C, WZ305...",
"ping_modal_title": "Сообщение пинга",
"ping_message_label": "Своё сообщение (необязательно)",
"ping_message_placeholder": "напр. Заходите! Стак начинается!",
"rename_modal_title": "Переименовать стак",
"rename_label": "Название стака",
"rename_placeholder": "напр. Ночные совы, Альфа отряд...",
"select_new_leader": "Выберите нового лидера…",
"select_applicants": "Выберите кандидатов…",
"no_pending_applications": "Нет ожидающих заявок.",
"select_to_remove": "Выберите людей для удаления…",
"no_members_or_applicants": "Нет участников или кандидатов.",
"select_to_ping": "Выберите людей для индивидуального пинга…",
"stack_not_found": "❌ Стак не найден.",
"no_longer_exists": "❌ Этот стак больше не существует.",
"member_not_exists": "❌ Этот участник больше не существует.",
"already_has_stack": "❌ У этого игрока уже есть активный стак.",
"already_member": "❌ Вы уже участник этого стака.",
"already_applied": "❌ У вас уже есть ожидающая заявка в этот стак.",
"queue_full": "❌ Очередь заполнена ({max}/{max}). Попробуйте позже.",
"application_sent": "✅ Заявка отправлена! Лидер стака её рассмотрит.",
"stack_disbanded": "✅ Стак распущен.",
"cancelled": "Отменено.",
"select_member_transfer": "❌ Выберите участника для передачи руководства.",
"ownership_transferred": "✅ Руководство передано {nick}. Вы покинули стак.",
"select_applicant_first": "❌ Сначала выберите хотя бы одного кандидата.",
"stack_full": "❌ Стак уже заполнен ({max}/{max} участников).",
"select_person_first": "❌ Сначала выберите хотя бы одного человека.",
"no_one_to_ping": "❌ Некого пинговать.",
"ping_footer": "Пинг от {leader} для {stack}.",
"pinged": "✅ Пинг отправлен!",
"select_from_dropdown": "❌ Сначала выберите хотя бы одного человека из выпадающего меню.",
"stack_renamed": "✅ Стак переименован в **{name}**.",
"only_member_use_disband": "❌ Вы единственный участник. Используйте **Распустить стак** для завершения.",
"select_transfer_prompt": "Выберите участника для передачи руководства перед уходом:",
"left_stack": "✅ Вы покинули стак.",
"application_withdrawn": "✅ Ваша заявка отозвана.",
"not_member_or_applicant": "❌ Вы не являетесь участником или кандидатом этого стака.",
"leader_only_manage": "❌ Только лидер стака может им управлять.",
"leader_only_disband": "❌ Только лидер стака может его распустить.",
"confirm_disband": "Вы уверены, что хотите распустить этот стак? Это действие нельзя отменить.",
"already_active_stack": "⚠️ У вас уже есть активный стак. Если исходное сообщение исчезло (напр. после перезапуска бота), вы можете принудительно распустить его и начать заново.",
"force_created": "✅ Предыдущий стак распущен. Новый стак создан.",
"no_active_stack": "❌ У вас нет активного стака. Используйте `/stack-create` для создания.",
"could_not_parse_channel": "⚠️ Не удалось обработать сохранённый ID канала."
},
"commands": {
"common": {
"season": "Сезон для создания карточки",
"theme": "Цветовая тема карточки",
"squadron_short": "Короткое название эскадрильи",
"player_username": "Имя игрока",
"choice_dark": "Тёмная",
"choice_light": "Светлая"
},
"comp": {
"description": "Найти последние известные составы команды",
"squadron_short": "Короткое название команды противника"
},
"quick_log": {
"description": "Настроить уведомление для этой эскадрильи в этом канале",
"squadron_name": "КОРОТКОЕ название эскадрильи для отслеживания",
"type": "Выберите Логи, Очки, Таблица лидеров, Еженедельный BR или Все",
"choice_logs": "Logs",
"choice_points": "Очки",
"choice_leaderboard": "Рейтинг",
"choice_both": "Оба (Logs + Очки)",
"choice_weekly_br": "Еженедельный BR"
},
"sq_info": {
"description": "Получить информацию об эскадрилье"
},
"sq_info_graph": {
"description": "Показать график состава полка по активности и винрейту (текущий сезон)"
},
"sq_card": {
"description": "Создать сезонную карточку эскадрильи",
"squadron": "Короткое название эскадрильи"
},
"sq_stats": {
"description": "Показать очки эскадрильи во времени"
},
"loss_calculator": {
"description": "Рассчитать потерю очков, если игроки уйдут из эскадрильи",
"player1": "Уходящий игрок",
"player_optional": "Уходящий игрок (необязательно)"
},
"website": {
"description": "Получить ссылку на сайт SRE Bot"
},
"card": {
"description": "Создать сезонную карточку игрока"
},
"player_stats": {
"description": "Показать подробную статистику техники игрока",
"username": "Имя WT для запроса статистики",
"uid": "UID WT для запроса статистики"
},
"view_player_games": {
"description": "Показать последние 20 игр игрока"
},
"view_match": {
"description": "Показать таблицу матча по ID или игроку",
"match_id": "Hex ID сессии матча",
"player_name": "Игрок для просмотра недавних матчей"
},
"compare": {
"description": "Сравнить суммарную SQB-статистику игроков",
"player1": "Первый игрок",
"player2": "Второй игрок",
"player_optional": "Дополнительный игрок (необязательно)"
},
"leaderboard": {
"description": "Открыть глобальный рейтинг SRE Bot"
},
"set_squadron": {
"description": "Задать тег эскадрильи для этого сервера",
"abbreviated_name": "Короткое название эскадрильи"
},
"setup": {
"description": "Настроить бота для этого сервера"
},
"meta_management": {
"description": "Управлять доступом к мета-данным этого сервера"
},
"meta": {
"description": "Искать мета-ростер по названию техники",
"vehicle": "Название техники для поиска"
},
"top": {
"description": "Показать топ-20 эскадрилий с подробной статистикой"
},
"language": {
"description": "Изменить язык бота."
},
"translate_message": {
"name": "Перевести сообщение"
},
"sq_track": {
"description": "Отслеживать эскадрилью и сравнить с прошлой проверкой",
"squadron_short_name": "Короткое название эскадрильи"
},
"analytics": {
"description": "Показать расширенную SQB-аналитику эскадрильи",
"view": "Какой вид аналитики показать",
"choice_maps": "Винрейт по картам",
"choice_comps": "Составы команд",
"choice_consistency": "Стабильность игроков",
"choice_time": "Время суток",
"choice_matchups": "История встреч"
},
"recent": {
"description": "Показать недавние бои эскадрильи",
"length": "Количество матчей"
},
"vs": {
"description": "Личная статистика двух эскадрилий",
"squadron_a": "Первая эскадрилья",
"squadron_b": "Вторая эскадрилья"
},
"autolog_management": {
"description": "Управлять autolog-уведомлениями и проверять права"
},
"diagnose_perms": {
"description": "Проверить права autolog в этом канале"
},
"unlock": {
"description": "Открыть Premium-функции для этого сервера"
},
"credits": {
"description": "Показать команду, создавшую проект"
},
"schedule": {
"description": "Показать текущий сезонный BR-график"
},
"news": {
"description": "Показать последние новости и объявления SRE Bot"
},
"help": {
"description": "Показать руководство, ToS и ссылки поддержки"
},
"donate": {
"description": "Поддержать разработку SRE Bot"
},
"stack_create": {
"description": "Создать стак игроков",
"vehicle": "На какой технике начнёте?"
},
"stack_manage": {
"description": "Повторно отправить активный стак в этот канал"
},
"bot_status": {
"description": "Статус бота: последняя полученная игра и среднее TTL"
}
},
"permission": {
"blacklisted_title": "❌ В чёрном списке",
"blacklisted_desc": "Вы не можете использовать эту команду.",
"reason_line": "**Причина:** {reason}",
"access_denied_title": "⛔ Доступ запрещён",
"no_permission_desc": "У вас нет прав для использования этой команды.",
"unexpected_error_title": "❗ Ошибка, сообщите о ней...."
},
"weekly_br": {
"title_wildcard": "Еженедельный отчёт BR — {br} BR",
"title_squadron": "Еженедельный отчёт BR — [{tag}] {long} • {br} BR",
"window_label": "Период: {start} → {end}",
"wildcard_desc_first": "Топ-{count} полков по ELO • Места {low}{high}",
"wildcard_desc_second": "Топ-{count} полков по ELO • Места {low}{high}",
"squadron_stats_line": "- {games} боёв • K/D {kdr} • Побед {wr}%",
"top_players_inline_header": "🥇 Лучшие игроки:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}б)",
"top_players_header": "**Топ-{count} игроков по ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} боёв • K/D {kdr}",
"squadron_header_line": "ELO полка: {score} • {games} боёв • Побед {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO полка: недостаточно активности команды на этой неделе.",
"no_data": "Нет матчей для [{tag}] в этой ротации BR."
}
}
+856
View File
@@ -0,0 +1,856 @@
{
"common": {
"error_title": "Помилка",
"no_data_title": "Немає даних",
"access_denied_title": "Доступ заборонено",
"access_denied_desc": "Цей сервер заблоковано.",
"no_players_selected": "Гравців не вибрано. Будь ласка, виберіть хоча б одного гравця.",
"must_use_in_server": "Ця команда може використовуватись лише на сервері.",
"could_not_resolve_channel": "Не вдалося визначити вибраний канал.",
"failed_update_setting": "❌ Не вдалося оновити налаштування.",
"configuration_not_found": "Конфігурацію не знайдено.",
"no_channel_selected": "Канал не вибрано.",
"no_selection_received": "Нічого не вибрано.",
"database_error": "❌ Помилка бази даних: {error}",
"enabled": "Увімкнено",
"disabled": "Вимкнено",
"not_configured": "Не налаштовано",
"unknown": "Невідомо",
"rating_field": "Рейтинг",
"battles_field": "Бої",
"wins_field": "Перемоги",
"losses_field": "Поразки",
"win_rate_field": "Відсоток перемог",
"kills_field": "Знищення",
"deaths_field": "Загибелі",
"kd_field": "K/D",
"members_field": "Учасники",
"placement_field": "Місце",
"points_field": "Очки",
"ground_kills_field": "Знищення наземних",
"air_kills_field": "Знищення повітряних",
"total_kills_field": "Всього знищень",
"assists_field": "Допомога",
"captures_field": "Захоплення",
"none_option": "Немає"
},
"buttons": {
"skip": "Пропустити",
"previous": "Попередня",
"next": "Наступна",
"prev": "Назад",
"prev_arrow": "◀ Попередня",
"next_arrow": "Наступна ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 Згенерувати графік",
"show_graph": "Показати графік",
"view_player_stats": "📊 Переглянути Статистику Гравців",
"compare_nearby": "📈 Порівняти Сусідні Ескадрильї",
"confirm_swap": "Так, замінити",
"cancel_swap": "Ні, залишити стару",
"set_squadron": "Встановити Ескадрилью",
"same_as_logs": "Як канал логів",
"require_password": "🔒 Вимагати Пароль",
"password_required": "🔒 Пароль Обов'язковий",
"lock_data": "🔐 Прив'язати Дані Ескадрильї",
"data_locked": "🔐 Дані Прив'язані до Сервера",
"allow_public": "👥 Дозволити Публічний Мета",
"public_enabled": "👥 Публічний Мета Увімкнено",
"update_accounts": "📋 Оновити Мета Акаунти",
"change_password": "🔑 Змінити Пароль",
"help": "❓ Допомога",
"add_player": "➕ Додати Гравця",
"update_all": "🔄 Оновити Всіх Учасників",
"back_to_settings": "⬅ Назад до Налаштувань",
"manage_notifications": "Керувати Сповіщеннями",
"diagnose_permissions": "Діагностувати Дозволи",
"enable": "Увімкнути",
"disable": "Вимкнути",
"change_channel": "Змінити Канал",
"view_replay": "Переглянути Повтор",
"view_website": "Переглянути на Сайті",
"view_video": "Переглянути Відео",
"view_log": "Переглянути Лог",
"view_chat": "Переглянути Чат",
"subscribe_website": "Підписатись через Сайт",
"yes_disband": "Так, розпустити",
"cancel": "Скасувати",
"transfer_leave": "Передати і вийти",
"accept_selected": "Прийняти обраних",
"accept_all": "Прийняти всіх",
"decline_selected": "Відхилити обраних",
"back": "Назад",
"remove_all": "Видалити всіх",
"remove_active": "Видалити активних",
"remove_queued": "Видалити в черзі",
"remove_selected": "Видалити обраних",
"ping_all": "Пінг всіх",
"ping_active": "Пінг активних",
"ping_queued": "Пінг у черзі",
"ping_selected": "Пінг обраних",
"accept_members": "Прийняти учасників",
"remove_members": "Видалити учасників",
"ping_members": "Пінг учасників",
"rename_stack": "Перейменувати стак",
"request_to_join": "Запит на вступ",
"leave_withdraw": "Вийти / Відкликати",
"manage_stack": "Керувати стаком ⚙️",
"disband_stack": "Розпустити стак",
"force_disband_create": "Примусово розпустити і створити новий"
},
"events": {
"guild_join_title": "Дякуємо за додавання!",
"guild_join_desc": "Виконайте `/setup`, щоб налаштувати бота для цього сервера."
},
"comp": {
"not_found_title": "Склади не знайдено",
"not_found_desc": "Немає даних для **{squadron}**, спробуйте пізніше.",
"error_loading_title": "Помилка завантаження складів",
"error_loading_desc": "Не вдалося завантажити дані складу: {error}",
"title": "Склади для {squadron}",
"desc": "Склади, помічені за останні {minutes} хвилин",
"no_recent_title": "Немає нещодавніх складів",
"no_recent_desc": "Немає складів за останні {minutes} хвилин.",
"comp_title": "СКЛАД {index}",
"last_seen_label": "**Останній раз помічено**: {timestamp}{warning}",
"comp_label": "**Склад**: {notation}",
"no_players_recorded": "Гравців не зафіксовано.",
"limit_reached_title": "Ліміт складів досягнуто",
"limit_reached_desc": "Цей сервер використав усі {limit} запитів складів для цього таймслоту. Підпишіться (через /unlock) для безлімітного доступу або зачекайте наступного таймслоту.",
"remaining_footer": "{remaining}/{limit} запитів складів залишилось у цьому таймслоті"
},
"quick_log": {
"invalid_type": "Тип може бути тільки Логи, Очки, Таблиця лідерів, Тижневий BR або Усі.",
"squadron_required": "Необхідно вказати назву ескадрильї для сигналів Логів, Очків або Обох.",
"wildcard_logs_only": "Лише Логи можна налаштувати на довільну ескадрилью.",
"squadron_not_resolved": "Ескадрилью `{squadron}` не вдалося знайти.",
"save_failed": "Не вдалося зберегти налаштування. Будь ласка, спробуйте пізніше.",
"premium_warning": "\n\n> ⚠️ **Ігрові логи потребують Premium.** Виконайте `/unlock` для підписки ($2.99/міс) — логи не будуть надсилатися до цього.",
"leaderboard_set": "Сигнал глобальної таблиці лідерів налаштовано на цей канал.",
"both_set": "Сигнали логів та очків для {squadron} налаштовано на цей канал.{premium_note}",
"alarm_set": "Сигнал {alarm_type} для {squadron} налаштовано на цей канал.{premium_note}",
"weekly_br_wildcard_set": "Тижневий звіт BR (топ-20 полків) налаштовано на цей канал. Надсилається в кінці кожної ротації BR.",
"weekly_br_squadron_set": "Тижневий звіт BR для {squadron} (топ-15 гравців) налаштовано на цей канал. Надсилається в кінці кожної ротації BR."
},
"diagnostics": {
"title": "Діагностика автологів",
"channel_permissions_header": "**Дозволи Каналу** (<#{channel_id}>)",
"perms_needed": " ^ Автологування потребує всіх перерахованих дозволів для надсилання таблиць результатів.",
"server_squadron_header": "**Ескадрилья Сервера** (`/set-squadron`)",
"server_squadron_short": " Скорочена: `{short}`",
"server_squadron_long": " Повна: `{long}`",
"server_squadron_not_set": " Не налаштовано (колір рядка таблиці відображатиметься як 'not_set')",
"autolog_prefs_header": "**Налаштування Автологів** (`/quick-log`)",
"autolog_none_configured": " ❌ НІЧОГО не налаштовано — автологування НЕ надсилатиме нічого на цей сервер.",
"autolog_setup_hint": " Використайте `/quick-log <squadron_short> Logs` у цільовому каналі для налаштування.",
"autolog_no_logs_channels": " ❌ Канали логів не налаштовано. Знайдено лише канали Очків/Таблиці Лідерів.",
"autolog_enable_hint": " Використайте `/quick-log <squadron_short> Logs` для увімкнення автологування.",
"selected_channel_tag": " **(вибраний канал)**",
"missing_send_attach": " (бракує дозволів надсилання/прикріплення)",
"channel_not_found": " (канал не знайдено)",
"invalid_channel_id": " (невірний ID каналу)",
"premium_status_header": "**Статус Premium** (`/unlock`)",
"premium_active": " ✅ Цей сервер має активну підписку Premium.",
"premium_not_subscribed": " ❌ Цей сервер **не має** підписки Premium.",
"premium_autolog_required": " Автологування потребує Premium. Використайте `/unlock` для підписки.",
"premium_not_subscribed_free": " ⚫ Немає підписки — використайте `/unlock` для підписки ($2.99/міс).",
"premium_free_note": " *(Автологи зараз безкоштовні для всіх серверів.)*"
},
"sq_info": {
"title": "Інформація про Ескадрилью: {squadron}",
"placement_field": "Місце",
"total_points_field": "Всього очків",
"total_members_field": "Всього учасників",
"members_field": "Учасники",
"fetch_failed": "Не вдалося отримати інформацію про ескадрилью."
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO (Сезон {season})",
"embed_title": "{squadron} — Склад ескадрильї",
"embed_desc": "Сезон **{season}** · Медіана боїв: **{median}** · Кістяк: **{core}** · Активні: **{active}** · Слабкі: **{weak}**\nСтовпці за спаданням боїв; висота = відсоток перемог. Кістяк = ≥ медіани і WR ≥ 1,5× WR ескадрильї. Слабкі = менше медіани або WR < WR ескадрильї ÷ 2. Активні = всі інші.",
"core_threshold_line": "КІСТЯК ≥ {wr} %",
"weak_threshold_line": "СЛАБКІ < {wr} %",
"y_label": "Відсоток перемог",
"core_header": "КІСТЯК — {count} · WR {avg}%",
"active_header": "АКТИВНІ — {count} · WR {avg}%",
"weak_header": "СЛАБКІ — {count} · WR {avg}%",
"no_active_season": "Активний сезон не знайдено. Спробуйте пізніше, коли почнеться наступний.",
"no_members": "Поточних учасників для {squadron} не знайдено."
},
"recap_card": {
"unknown_season": "Невідомий сезон: `{season}`.",
"no_clan_id": "Не вдалося визначити ID ескадрильї `{squadron}`.",
"render_failed": "Не вдалося згенерувати сезонну картку. Спробуйте пізніше."
},
"sq_stats": {
"no_data_title": "Немає даних",
"no_data_desc": "Не знайдено історичних даних для ескадрильї: {squadron}",
"title": "{squadron} // ЕСКАДРИЛЬЯ",
"desc": "Тенденція Загального Рахунку (Останні {count} точок даних)",
"previous_score_field": "Попередній Рахунок",
"current_score_field": "Поточний Рахунок",
"change_field": "Зміна",
"player_title": "{squadron} // ГРАВЦІ",
"player_desc": "Тенденції очків окремих гравців",
"comparison_title": "{squadron} // ПОРІВНЯННЯ ТАБЛИЦІ ЛІДЕРІВ",
"comparison_desc": "Порівняння з ескадрильями, що займають місця {range}",
"current_position_field": "Поточна Позиція",
"squadrons_shown_field": "Показано Ескадрилей",
"squadron_not_found_error": "Ескадрилью не знайдено в таблиці лідерів",
"no_nearby_error": "Не знайдено сусідніх ескадрилей",
"no_historical_error": "Не знайдено історичних даних для сусідніх ескадрилей",
"comparison_chart_failed": "Не вдалося згенерувати графік порівняння",
"select_players_placeholder": "Виберіть гравців (Сторінка {page})"
},
"loss_calc": {
"title": "Втрата Очків — {squadron}",
"players_leaving_field": "Гравці, що Виходять",
"share_of_total_field": "% від Загального",
"points_lost_real_field": "Втрачено Очків (Реально)",
"points_lost_raw_field": "Втрачено Очків (Сирих)",
"squadron_rating_field": "Рейтинг Ескадрильї",
"squadron_position_field": "Позиція Ескадрильї",
"positions_lost_field": "Втрачено Позицій",
"not_found_footer": "Не знайдено в ескадрильї: {players}",
"fetch_failed": "Не вдалося отримати дані ескадрильї: {error}",
"no_point_data": "Немає даних про очки для цієї ескадрильї.",
"no_matching_players": "Не знайдено відповідних гравців у **{squadron}**."
},
"player": {
"select_player_placeholder": "Виберіть гравця",
"no_stats_found": "❌ Статистику не знайдено для UID: {uid}",
"no_vehicle_stats": "❌ Статистику техніки не знайдено для цього гравця.",
"vehicles_found": "Знайдено **{count}** одиниць техніки для **{nick}**\nВиберіть техніку для перегляду детальної статистики:",
"vehicle_select_placeholder": "Виберіть техніку (Сторінка {page}/{total})",
"combat_stats_header": "**__БОЙОВА СТАТИСТИКА__**",
"ground_kills_label": "**Знищення Наземних:** {value}",
"air_kills_label": "**Знищення Повітряних:** {value}",
"total_kills_label": "**Всього Знищень:** {value}",
"assists_label": "**Допомога:** {value}",
"deaths_label": "**Загибелі:** {value}",
"kd_label": "**K/D:** {value}",
"captures_label": "**Захоплення:** {value}",
"battle_record_header": "**__БОЙОВИЙ ЗАПИС__**",
"total_battles_label": "**Всього Боїв:** {value}",
"wins_label": "**Перемоги:** {value}",
"losses_label": "**Поразки:** {value}",
"win_rate_label": "**Відсоток Перемог:** {value}%",
"stats_desc": "Статистика для **{nick}** (**{squadron}**)\nUID: `{uid}`",
"not_found_title": "Гравця Не Знайдено",
"not_found_desc": "Не знайдено ігрової історії для `{player}`.",
"no_players_found": "Не знайдено гравців, що відповідають **{username}**\nСпробуйте скористатися `/website` для пошуку на сайті.",
"multiple_matches": "Знайдено декілька збігів, виберіть правильний нижче:",
"must_provide_input": "Необхідно вказати хоча б UID або ім'я користувача."
},
"player_games": {
"no_recent_title": "Немає Нещодавніх Ігор",
"no_recent_desc": "Не знайдено ігор для **{player}** за останні 8 годин.",
"squadron_label": "**Ескадрилья:** {squadron}",
"record_label": "**П:** {wins} **Пор:** {losses} **ВП:** {wr}%",
"comps_played_header": "\n\n**Зіграні Склади**"
},
"match": {
"missing_input_title": "Відсутні Вхідні Дані",
"missing_input_desc": "Вкажіть `match_id` або `player_name`.",
"not_found_title": "Матч Не Знайдено",
"not_found_desc": "Не вдалося знайти матч з ID `{match_id}`.",
"invalid_data_title": "Невірні Дані Матчу",
"invalid_data_desc": "Не вдалося розібрати дані повтору.",
"scoreboard_error_title": "Помилка Таблиці Результатів",
"scoreboard_error_desc": "Не вдалося згенерувати зображення таблиці результатів.",
"no_games_title": "Ігор Не Знайдено",
"no_games_desc": "Не знайдено ігрової історії для **{player}**.",
"recent_matches_title": "Нещодавні матчі для {player}",
"recent_matches_desc": "Показано до {count} нещодавніх ігор. Виберіть одну для перегляду повної таблиці результатів.",
"select_match_placeholder": "Виберіть матч для перегляду..."
},
"compare": {
"no_players_found": "Не знайдено гравців, що відповідають **{name}**.",
"multiple_matches": "Декілька збігів для **{name}**: {matches}\nБудь ласка, використайте точніше ім'я (підказки автодоповнення є точними).",
"could_not_resolve": "Не вдалося визначити гравців.",
"could_not_fetch": "❌ Не вдалося отримати статистику для **{name}**.",
"no_graph_data": "Немає даних за останні 90 днів.",
"no_squadron_points_data": "Немає даних про очки ескадрильї для {names} (гравця не знайдено в відстежуваній історії ескадрильї).",
"graph_title": "Очки Гравця — Останні 90 Днів",
"battles_label": "Бої",
"wins_label": "Перемоги",
"losses_label": "Поразки",
"win_rate_label": "Відсоток Перемог",
"ground_kills_label": "Знищення Наземних",
"air_kills_label": "Знищення Повітряних",
"total_kills_label": "Всього Знищень",
"assists_label": "Допомога",
"deaths_label": "Загибелі",
"kd_label": "K/D",
"captures_label": "Захоплення"
},
"squadron": {
"not_found_desc": "Ескадрилью `{squadron}` не знайдено.",
"set_title": "✅ Ескадрилью Встановлено",
"set_desc": "Ескадрилью **{squadron}** встановлено для цього сервера.",
"short_name_field": "Скорочена Назва",
"long_name_field": "Повна Назва",
"swap_title": "✅ Ескадрилью Замінено",
"swap_desc": "Замінено **{old}** на **{new}** для цього сервера.",
"already_set_title": "⚠️ Ескадрилью Вже Встановлено",
"already_set_desc": "Наразі для цього сервера встановлено **{old}**.\nЗамінити на **{new}**?",
"swap_cancelled": "❌ Зміну ескадрильї скасовано."
},
"setup": {
"step1_title": "Налаштування Сервера — Крок 1 з 3",
"step1_desc": "Цей майстер допоможе вам налаштувати бота для вашого сервера.\n\n**Крок 1** — Встановіть вашу ескадрилью\n**Крок 2** — Виберіть канал логів\n**Крок 3** — Виберіть канал очків\n",
"step1_current_sq": "\nПоточна налаштована ескадрилья: **[{short}] {long}**",
"step2_title": "Налаштування Сервера — Крок 2 з 3",
"step2_desc": "Ескадрилью встановлено на **[{short}] {long}**.\n\nКуди надсилати **ігрові логи**?\nВиберіть текстовий канал нижче або пропустіть цей крок.",
"step3_title": "Налаштування Сервера — Крок 3 з 3",
"step3_desc": "Куди надсилати **сповіщення про очки**?\nВиберіть текстовий канал нижче або пропустіть цей крок.",
"step3_same_as_logs": "\n\nТакож можна натиснути «Як канал логів», щоб використати той самий канал.",
"summary_title": "Налаштування Завершено",
"summary_desc": "Ви можете використовувати `/autolog-management` для зміни цих налаштувань пізніше.",
"squadron_field": "Ескадрилья",
"logs_channel_field": "Канал Логів",
"points_channel_field": "Канал Очків",
"premium_required_field": "⚠️ Ігрові Логи Потребують Premium",
"premium_required_value": "Автоматичні таблиці результатів не надсилатимуться, поки цей сервер не матиме активної підписки. Виконайте `/unlock` для підписки ($2.99/міс).",
"modal_title": "Встановити Ескадрилью",
"modal_label": "Скорочена Назва Ескадрильї",
"modal_placeholder": "наприклад, AXYS",
"squadron_not_found": "Ескадрилью `{squadron}` не знайдено. Будь ласка, спробуйте знову.",
"logs_channel_placeholder": "Виберіть канал логів...",
"points_channel_placeholder": "Виберіть канал очків..."
},
"meta_management": {
"squadron_not_found_title": "❌ Ескадрилью Не Знайдено",
"squadron_not_found_desc": "Не вдалося знайти ID клану для ескадрильї: **{squadron}**",
"access_denied_title": "❌ Доступ Заборонено",
"access_denied_desc": "Невірний пароль. Мета-дані цієї ескадрильї захищено.",
"data_locked_title": "🔐 Дані Ескадрильї Прив'язано",
"data_locked_desc": "**{squadron}** має увімкнену прив'язку даних і не може бути передана на інший сервер.\n\nВласник ескадрильї повинен вимкнути **Прив'язку Даних Ескадрильї** перед переміщенням.",
"error_retrieving_settings": "❌ Помилка отримання налаштувань сервера після передачі. Будь ласка, спробуйте знову.",
"error_retrieving_settings_retry": "❌ Помилка отримання налаштувань сервера. Будь ласка, запустіть команду знову.",
"authenticated_title": "✅ Автентифіковано",
"authenticated_desc": "Пароль підтверджено. Керування налаштуваннями для **{squadron}**.",
"claimed_title": "✅ Ескадрилью закріплено",
"claimed_desc": "**{squadron}** успішно закріплено за цим сервером!",
"password_requirement_field": "🔒 Вимога Пароля",
"data_lock_field": "🔐 Прив'язка Даних Ескадрильї",
"public_meta_field": "👥 Публічний Доступ до Мета",
"access_password_field": "🔑 Пароль Доступу",
"enabled_value": "✅ Увімкнено",
"disabled_value": "❌ Вимкнено",
"settings_title": "🔐 Налаштування Керування Мета",
"settings_desc": "**Ескадрилья:** {squadron}\n**ID Клану:** {clan_id}",
"first_time_title": "🔐 Керування Мета - Початкове Налаштування",
"first_time_owner_desc": "**Ескадрилья:** {squadron}\n**ID Клану:** {clan_id}\n\n🔑 Ваш пароль доступу було згенеровано. **Збережіть цей пароль** — він знадобиться для автентифікації доступу до мета-даних у майбутньому.\n\n**Пароль:** `{password}`",
"first_time_non_owner_desc": "**Ескадрилья:** {squadron}\n**ID Клану:** {clan_id}\n\nЕскадрилью налаштовано. Попросіть власника сервера надати пароль доступу.",
"settings_field": "Налаштування",
"settings_hint": "Використайте кнопки нижче для налаштування параметрів доступу.",
"password_toggled": "✅ Вимога пароля: **{state}**",
"lock_toggled": "✅ Прив'язка даних ескадрильї: **{state}**",
"public_meta_toggled": "✅ Публічний доступ до мета: **{state}**\n{detail}",
"public_meta_enabled_detail": "Не-адміністратори тепер можуть використовувати команду `/meta`.",
"public_meta_disabled_detail": "Лише адміністратори можуть використовувати команду `/meta`.",
"owner_only_password": "❌ Лише власник сервера може змінити пароль ескадрильї.",
"help_title": "📖 Довідка з Керування Мета",
"help_desc": "Пояснення кожного налаштування та функції:",
"help_password_field": "🔑 Пароль Доступу",
"help_password_value": "Пароль доступу вашої ескадрильї. Лише **власник сервера** може бачити пароль на панелі налаштувань. Будь-хто з паролем може захопити мета-дані вашої ескадрильї на своєму сервері, тому зберігайте його в безпеці.",
"help_require_field": "🔒 Вимагати Пароль",
"help_require_value": "Якщо увімкнено, навіть адміністратори цього сервера повинні вводити пароль ескадрильї для доступу до `/meta-management`. Забезпечує додатковий рівень захисту від випадкових змін.",
"help_lock_field": "🔐 Прив'язка Даних Ескадрильї",
"help_lock_value": "Якщо увімкнено, прив'язує дані ескадрильї до цього серверу, забороняючи передачу навіть з правильним паролем. Необхідно вимкнути перед передачею ескадрильї.",
"help_public_field": "👥 Дозволити Публічний Мета",
"help_public_value": "Якщо увімкнено, дозволяє не-адміністраторам використовувати команду `/meta` для пошуку техніки ескадрильї. Якщо вимкнено, лише адміністратори сервера можуть використовувати `/meta`.",
"help_accounts_field": "📋 Оновити Мета Акаунти",
"help_accounts_value": "Відкриває менеджер списку гравців, де можна додавати або видаляти гравців із мета-ростеру ескадрильї. Використайте **Оновити Всіх Учасників** для синхронізації всієї ескадрильї одразу.",
"help_change_pw_field": "🔑 Змінити Пароль",
"help_change_pw_value": "**Лише для власника сервера.** Змінює пароль доступу ескадрильї та встановлює необов'язкову підказку. Підказка відображається у запиті пароля для допомоги з пам'яттю.",
"password_modal_title": "Пароль Доступу до Ескадрильї",
"password_modal_label": "Введіть Пароль Ескадрильї",
"password_modal_placeholder": "XXXX-XXXX-XXXX",
"change_pw_modal_title": "Змінити Пароль Ескадрильї",
"current_password_label": "Поточний Пароль",
"current_password_placeholder": "Введіть ваш поточний пароль",
"new_password_label": "Новий Пароль",
"new_password_placeholder": "Введіть ваш новий пароль",
"confirm_password_label": "Підтвердіть Новий Пароль",
"confirm_password_placeholder": "Повторно введіть ваш новий пароль",
"hint_label": "Підказка до Пароля (Необов'язково)",
"hint_placeholder": "Підказка для запам'ятовування пароля",
"pw_incorrect": "❌ Поточний пароль невірний.",
"pw_mismatch": "❌ Нові паролі не збігаються. Будь ласка, спробуйте знову.",
"pw_empty": "❌ Новий пароль не може бути порожнім.",
"pw_changed": "✅ Пароль успішно оновлено для **{squadron}**.\n**Новий Пароль:** `{password}`",
"pw_changed_hint": "\n**Підказка:** {hint}",
"player_add_modal_title": "Додати Гравця до Мета Ростеру",
"player_add_label": "UID або Нікнейм Гравця",
"player_add_placeholder": "Введіть UID гравця (наприклад, 12345678) або нікнейм",
"player_not_found": "❌ Гравця `{player}` не знайдено в базі даних Players_Global.\n",
"roster_title": "📋 Керування Мета Ростером - {squadron}",
"roster_desc": "**ID Клану Ескадрильї:** {clan_id}\n**Всього Гравців:** {count}",
"roster_page_field": "Гравці (Сторінка {page}/{total})",
"no_players_field": "Немає Гравців",
"no_players_hint": "Гравців ще не додано до мета ростеру. Натисніть **Додати Гравця** для початку.",
"remove_player_placeholder": "Виберіть гравця для видалення...",
"fetch_members_failed": "❌ Не вдалося отримати учасників ескадрильї: {error}",
"no_members_found": "❌ Учасників ескадрильї не знайдено або виклик API зазнав невдачі.",
"roster_synced": "✅ Ростер синхронізовано з ескадрильєю.",
"roster_added": "**+{count}** додано",
"roster_removed": "**-{count}** видалено (покинули ескадрилью)",
"roster_up_to_date": "**{count}** вже актуально",
"refreshing_vehicles": "Оновлення даних техніки у фоновому режимі..."
},
"meta": {
"not_configured": "❌ Мета-дані не налаштовано для цього сервера. Спочатку виконайте `/meta-management`.",
"no_permission": "❌ Для використання цієї команди потрібні права адміністратора.\nАдміністратори можуть увімкнути публічний доступ через `/meta-management`.",
"no_results": "❌ Жоден гравець у ростері вашої ескадрильї не має **{vehicle}**.",
"no_results_admin_hint": "\n*Очікуєте, що хтось має це? Натисніть кнопку оновлення учасників у `/meta-management` та перевірте ще раз.*",
"search_title": "🔍 Результати Пошуку - {vehicle}",
"matches_found": "**Знайдено Збігів:** {count} гравець(ів)",
"spawns_label": "Спауни",
"deaths_label": "Загибелі",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "Очки",
"kdr_label": "K/D",
"games_label": "Бої",
"no_points": "—"
},
"top": {
"title": "**Топ 20 Ескадрилей**",
"rating_label": "**Рейтинг:** {value}",
"air_kills_label": "**Знищення Повітряних:** {value}",
"ground_kills_label": "**Знищення Наземних:** {value}",
"deaths_label": "**Загибелі:** {value}",
"kd_label": "**K/D:** {value}",
"win_rate_label": "**Відсоток Перемог:** {value}",
"playtime_label": "**Час Гри:** {value}",
"fetch_failed": "Не вдалося отримати дані ескадрильї."
},
"analytics": {
"no_data_title": "Немає даних",
"no_matches_desc": "Матчів не знайдено.",
"no_comp_desc": "Дані про склади не знайдено.",
"no_consistency_desc": "Недостатньо даних гравців (мінімум 50 матчів).",
"no_time_desc": "Дані про час не знайдено.",
"unknown_view": "Невідомий режим.",
"map_title": "Відсоток Перемог на Картах: {squadron}",
"comp_title": "Склади Команди: {squadron}",
"consistency_title": "Стабільність Гравців: {squadron}",
"consistency_desc": "Відсортовано за K/D",
"time_title": "Результативність за Часом Доби: {squadron}",
"eu_timeslot": "\n**Часовий Слот ЄС**",
"na_timeslot": "\n**Часовий Слот ПА**",
"off_peak": "\n**Поза Піком**",
"matchups_title": "📜 {squadron} — Історія Протистоянь",
"matchups_won_field": "🏆 Найбільше Перемог Проти",
"matchups_lost_field": "💀 Найбільше Поразок Від",
"no_matchups_desc": "Немає записаних матчів проти інших полків."
},
"recent": {
"title": "Нещодавні Матчі: {squadron}",
"no_matches_desc": "Для цієї ескадрильї матчів не знайдено."
},
"h2h": {
"two_required_title": "Потрібні Дві Ескадрильї",
"two_required_desc": "Вкажіть хоча б одну ескадрилью або використайте `/set-squadron` та вкажіть суперника.",
"provide_a_desc": "Вкажіть `squadron_a` або спочатку використайте `/set-squadron`.",
"provide_b_desc": "Вкажіть `squadron_b` або спочатку використайте `/set-squadron`.",
"squadron_not_found_title": "Ескадрилью Не Знайдено",
"same_squadron_title": "Однакові Ескадрильї",
"same_squadron_desc": "Не можна порівнювати ескадрилью з самою собою.",
"record_desc": "**Рахунок:** {a_wins}П - {b_wins}Пор ({total} ігор)",
"no_matches_desc": "Немає зафіксованих матчів між **{a}** та **{b}**."
},
"autolog": {
"premium_active_line": "✅ **Premium:** Активно — автологування увімкнено для цього сервера.",
"premium_not_subscribed_line": "❌ **Premium:** Немає підписки — використайте `/unlock` для увімкнення автологування.",
"premium_free_line": "⚫ **Premium:** Немає підписки — використайте `/unlock` для підписки ($2.99/міс). *(Автологи зараз безкоштовні для всіх серверів.)*",
"what_to_do": "\n\nЩо ви хочете зробити?",
"select_notif_type": "Виберіть тип сповіщення для керування:",
"select_notif_placeholder": "Виберіть тип сповіщення",
"logs_option": "Логи",
"logs_option_desc": "Керувати сповіщеннями Логів",
"points_option": "Очки",
"points_option_desc": "Керувати сповіщеннями Очків",
"leaderboard_option": "Таблиця Лідерів",
"leaderboard_option_desc": "Керувати сповіщеннями Таблиці Лідерів",
"selected_type": "Вибрано **{type}**. Тепер виберіть ескадрилью для керування:",
"select_squadron_placeholder": "Виберіть ескадрилью",
"select_squadron_page_placeholder": "Виберіть ескадрилью (Сторінка {page})",
"no_squadrons_available": "Для цього типу сповіщення немає доступних ескадрилей.",
"managing_global": "Керування **{type}** (глобально) в каналі **{channel}**.",
"managing_squadron": "Керування **{type}** для ескадрильї **{squadron}** в каналі **{channel}**.",
"select_channel": "Виберіть новий канал:",
"select_channel_placeholder": "Виберіть канал",
"select_channel_page_placeholder": "Виберіть канал (Сторінка {page})",
"global_toggled": "{type} (глобально) тепер {state}.",
"squadron_toggled": "{type} для **{squadron}** тепер {state}.",
"channel_updated_global": "Оновлено {type} (глобально) на {channel}",
"channel_updated_squadron": "Оновлено {type} для **{squadron}** на {channel}",
"diagnose_channel_placeholder": "Виберіть канал для діагностики...",
"select_channel_diagnose": "Виберіть канал для діагностики:",
"game_not_logged_title": "Гру Не Зафіксовано",
"game_not_logged_desc": "Виконайте `/unlock`, щоб оформити підписку рівня **Standard** (або вище) та отримувати автоматичні таблиці результатів.",
"server_not_upgraded_title": "⚠️ Сервер Не Оновлено",
"server_not_upgraded_autolog_desc": "Цей сервер не має активної підписки Premium.\n\n**Автоматичні таблиці результатів перестануть надсилатись на не оновлені сервери після <t:{deadline}:D>.**\n\nВиконайте `/unlock` для підписки та продовжуйте отримувати автоматичні ігрові логи.",
"replay_not_available": "Дані повтору ще недоступні — почекайте трохи і спробуйте знову!",
"too_many_videos": "Зараз рендериться забагато відео — будь ласка, спробуйте за мить.",
"video_gen_failed": "Помилка генерації відео: `{error}`",
"video_missing": "Не вдалося згенерувати відео повтору — вихідний файл відсутній або порожній.",
"video_too_large": "Відео повтору занадто велике для завантаження ({file_mb:.1f} МБ). Ліміт сервера — {limit_mb:.0f} МБ.",
"video_web_fallback": "Також можна переглянути цей матч за адресою {url}",
"video_upload_failed": "Відео занадто велике для завантаження — перегляньте на сайті:\n{url}",
"video_unexpected_error": "Непередбачена помилка при генерації відео повтору: `{error}`",
"replay_not_found": "Дані повтору для сесії `{session_id}` не знайдено на диску.",
"chat_log_title": "**Лог Чату для Гри [{session_id}]({url})**",
"chat_log_part_title": "**Лог Чату для Гри [{session_id}]({url}) (Частина {part}/{total})**",
"chat_log_part_only": "**Лог Чату (Частина {part}/{total})**",
"no_chat_log": "Лог чату для сесії `{session_id}` не знайдено.",
"chat_log_error": "Непередбачена помилка при завантаженні лога чату: `{error}`",
"battle_log_title": "**Бойовий Лог для Гри [{session_id}]({url})**",
"battle_log_part_title": "**Бойовий Лог для Гри [{session_id}]({url}) (Частина {part}/{total})**",
"battle_log_part_only": "**Бойовий Лог (Частина {part}/{total})**",
"no_battle_log": "Бойових подій для сесії `{session_id}` не знайдено.",
"battle_log_error": "Непередбачена помилка при завантаженні бойового лога: `{error}`",
"points_update_title": "**{squadron} {region} Оновлення Очків**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**Зміни Гравців:**",
"points_table_header": "Ім'я Зміна Тепер\n",
"wl_line": "\n**{squadron}** зіграла **{wins}П-{losses}Пор** цієї сесії",
"placement_rose": "\n**{squadron}** піднявся на **{new_place}** з **{old_place}**",
"placement_fell": "\n**{squadron}** опустився на **{new_place}** з **{old_place}**",
"points_not_logged_title": "Очки Не Зафіксовано",
"points_not_logged_desc": "Виконайте `/unlock`, щоб оформити підписку рівня **Standard** (або вище) та отримувати автоматичні оновлення очків.",
"server_not_upgraded_points_desc": "Цей сервер не має активної підписки Premium.\n\n**Автоматичні оновлення перестануть надсилатись на не оновлені сервери після <t:{deadline}:D>.**\n\nВиконайте `/unlock` для підписки та продовжуйте отримувати автоматичні оновлення.",
"leave_title": "⚠️ Гравець Покинув {squadron}",
"leave_desc": "**{nick}** ({uid}) покинув ескадрилью.\n\nОстанні зафіксовані очки: **{points}**",
"no_squadrons_desc": "No squadrons configured",
"no_channels_desc": "No channels available",
"over_cap_title": "Полк перевищує ліміт вашого тарифу",
"over_cap_desc": "Ваш сервер на тарифі **{tier}**, який дозволяє **{cap} {notif}** полків. Полк **{squadron}** зараз перевищує ліміт і не логується. Перейдіть на вищий тариф для відновлення.",
"over_cap_footer": "Оновити на srebot-meow.ing/premium або через /unlock",
"wildcard_blocked_title": "Для wildcard потрібен вищий тариф",
"wildcard_blocked_desc": "Wildcard-записи (*, all, everything) доступні лише на тарифах Pro або Max. Ваш сервер на **{tier}** для {notif}. Оновіть, щоб увімкнути.",
"cap_header": "{used}/{cap} {notif} активних — тариф {tier}"
},
"track": {
"squadron_not_found": "Ескадрилью не знайдено.",
"fetch_failed": "Не вдалося отримати інформацію про ескадрилью."
},
"unlock": {
"title": "SRE Bot Premium",
"desc": "**Розблокуйте преміум-функції для цього сервера.**\n\nPremium включає:\n> • Автоматичні публікації таблиць результатів\n> • Логи чату та бою\n> • Перегляд повторів\n> • Безлімітні запити /comp\n> • Пріоритетна підтримка\n\n**$2.99 / місяць · за сервер · скасування у будь-який час**\n\n⚠️ Білінг Discord доступний лише в окремих країнах. Якщо кнопка нижче показує **«Продукт Недоступний»**, це може бути через непідтримувану країну або використання **мобільного пристрою**. Натомість скористайтесь кнопкою **Підписатись через Сайт**.",
"already_subscribed_title": "SRE Bot Premium",
"already_subscribed_desc": "✅ **Цей сервер вже підписаний!**",
"manage_discord_field": "Керувати Підпискою",
"manage_discord_value": "Ваша підписка оформлена через **Discord**.\nДля скасування перейдіть до **Налаштувань Користувача → Підписки** в Discord.",
"manage_website_field": "Керувати Підпискою",
"manage_website_value": "Ваша підписка оформлена через **сайт**.\nКеруйте нею на [whop.com/billing](https://whop.com/billing).",
"coming_soon_field": "Незабаром",
"coming_soon_value": "Підписки Premium ще недоступні. Перевірте пізніше!",
"current_tier": "Ваш поточний тариф: **{tier}**.",
"upgrade_to": "Перейти на {tier}",
"upgrade_to_value": "Більше полків і функцій із тарифом **{tier}**."
},
"language": {
"prompt": "Будь ласка, виберіть мову сервера:",
"select_placeholder": "Оберіть мову сервера",
"language_set": "Мову встановлено на {language}.",
"translate_prompt": "Виберіть цільову мову нижче 👇",
"translate_placeholder": "Оберіть цільову мову…",
"translate_result": "**{author} → {language}:**\n{text}",
"translation_unavailable": "Переклад недоступний (DeepL не налаштовано)",
"translation_failed": "Переклад не вдався"
},
"misc": {
"credits_title": "Подяки",
"credits_desc": "**Meowww**\n\n> **NotSoToothless** - Провідний Розробник, Менеджер Бота, Менеджер Спільноти\n> **Z3R0** - Розробник, Розробник Оптимізації, Інженер Баз Даних\n> **Clippii (Heidi)** - Розробник, Розробник Сайту, Менеджер Спільноти\n> **LivingTheDagor** - Розробник, Розробник Парсера, Консультант\n> **Lux_** - Інженер API, Розробник Spectra\n> **Konigallerwaffen** - Консультант з відгуків та функцій\n> **Žralok Tonda** - Чеський перекладач\n> **Styevy**, **Lopais** - Німецькі перекладачі\n> **Susogus**, **playforfun698** - Польські перекладачі\n> **Bobr** - Російський перекладач\n\n\n[Хочете приєднатися до нас?](https://discord.gg/BCvkK8JhPe)",
"schedule_title": "РОЗКЛАД СЕЗОНУ",
"schedule_not_found_title": "Розклад Не Знайдено",
"schedule_not_found_desc": "Дані розкладу ще недоступні.",
"news_no_news_title": "Немає Новин",
"news_no_news_desc": "Зараз немає оголошень. Перевірте пізніше!",
"news_footer": "Дякуємо за вашу підтримку! ᖙᘚᗢ",
"help_title": "Довідник Бота",
"donate_title": "Підтримати SRE Bot",
"donate_desc": "Якщо вам подобається SRE Bot і ви хочете підтримати його розробку, розгляньте можливість купити мені каву!\n\n**[Донат на Ko-fi](https://ko-fi.com/notsotoothless)**\n\nКожен внесок допомагає підтримувати роботу бота та розробку нових функцій. Дякуємо!",
"status_title": "Статус бота",
"status_last_received": "Остання отримана гра",
"status_avg_ttl": "Середній TTL (останні 30)",
"status_no_data": "Поки немає даних",
"status_gaijin_slow": "⚠️ Сервери Gaijin повільні",
"help_commands_header": "**Огляд команд**",
"help_links": "Деталі в документації [тут]({docs}) або підтримка [тут]({support}).",
"help_terms": "[Умови використання]({terms}) • [Політика конфіденційності]({terms})"
},
"dev": {
"restricted_dev_team": "This command is restricted to the dev team.",
"restricted_bot_owner": "❌ This command is restricted to the bot owner.",
"invalid_server_id": "❌ Invalid server ID. Must be a 17-19 digit Discord server ID.",
"expiry_too_soon": "❌ Expiry timestamp must be at least 1 month from now.\n> Now: <t:{now}:F>\n> Minimum: <t:{min}:F>\n> You provided: <t:{provided}:F>",
"entitlement_write_failed": "❌ Failed to write entitlement: {error}",
"entitlement_created_title": "✅ Manual Entitlement Created",
"entitlement_created_desc": "**Server:** {guild_name} (`{server_id}`)\n**Expires:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**Created:** <t:{now}:F>",
"query_failed": "Query failed: {error}",
"health_title": "Bot Health Dashboard",
"health_uptime": "Uptime",
"health_guilds": "Guilds",
"health_games_processed": "Games Processed",
"health_tasks": "Tasks",
"health_websocket": "WebSocket",
"health_never": "never",
"health_errors": "({count} errors)",
"health_last_msg": "last msg {ago} ({count} total)",
"health_avg_ttl": "Avg TTL (Last 30)",
"entitlements_title": "Active Entitlements ({count} total)",
"entitlements_no_entries": "No entitlements.",
"entitlements_empty_title": "Active Entitlements",
"entitlements_empty_desc": "No active entitlements found.",
"entitlements_tag_discord": "Discord",
"entitlements_tag_whop": "Whop",
"entitlements_tag_manual": "Manual",
"query_prefix": "Query: {name}"
},
"leaderboard_alarm": {
"title": "🏆 Таблиця Лідерів Ескадрилей",
"top15_desc": "Топ 15 ескадрилей зі статистикою, надсилається через 35 хвилин після закриття часового слоту.\nЦе надіслано <t:{timestamp}:R>.",
"top30_desc": "Ескадрильї 16-30 зі статистикою.",
"not_logged_title": "Таблицю Лідерів Не Зафіксовано",
"not_logged_desc": "Виконайте `/unlock`, щоб оформити підписку рівня **Standard** (або вище) та отримувати автоматичні оновлення таблиці лідерів.",
"server_not_upgraded_title": "⚠️ Сервер Не Оновлено",
"server_not_upgraded_desc": "Цей сервер не має активної підписки Premium.\n\n**Автоматичні оновлення перестануть надсилатись на не оновлені сервери після <t:{deadline}:D>.**\n\nВиконайте `/unlock` для підписки та продовжуйте отримувати автоматичні оновлення."
},
"stacks": {
"stack_title": "Стак {leader}",
"stack_named_title": "{name}",
"no_members": "Ще немає учасників.",
"members_field": "Учасники ({count}/{max})",
"queue_field": "Черга ({count}/{max})",
"manage_title": "Керування стаком",
"no_pending_requests": "Немає очікуваних запитів.",
"disbanded_title": "Стак [Розпущений]",
"disbanded_desc": "Цей стак був розпущений лідером.",
"expired_title": "Стак [Закінчився]",
"expired_desc": "Цей стак закінчився.",
"join_modal_title": "Запит на вступ до стаку",
"join_vehicle_label": "На чому будете грати?",
"join_vehicle_placeholder": "напр. F-16C, WZ305...",
"ping_modal_title": "Повідомлення пінгу",
"ping_message_label": "Власне повідомлення (необов'язково)",
"ping_message_placeholder": "напр. Заходьте! Стак починається!",
"rename_modal_title": "Перейменувати стак",
"rename_label": "Назва стаку",
"rename_placeholder": "напр. Нічні сови, Альфа загін...",
"select_new_leader": "Оберіть нового лідера…",
"select_applicants": "Оберіть кандидатів…",
"no_pending_applications": "Немає очікуваних заявок.",
"select_to_remove": "Оберіть людей для видалення…",
"no_members_or_applicants": "Немає учасників або кандидатів.",
"select_to_ping": "Оберіть людей для індивідуального пінгу…",
"stack_not_found": "❌ Стак не знайдено.",
"no_longer_exists": "❌ Цей стак більше не існує.",
"member_not_exists": "❌ Цей учасник більше не існує.",
"already_has_stack": "❌ Цей гравець вже має активний стак.",
"already_member": "❌ Ви вже є учасником цього стаку.",
"already_applied": "❌ У вас вже є очікувана заявка до цього стаку.",
"queue_full": "❌ Черга заповнена ({max}/{max}). Спробуйте пізніше.",
"application_sent": "✅ Заявку надіслано! Лідер стаку її розгляне.",
"stack_disbanded": "✅ Стак розпущено.",
"cancelled": "Скасовано.",
"select_member_transfer": "❌ Оберіть учасника для передачі керівництва.",
"ownership_transferred": "✅ Керівництво передано {nick}. Ви покинули стак.",
"select_applicant_first": "❌ Спочатку оберіть хоча б одного кандидата.",
"stack_full": "❌ Стак вже заповнений ({max}/{max} учасників).",
"select_person_first": "❌ Спочатку оберіть хоча б одну людину.",
"no_one_to_ping": "❌ Нікого пінгувати.",
"ping_footer": "Пінг від {leader} для {stack}.",
"pinged": "✅ Пінг надіслано!",
"select_from_dropdown": "❌ Спочатку оберіть хоча б одну людину з випадаючого меню.",
"stack_renamed": "✅ Стак перейменовано на **{name}**.",
"only_member_use_disband": "❌ Ви єдиний учасник. Використайте **Розпустити стак** для завершення.",
"select_transfer_prompt": "Оберіть учасника для передачі керівництва перед виходом:",
"left_stack": "✅ Ви покинули стак.",
"application_withdrawn": "✅ Вашу заявку відкликано.",
"not_member_or_applicant": "❌ Ви не є учасником чи кандидатом цього стаку.",
"leader_only_manage": "❌ Тільки лідер стаку може ним керувати.",
"leader_only_disband": "❌ Тільки лідер стаку може його розпустити.",
"confirm_disband": "Ви впевнені, що хочете розпустити цей стак? Цю дію неможливо скасувати.",
"already_active_stack": "⚠️ У вас вже є активний стак. Якщо оригінальне повідомлення зникло (напр. після перезапуску бота), ви можете примусово розпустити його і почати заново.",
"force_created": "✅ Попередній стак розпущено. Новий стак створено.",
"no_active_stack": "❌ У вас немає активного стаку. Використайте `/stack-create` для створення.",
"could_not_parse_channel": "⚠️ Не вдалося обробити збережений ID каналу."
},
"commands": {
"common": {
"season": "Сезон для створення картки",
"theme": "Колірна тема картки",
"squadron_short": "Коротка назва ескадрильї",
"player_username": "Ім'я гравця",
"choice_dark": "Темна",
"choice_light": "Світла"
},
"comp": {
"description": "Знайти останні відомі склади команди",
"squadron_short": "Коротка назва команди суперника"
},
"quick_log": {
"description": "Налаштувати сповіщення для цієї ескадрильї в цьому каналі",
"squadron_name": "КОРОТКА назва ескадрильї для відстеження",
"type": "Оберіть Логи, Очки, Таблиця лідерів, Тижневий BR або Усі",
"choice_logs": "Logs",
"choice_points": "Очки",
"choice_leaderboard": "Рейтинг",
"choice_both": "Обидва (Logs + Очки)",
"choice_weekly_br": "Тижневий BR"
},
"sq_info": {
"description": "Отримати інформацію про ескадрилью"
},
"sq_info_graph": {
"description": "Показати графік складу ескадрильї за активністю та відсотком перемог (поточний сезон)"
},
"sq_card": {
"description": "Створити сезонну картку ескадрильї",
"squadron": "Коротка назва ескадрильї"
},
"sq_stats": {
"description": "Показати очки ескадрильї з часом"
},
"loss_calculator": {
"description": "Розрахувати втрату очок, якщо гравці підуть з ескадрильї",
"player1": "Гравець, що йде",
"player_optional": "Гравець, що йде (необов'язково)"
},
"website": {
"description": "Отримати посилання на сайт SRE Bot"
},
"card": {
"description": "Створити сезонну картку гравця"
},
"player_stats": {
"description": "Показати детальну статистику техніки гравця",
"username": "WT-ім'я для запиту статистики",
"uid": "WT UID для запиту статистики"
},
"view_player_games": {
"description": "Показати останні 20 ігор гравця"
},
"view_match": {
"description": "Показати таблицю матчу за ID або гравцем",
"match_id": "Hex ID сесії матчу",
"player_name": "Гравець для перегляду недавніх матчів"
},
"compare": {
"description": "Порівняти сумарну SQB-статистику гравців",
"player1": "Перший гравець",
"player2": "Другий гравець",
"player_optional": "Додатковий гравець (необов'язково)"
},
"leaderboard": {
"description": "Відкрити глобальний рейтинг SRE Bot"
},
"set_squadron": {
"description": "Задати тег ескадрильї для цього сервера",
"abbreviated_name": "Коротка назва ескадрильї"
},
"setup": {
"description": "Налаштувати бота для цього сервера"
},
"meta_management": {
"description": "Керувати доступом до мета-даних цього сервера"
},
"meta": {
"description": "Шукати мета-ростер за назвою техніки",
"vehicle": "Назва техніки для пошуку"
},
"top": {
"description": "Показати топ-20 ескадрилій з детальною статистикою"
},
"language": {
"description": "Змінити мову бота."
},
"translate_message": {
"name": "Перекласти повідомлення"
},
"sq_track": {
"description": "Відстежувати ескадрилью і порівняти з минулою перевіркою",
"squadron_short_name": "Коротка назва ескадрильї"
},
"analytics": {
"description": "Показати розширену SQB-аналітику ескадрильї",
"view": "Який вид аналітики показати",
"choice_maps": "Вінрейт за мапами",
"choice_comps": "Склади команд",
"choice_consistency": "Стабільність гравців",
"choice_time": "Час доби",
"choice_matchups": "Історія зустрічей"
},
"recent": {
"description": "Показати недавні бої ескадрильї",
"length": "Кількість матчів"
},
"vs": {
"description": "Особиста статистика двох ескадрилій",
"squadron_a": "Перша ескадрилья",
"squadron_b": "Друга ескадрилья"
},
"autolog_management": {
"description": "Керувати autolog-сповіщеннями і перевіряти права"
},
"diagnose_perms": {
"description": "Перевірити права autolog у цьому каналі"
},
"unlock": {
"description": "Відкрити Premium-функції для цього сервера"
},
"credits": {
"description": "Показати команду, що створила цей проєкт"
},
"schedule": {
"description": "Показати поточний сезонний BR-графік"
},
"news": {
"description": "Показати останні новини й оголошення SRE Bot"
},
"help": {
"description": "Показати гайд, ToS і посилання підтримки"
},
"donate": {
"description": "Підтримати розробку SRE Bot"
},
"stack_create": {
"description": "Створити стак гравців",
"vehicle": "На якій техніці почнеш?"
},
"stack_manage": {
"description": "Повторно надіслати активний стак у цей канал"
},
"bot_status": {
"description": "Статус бота: остання отримана гра та середній TTL"
}
},
"permission": {
"blacklisted_title": "❌ У чорному списку",
"blacklisted_desc": "Ви не можете використовувати цю команду.",
"reason_line": "**Причина:** {reason}",
"access_denied_title": "⛔ Доступ заборонено",
"no_permission_desc": "У вас немає прав для використання цієї команди.",
"unexpected_error_title": "❗ Помилка, повідомте про неї...."
},
"weekly_br": {
"title_wildcard": "Тижневий звіт BR — {br} BR",
"title_squadron": "Тижневий звіт BR — [{tag}] {long} • {br} BR",
"window_label": "Період: {start} → {end}",
"wildcard_desc_first": "Топ-{count} полків за ELO • Місця {low}{high}",
"wildcard_desc_second": "Топ-{count} полків за ELO • Місця {low}{high}",
"squadron_stats_line": "- {games} боїв • K/D {kdr} • Перемог {wr}%",
"top_players_inline_header": "🥇 Найкращі гравці:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}б)",
"top_players_header": "**Топ-{count} гравців за ELO:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} боїв • K/D {kdr}",
"squadron_header_line": "ELO полку: {score} • {games} боїв • Перемог {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "ELO полку: недостатньо активності команди цього тижня.",
"no_data": "Немає матчів для [{tag}] у цій ротації BR."
}
}
+858
View File
@@ -0,0 +1,858 @@
{
"common": {
"error_title": "错误",
"no_data_title": "没有数据",
"access_denied_title": "访问被拒绝",
"access_denied_desc": "此服务器已被加入黑名单。",
"no_players_selected": "未选择玩家。请至少选择一名玩家。",
"must_use_in_server": "此命令必须在服务器中使用。",
"could_not_resolve_channel": "无法识别所选频道。",
"failed_update_setting": "❌ 设置更新失败。",
"configuration_not_found": "未找到配置。",
"no_channel_selected": "未选择频道。",
"no_selection_received": "未收到选择。",
"database_error": "❌ 数据库错误:{error}",
"enabled": "已启用",
"disabled": "已禁用",
"not_configured": "未配置",
"unknown": "未知",
"rating_field": "评分",
"battles_field": "战斗",
"wins_field": "胜场",
"losses_field": "负场",
"win_rate_field": "胜率",
"kills_field": "击杀",
"deaths_field": "死亡",
"kd_field": "K/D",
"members_field": "成员",
"placement_field": "排名",
"points_field": "点数",
"ground_kills_field": "地面击杀",
"air_kills_field": "空中击杀",
"total_kills_field": "总击杀",
"assists_field": "助攻",
"captures_field": "占点",
"none_option": "无"
},
"buttons": {
"skip": "跳过",
"previous": "上一页",
"next": "下一页",
"prev": "上一页",
"prev_arrow": "◀ 上一页",
"next_arrow": "下一页 ▶",
"prev_arrow_only": "◀",
"next_arrow_only": "▶",
"generate_chart": "📊 生成图表",
"show_graph": "显示图表",
"view_player_stats": "📊 查看玩家统计",
"compare_nearby": "📈 对比附近中队",
"confirm_swap": "是,替换",
"cancel_swap": "否,保留原设置",
"set_squadron": "设置中队",
"same_as_logs": "与日志相同",
"require_password": "🔒 需要密码",
"password_required": "🔒 需要密码",
"lock_data": "🔐 绑定中队数据",
"data_locked": "🔐 数据已绑定到服务器",
"allow_public": "👥 允许公开 Meta",
"public_enabled": "👥 公开 Meta 已启用",
"update_accounts": "📋 更新 Meta 账号",
"change_password": "🔑 修改密码",
"help": "❓ 帮助",
"add_player": " 添加玩家",
"update_all": "🔄 更新所有成员",
"back_to_settings": "⬅ 返回设置",
"manage_notifications": "管理通知",
"diagnose_permissions": "诊断权限",
"enable": "启用",
"disable": "禁用",
"change_channel": "更改频道",
"view_replay": "查看回放",
"view_website": "在网站查看",
"view_video": "查看视频",
"view_log": "查看战斗日志",
"view_chat": "查看聊天",
"subscribe_website": "通过网站订阅",
"cancel": "取消",
"back": "返回",
"yes_disband": "是,解散",
"transfer_leave": "转让并离开",
"accept_selected": "接受所选",
"accept_all": "全部接受",
"decline_selected": "拒绝所选",
"remove_all": "全部移除",
"remove_active": "移除正式成员",
"remove_queued": "移除排队成员",
"remove_selected": "移除所选",
"ping_all": "提醒全部",
"ping_active": "提醒正式成员",
"ping_queued": "提醒排队成员",
"ping_selected": "提醒所选",
"accept_members": "接受成员",
"remove_members": "移除成员",
"ping_members": "提醒成员",
"rename_stack": "重命名车队",
"request_to_join": "申请加入",
"leave_withdraw": "离开 / 撤回申请",
"manage_stack": "管理车队 ⚙️",
"disband_stack": "解散车队",
"force_disband_create": "强制解散并新建"
},
"events": {
"guild_join_title": "感谢添加我!",
"guild_join_desc": "该项目的详细说明。"
},
"comp": {
"not_found_title": "未找到阵容",
"not_found_desc": "没有 **{squadron}** 的数据,请稍后再试。",
"error_loading_title": "加载阵容时出错",
"error_loading_desc": "加载阵容数据失败:{error}",
"title": "{squadron} 的阵容",
"desc": "最近 {minutes} 分钟内出现过的阵容",
"no_recent_title": "没有近期阵容",
"no_recent_desc": "最近 {minutes} 分钟内没有阵容记录。",
"comp_title": "阵容 {index}",
"last_seen_label": "**最后出现**{timestamp}{warning}",
"comp_label": "**阵容**{notation}",
"no_players_recorded": "没有记录到玩家。",
"limit_reached_title": "阵容查询次数已用完",
"limit_reached_desc": "此服务器已用完本时段的 {limit} 次阵容查询。订阅(使用 /unlock)可获得无限访问,或等待下一个时段。",
"remaining_footer": "本时段剩余 {remaining}/{limit} 次免费阵容查询"
},
"quick_log": {
"invalid_type": "类型只能设置为 Logs、Points、排行榜、周BR 或 全部。",
"squadron_required": "此通知类型需要中队名称。",
"wildcard_logs_only": "通配符只适用于日志通知。",
"squadron_not_resolved": "无法识别中队 `{squadron}`。",
"save_failed": "保存通知设置失败。",
"premium_warning": "\n\n注意:高级功能可能需要有效订阅。",
"leaderboard_set": "排行榜通知已设置。",
"both_set": "已为 **{squadron}** 设置日志和点数通知。{premium_note}",
"alarm_set": "已为 **{squadron}** 设置 {alarm_type} 通知。{premium_note}",
"weekly_br_wildcard_set": "周BR报告(前20中队)已设置到该频道。在每次BR轮换结束时发送。",
"weekly_br_squadron_set": "{squadron} 的周BR报告(前15玩家)已设置到该频道。在每次BR轮换结束时发送。"
},
"autolog": {
"game_not_logged_title": "比赛未记录",
"game_not_logged_desc": "此频道未配置自动日志,或该比赛不符合当前服务器的记录规则。",
"wildcard_blocked_title": "通配符日志不可用",
"wildcard_blocked_desc": "通配符中队条目(*, all, everything)仅 Pro 或 Max 档位可用。你的服务器在 {notif} 上当前为 **{tier}** 档位。升级后可重新启用通配符日志。",
"over_cap_title": "已达到中队数量上限",
"over_cap_desc": "你的服务器当前为 **{tier}** 档位,可启用 **{cap} 个 {notif}** 中队。中队 **{squadron}** 当前超出上限,因此不会被记录。升级到更高档位可恢复记录。",
"over_cap_footer": "升级套餐可提高或取消此限制。",
"server_not_upgraded_title": "此服务器未升级",
"server_not_upgraded_autolog_desc": "此服务器没有有效的高级订阅。\n\n**未升级服务器将在 <t:{deadline}:D> 后停止接收自动比赛计分板。**\n\n使用 `/unlock` 订阅以继续接收自动比赛日志。",
"server_not_upgraded_points_desc": "此服务器没有有效的高级订阅。\n\n**未升级服务器将在 <t:{deadline}:D> 后停止接收自动更新。**\n\n使用 `/unlock` 订阅以继续接收自动更新。",
"points_not_logged_title": "点数未记录",
"points_not_logged_desc": "没有为此服务器配置点数通知。",
"wl_line": "战绩:**{wins}胜 / {losses}负**,中队:**{squadron}**",
"placement_rose": "\n**{squadron}** 从 **{old_place}** 上升到 **{new_place}**",
"placement_fell": "\n**{squadron}** 从 **{old_place}** 下降到 **{new_place}**",
"points_update_title": "**{squadron} {region} 点数更新**",
"points_update_desc": "# {old_total} -> {new_total} {chart}{wl_line}{placement_line}\n\n**玩家变化:**",
"leave_title": "⚠️ 玩家离开 {squadron}",
"leave_desc": "**{nick}**{uid})已离开中队。\n\n最后记录点数:**{points}**",
"replay_not_available": "此比赛的回放暂不可用。",
"too_many_videos": "正在生成的视频过多,请稍后再试。",
"video_gen_failed": "视频生成失败:{error}",
"video_missing": "视频文件不存在。",
"video_too_large": "回放视频过大,无法上传({file_mb:.1f} MB)。服务器限制为 {limit_mb:.0f} MB。",
"video_web_fallback": "视频太大,无法直接上传。可在这里查看:{url}",
"video_upload_failed": "视频上传失败。可在网站查看:{url}",
"video_unexpected_error": "处理视频时出现意外错误:{error}",
"replay_not_found": "未找到回放:{session_id}",
"no_chat_log": "此回放没有可用聊天记录:{session_id}",
"chat_log_title": "**比赛 [{session_id}]({url}) 的聊天记录**",
"chat_log_part_title": "**比赛 [{session_id}]({url}) 的聊天记录(第 {part}/{total} 段)**",
"chat_log_part_only": "**聊天记录(第 {part}/{total} 段)**",
"chat_log_error": "读取聊天记录失败:{error}",
"no_battle_log": "此回放没有可用战斗日志:{session_id}",
"battle_log_title": "**比赛 [{session_id}]({url}) 的战斗日志**",
"battle_log_part_title": "**比赛 [{session_id}]({url}) 的战斗日志(第 {part}/{total} 段)**",
"battle_log_part_only": "**战斗日志(第 {part}/{total} 段)**",
"battle_log_error": "读取战斗日志失败:{error}",
"premium_active_line": "✅ **高级版:** 已启用 — autologging is 已启用 for this 服务器.",
"premium_not_subscribed_line": "❌ **高级版:** 未订阅 — 使用 `/unlock` to enable autologging.",
"premium_free_line": "⚪ **高级版:** 未订阅 — 使用 `/unlock` to 订阅 ($2.99/mo). *(Autologs are free for all 服务器s right now.)*",
"what_to_do": "\n\n你想做什么?",
"select_notif_type": "选择the notification type to manage:",
"select_notif_placeholder": "选择通知类型",
"logs_option": "日志",
"logs_option_desc": "该项目的详细说明。",
"points_option": "点数",
"points_option_desc": "该项目的详细说明。",
"leaderboard_option": "排行榜",
"leaderboard_option_desc": "该项目的详细说明。",
"selected_type": "已选择 **{type}**。现在请选择要管理的中队:",
"select_squadron_placeholder": "选择中队",
"select_squadron_page_placeholder": "选择中队(第 {page} 页)",
"no_squadrons_available": "没有squadron available for this notification type.",
"managing_global": "正在管理频道 **{channel}** 中的 **{type}**(全局)。",
"managing_squadron": "正在管理频道 **{channel}** 中 **{squadron}** 的 **{type}**。",
"select_channel": "选择a new channel:",
"select_channel_placeholder": "选择频道",
"select_channel_page_placeholder": "选择频道(第 {page} 页)",
"global_toggled": "{type}(全局)现在为 {state}。",
"squadron_toggled": "**{squadron}** 的 {type} 现在为 {state}。",
"channel_updated_global": "已将 {type}(全局)更新到 {channel}",
"channel_updated_squadron": "已将 **{squadron}** 的 {type} 更新到 {channel}",
"diagnose_channel_placeholder": "请输入或选择…",
"select_channel_diagnose": "选择the channel to diagnose:",
"points_table_header": "名称 变化 当前\n",
"no_squadrons_desc": "未配置中队",
"no_channels_desc": "没有可用频道",
"cap_header": "已启用 {used}/{cap} 个 {notif} — {tier} 档位"
},
"language": {
"prompt": "选择此服务器的机器人显示语言。",
"select_placeholder": "选择语言",
"language_set": "语言已设置为 {language}。",
"translate_prompt": "在下方选择目标语言 👇",
"translate_result": "**{author} → {language}**\n{text}",
"translate_failed": "翻译失败:{error}",
"translate_placeholder": "选择目标语言…",
"translation_unavailable": "翻译不可用(未配置 DeepL",
"translation_failed": "翻译失败"
},
"diagnostics": {
"title": "诊断",
"channel_permissions_header": "频道权限:{channel_id}",
"perms_needed": "需要权限:发送消息、嵌入链接、附加文件。",
"server_squadron_header": "服务器中队",
"server_squadron_short": "简称:{short}",
"server_squadron_long": "全称:{long}",
"server_squadron_not_set": "未设置服务器中队。",
"autolog_prefs_header": "自动日志设置",
"autolog_none_configured": "尚未配置自动日志。",
"autolog_setup_hint": "使用 `/quick-log` 或 `/setup` 开始配置。",
"missing_send_attach": "缺少发送消息或附加文件权限",
"channel_not_found": "找不到频道",
"invalid_channel_id": "无效频道 ID",
"selected_channel_tag": "(当前选择)",
"autolog_no_logs_channels": "未配置日志频道。",
"autolog_enable_hint": "启用日志通知后,这里会显示频道。",
"premium_status_header": "高级状态",
"premium_active": "高级功能:已激活",
"premium_not_subscribed": "高级功能:未订阅",
"premium_autolog_required": "自动日志需要订阅。",
"premium_not_subscribed_free": " ⚪ 未订阅 — 使用 `/unlock` 订阅($2.99/月)。",
"premium_free_note": " *(Autologs are free for all 服务器s right now.)*"
},
"sq_info": {
"title": "中队信息:{squadron}",
"placement_field": "排名",
"total_points_field": "总点数",
"total_members_field": "成员数",
"members_field": "成员",
"fetch_failed": "无法获取中队信息。"
},
"sq_info_graph": {
"title": "{squadron} — SQ-INFO(赛季 {season}",
"embed_title": "{squadron} — 阵容构成",
"embed_desc": "赛季 **{season}** · 中位场次:**{median}** · 核心:**{core}** · 活跃:**{active}** · 边缘:**{weak}**\n按场次降序排列;高度 = 胜率。核心 = 场次 ≥ 中位 且 胜率 ≥ 中队胜率。活跃 = 场次 ≥ 中位 且 胜率 < 中队胜率。边缘 = 场次低于中位。",
"squadron_wr_line": "中队胜率 {wr}%",
"y_label": "胜率",
"core_header": "核心 — {count} · 胜率 {avg}%",
"active_header": "活跃 — {count} · 胜率 {avg}%",
"weak_header": "边缘 — {count} · 胜率 {avg}%",
"no_active_season": "未找到进行中的赛季,请在下一个赛季开始后再试。",
"no_members": "未找到 {squadron} 的当前成员。"
},
"recap_card": {
"unknown_season": "未知赛季:{season}",
"no_clan_id": "找不到 **{squadron}** 的中队 ID。",
"render_failed": "回顾卡渲染失败。"
},
"leaderboard_alarm": {
"not_logged_title": "排行榜未记录",
"not_logged_desc": "此服务器未配置排行榜通知。",
"title": "中队排行榜更新",
"top15_desc": "含统计数据的前 15 名中队,将在时段结束 35 分钟后发送。\n本条发送于 <t:{timestamp}:R>。",
"top30_desc": "前 30 名中队",
"server_not_upgraded_title": "此服务器未升级",
"server_not_upgraded_desc": "此服务器没有有效的高级订阅。\n\n**未升级服务器将在 <t:{deadline}:D> 后停止接收自动更新。**\n\n使用 `/unlock` 订阅以继续接收自动更新。"
},
"misc": {
"schedule_title": "赛季日程",
"schedule_timeslot_label": "{region} 时段",
"schedule_not_found_title": "未找到日程",
"schedule_not_found_desc": "目前还没有可用的日程数据。",
"credits_title": "鸣谢",
"credits_desc": "该项目的详细说明。",
"news_no_news_title": "暂无公告",
"news_no_news_desc": "目前没有公告,请稍后再查看!",
"news_footer": "感谢你的支持!ᕙᘘᗢ",
"help_title": "机器人指南",
"donate_title": "支持 SRE Bot",
"donate_desc": "该项目的详细说明。",
"status_title": "机器人状态",
"status_last_received": "最近接收的对局",
"status_avg_ttl": "平均 TTL (最近 30 局)",
"status_no_data": "暂无数据",
"status_gaijin_slow": "⚠️ Gaijin 服务器较慢",
"help_commands_header": "**命令概览**",
"help_links": "详细信息请阅读[文档]({docs}),或在[支持服务器]({support})获取帮助。",
"help_terms": "[服务条款]({terms}) • [隐私政策]({terms})"
},
"sq_stats": {
"no_data_title": "没有数据",
"no_data_desc": "未找到中队 {squadron} 的历史数据。",
"title": "{squadron} // 中队",
"desc": "总分趋势(最近 {count} 个数据点)",
"previous_score_field": "上次分数",
"current_score_field": "当前分数",
"change_field": "变化",
"player_title": "{squadron} // 玩家",
"player_desc": "单个玩家点数趋势",
"comparison_title": "{squadron} // 排行榜对比",
"comparison_desc": "正在与排名 {range} 的中队对比",
"current_position_field": "当前排名",
"squadrons_shown_field": "显示的中队",
"squadron_not_found_error": "排行榜中找不到该中队",
"no_nearby_error": "未找到附近排名的中队",
"no_historical_error": "未找到附近中队的历史数据",
"comparison_chart_failed": "生成对比图表失败",
"select_players_placeholder": "选择玩家(第 {page} 页)"
},
"loss_calc": {
"title": "点数损失 — {squadron}",
"players_leaving_field": "离队玩家",
"share_of_total_field": "占总数比例",
"points_lost_real_field": "损失点数(实际)",
"points_lost_raw_field": "损失点数(原始)",
"squadron_rating_field": "中队评分",
"squadron_position_field": "中队排名",
"positions_lost_field": "下降名次",
"not_found_footer": "未在中队中找到:{players}",
"fetch_failed": "获取中队数据失败:{error}",
"no_point_data": "此中队没有可用点数数据。",
"no_matching_players": "在 **{squadron}** 中未找到匹配玩家。"
},
"player": {
"select_player_placeholder": "选择玩家",
"no_stats_found": "❌ 未找到 UID {uid} 的统计数据。",
"no_vehicle_stats": "❌ 未找到此玩家的载具统计。",
"vehicles_found": "找到 **{nick}** 的 **{count}** 个载具\n选择一个载具查看详细统计:",
"vehicle_select_placeholder": "选择载具(第 {page}/{total} 页)",
"combat_stats_header": "**__战斗统计__**",
"ground_kills_label": "**地面击杀:** {value}",
"air_kills_label": "**空中击杀:** {value}",
"total_kills_label": "**总击杀:** {value}",
"assists_label": "**助攻:** {value}",
"deaths_label": "**死亡:** {value}",
"kd_label": "**K/D** {value}",
"captures_label": "**占点:** {value}",
"battle_record_header": "**__战绩记录__**",
"total_battles_label": "**总战斗:** {value}",
"wins_label": "**胜场:** {value}",
"losses_label": "**负场:** {value}",
"win_rate_label": "**胜率:** {value}%",
"stats_desc": "**{nick}****{squadron}**)的统计\nUID`{uid}`",
"not_found_title": "未找到玩家",
"not_found_desc": "未找到 `{player}` 的比赛历史。",
"no_players_found": "未找到匹配 **{username}** 的玩家\n可尝试使用 `/website` 在网站搜索。",
"multiple_matches": "找到多个匹配项,请在下方选择正确的玩家:",
"must_provide_input": "你必须至少提供 UID 或用户名。"
},
"player_games": {
"no_recent_title": "没有近期比赛",
"no_recent_desc": "最近 8 小时内未找到 **{player}** 的比赛。",
"squadron_label": "**中队:** {squadron}",
"record_label": "**胜:** {wins} **负:** {losses} **胜率:** {wr}%",
"comps_played_header": "\n\n**使用过的阵容**"
},
"match": {
"missing_input_title": "缺少输入",
"missing_input_desc": "该项目的详细说明。",
"not_found_title": "未找到比赛",
"not_found_desc": "找不到 ID 为 `{match_id}` 的比赛。",
"invalid_data_title": "比赛数据无效",
"invalid_data_desc": "无法解析回放数据。",
"scoreboard_error_title": "计分板错误",
"scoreboard_error_desc": "生成计分板图片失败。",
"no_games_title": "未找到比赛",
"no_games_desc": "未找到 **{player}** 的比赛历史。",
"recent_matches_title": "{player} 的近期比赛",
"recent_matches_desc": "最多显示 {count} 场近期比赛。选择一场查看完整计分板。",
"select_match_placeholder": "选择要查看的比赛..."
},
"compare": {
"no_players_found": "未找到匹配 **{name}** 的玩家。",
"multiple_matches": "**{name}** 有多个匹配项:{matches}\n请使用更具体的名称(自动补全建议为精确匹配)。",
"could_not_resolve": "无法识别玩家。",
"could_not_fetch": "❌ 无法获取 **{name}** 的统计。",
"no_graph_data": "最近 90 天没有可用数据。",
"no_squadron_points_data": "{names} 没有中队点数数据(在已追踪的中队历史中找不到该玩家)。",
"graph_title": "玩家点数 — 最近 90 天",
"battles_label": "战斗",
"wins_label": "胜场",
"losses_label": "负场",
"win_rate_label": "胜率",
"ground_kills_label": "地面击杀",
"air_kills_label": "空中击杀",
"total_kills_label": "总击杀",
"assists_label": "助攻",
"deaths_label": "死亡",
"kd_label": "K/D",
"captures_label": "占点"
},
"squadron": {
"not_found_desc": "未找到中队 `{squadron}`。",
"set_title": "✅ 中队已设置",
"set_desc": "此服务器的中队已设置为 **{squadron}**。",
"short_name_field": "简称",
"long_name_field": "全称",
"swap_title": "✅ 中队已替换",
"swap_desc": "已将此服务器的 **{old}** 替换为 **{new}**。",
"already_set_title": "⚠️ 中队已设置",
"already_set_desc": "此服务器当前设置为 **{old}**。\n是否替换为 **{new}**",
"swap_cancelled": "❌ 中队更改已取消。"
},
"setup": {
"step1_title": "服务器设置 — 第 1/3 步",
"step1_desc": "此向导将引导你为服务器配置机器人。\n\n**第 1 步** — 设置中队\n**第 2 步** — 选择日志频道\n**第 3 步** — 选择点数频道\n",
"step1_current_sq": "\n当前配置的中队:**[{short}] {long}**",
"step2_title": "服务器设置 — 第 2/3 步",
"step2_desc": "中队已设置为 **[{short}] {long}**。\n\n**战斗日志** 应发布到哪里?\n请在下方选择文字频道,或跳过此步骤。",
"step3_title": "服务器设置 — 第 3/3 步",
"step3_desc": "**点数通知** 应发布到哪里?\n请在下方选择文字频道,或跳过此步骤。",
"step3_same_as_logs": "\n\n你也可以点击“与日志相同”来复用日志频道。",
"summary_title": "设置完成",
"summary_desc": "该项目的详细说明。",
"squadron_field": "中队",
"logs_channel_field": "日志频道",
"points_channel_field": "点数频道",
"premium_required_field": "⚠️ 比赛日志需要高级版",
"premium_required_value": "在此服务器拥有有效订阅之前,自动比赛计分板不会发布。运行 `/unlock` 订阅($2.99/月)。",
"modal_title": "设置中队",
"modal_label": "中队 Short 名称",
"modal_placeholder": "请输入或选择…",
"squadron_not_found": "未找到中队 `{squadron}`。请重试。",
"logs_channel_placeholder": "选择日志频道...",
"points_channel_placeholder": "选择点数频道..."
},
"meta_management": {
"squadron_not_found_title": "❌ 中队 Not 已找到",
"squadron_not_found_desc": "找不到中队 **{squadron}** 的 clan ID。",
"access_denied_title": "标题",
"access_denied_desc": "密码错误。此中队的 Meta 数据受保护。",
"data_locked_title": "标题",
"data_locked_desc": "**{squadron}** 已启用数据绑定,不能转移到其他服务器。\n\n移动前,中队所有者必须先禁用 **绑定中队数据**。",
"error_retrieving_settings": "❌ 错误 retrieving 服务器 settings after transfer. Please try again.",
"error_retrieving_settings_retry": "❌ 错误 retrieving 服务器 settings. Please try running the 命令 again.",
"authenticated_title": "✅ 已验证",
"authenticated_desc": "密码已验证。正在管理 **{squadron}** 的设置。",
"claimed_title": "标题",
"claimed_desc": "**{squadron}** 已成功绑定到此服务器!",
"password_requirement_field": "🔒 密码 Requirement",
"data_lock_field": "🔐 中队 数据 Binding",
"public_meta_field": "👥 公开 Meta 访问",
"access_password_field": "🔑 访问 密码",
"enabled_value": "✅ 已启用",
"disabled_value": "❌ 已禁用",
"settings_title": "标题",
"settings_desc": "**中队:** {squadron}\n**Clan ID** {clan_id}",
"first_time_title": "🔐 Meta 管理 - 首次设置",
"first_time_owner_desc": "**中队:** {squadron}\n**Clan ID** {clan_id}\n\n🔑 已生成访问密码。**请保存此密码**,之后验证 Meta 数据访问时会用到。\n\n**密码:** `{password}`",
"first_time_non_owner_desc": "**中队:** {squadron}\n**Clan ID** {clan_id}\n\n中队已设置。请向服务器所有者索要访问密码。",
"settings_field": "设置",
"settings_hint": "使用下方按钮配置访问设置。",
"password_toggled": "✅ 密码 requirement: **{state}**",
"lock_toggled": "✅ 中队 data binding: **{state}**",
"public_meta_toggled": "✅ 公开 meta access: **{state}**\n{detail}",
"public_meta_enabled_detail": "Non-admins can now 使用 `/meta` 命令.",
"public_meta_disabled_detail": "Only admins can 使用 `/meta` 命令.",
"owner_only_password": "❌ Only the 服务器 owner can change the squadron password.",
"help_title": "📖 Meta 管理帮助",
"help_desc": "各项设置和功能说明:",
"help_password_field": "🔑 访问 密码",
"help_password_value": "Your squadron's access password. Only the **服务器 owner** can see the password in the settings panel. Anyone with the password can claim your squadron's meta data on their 服务器, so keep it secure.",
"help_require_field": "🔒 Require 密码",
"help_require_value": "When 已启用, even admins on this 服务器 must enter the squadron password to access `/meta-management`. Adds an extra layer of security to prevent accidental changes.",
"help_lock_field": "🔐 Bind 中队 数据",
"help_lock_value": "When 已启用, prevents the squadron from being transferred to other 服务器s, even with the correct password. Must be disabled before the squadron can be transferred.",
"help_public_field": "👥 Allow 公开 Meta",
"help_public_value": "When 已启用, allows non-admin 成员s to 使用 the `/meta` 命令 to search squadron vehicles. When disabled, only 服务器 administrators can 使用 `/meta`.",
"help_accounts_field": "📋 更新Meta Accounts",
"help_accounts_value": "Opens the player roster manager where you can add or remove players from your squadron's meta roster. Use **更新All 成员** to sync your entire squadron at once.",
"help_change_pw_field": "🔑 Change 密码",
"help_change_pw_value": "**服务器 owner only.** Change the squadron's access password and set an optional hint. The hint is shown in the password prompt to help re成员 it.",
"password_modal_title": "中队 访问 密码",
"password_modal_label": "Enter 中队 密码",
"password_modal_placeholder": "请输入或选择…",
"change_pw_modal_title": "标题",
"current_password_label": "当前密码",
"current_password_placeholder": "请输入或选择…",
"new_password_label": "新密码",
"new_password_placeholder": "请输入或选择…",
"confirm_password_label": "确认新密码",
"confirm_password_placeholder": "请输入或选择…",
"hint_label": "密码提示(可选)",
"hint_placeholder": "请输入或选择…",
"pw_incorrect": "❌ 当前password is incorrect.",
"pw_mismatch": "❌ 两次输入的新密码不一致。请重试。",
"pw_empty": "❌ 新密码不能为空。",
"pw_changed": "✅ 密码 updated successfully for **{squadron}**.\n**New 密码:** `{password}`",
"pw_changed_hint": "\n**提示:** {hint}",
"player_add_modal_title": "标题",
"player_add_label": "玩家 UID 或昵称",
"player_add_placeholder": "请输入或选择…",
"player_not_found": "❌ 玩家 `{player}` not found in 玩家s_Global database.\n",
"roster_title": "📋 Meta 名单管理 - {squadron}",
"roster_desc": "**中队 Clan ID** {clan_id}\n**总玩家:** {count}",
"roster_page_field": "玩家(第 {page}/{total} 页)",
"no_players_field": "没有玩家",
"no_players_hint": "没有players added to meta roster yet. Click **添加玩家** to get started.",
"remove_player_placeholder": "请输入或选择…",
"fetch_members_failed": "❌ 无法fetch squadron 成员s: {error}",
"no_members_found": "❌ 没有成员s found in squadron or API call failed.",
"roster_synced": "✅ 已与中队同步名单。",
"roster_added": "已添加 **+{count}**",
"roster_removed": "已移除 **-{count}**(已离开中队)",
"roster_up_to_date": "**{count}** 已是最新",
"refreshing_vehicles": "正在后台刷新载具数据..."
},
"meta": {
"not_configured": "❌ Meta data not configured for this 服务器. Run `/meta-management` first.",
"no_permission": "❌ You need administrator permissions to 使用 this 命令.\nAdmins can enable public access via `/meta-management`.",
"no_results": "❌ 没有players in your squadron roster have **{vehicle}**.",
"no_results_admin_hint": "\n*如果你认为有人应该拥有它,请在 `/meta-management` 中点击更新成员按钮并再次检查。*",
"search_title": "🔍 搜索结果 - {vehicle}",
"matches_found": "**比赛es 已找到:** {count} player(s)",
"spawns_label": "出场",
"deaths_label": "死亡",
"gk_label": "GK",
"ak_label": "AK",
"points_label": "点数",
"kdr_label": "战损比",
"games_label": "场次",
"no_points": "—"
},
"top": {
"title": "**Top 20 中队s**",
"rating_label": "**评分:** {value}",
"air_kills_label": "**空战 击杀:** {value}",
"ground_kills_label": "**陆战 击杀:** {value}",
"deaths_label": "**死亡:** {value}",
"kd_label": "**K/D** {value}",
"win_rate_label": "**胜率:** {value}",
"playtime_label": "**游玩时间:** {value}",
"fetch_failed": "无法获取中队数据。"
},
"analytics": {
"no_data_title": "没有数据",
"no_matches_desc": "未找到比赛。",
"no_comp_desc": "未找到阵容数据。",
"no_consistency_desc": "玩家数据不足(至少需要 50 场比赛)。",
"no_time_desc": "该项目的详细说明。",
"unknown_view": "未知视图。",
"map_title": "地图胜率:{squadron}",
"comp_title": "队伍阵容:{squadron}",
"consistency_title": "玩家稳定性:{squadron}",
"consistency_desc": "按 K/D 排序",
"time_title": "分时段表现:{squadron}",
"eu_timeslot": "\n**EU 时段**",
"na_timeslot": "\n**NA 时段**",
"off_peak": "\n**非高峰时段**",
"matchups_title": "📜 {squadron} — 对战历史",
"matchups_won_field": "🏆 胜场最多的对手",
"matchups_lost_field": "💀 负场最多的对手",
"no_matchups_desc": "该项目的详细说明。"
},
"recent": {
"title": "近期比赛:{squadron}",
"no_matches_desc": "该项目的详细说明。"
},
"h2h": {
"two_required_title": "需要两个中队",
"two_required_desc": "请至少提供一个中队,或先使用 `/set-squadron` 后再提供对手。",
"provide_a_desc": "请提供 `squadron_a`,或先使用 `/set-squadron`。",
"provide_b_desc": "请提供 `squadron_b`,或先使用 `/set-squadron`。",
"squadron_not_found_title": "未找到中队",
"same_squadron_title": "同一个中队",
"same_squadron_desc": "不能和自己进行交手记录查询。",
"record_desc": "**战绩:** {a_wins}胜 - {b_wins}负({total} 场)",
"no_matches_desc": "**{a}** 和 **{b}** 之间没有已记录比赛。"
},
"track": {
"squadron_not_found": "未找到中队。",
"fetch_failed": "无法获取中队信息。"
},
"unlock": {
"title": "SRE Bot 高级版",
"desc": "该项目的详细说明。",
"already_subscribed_title": "SRE Bot 高级版",
"already_subscribed_desc": "该项目的详细说明。",
"manage_discord_field": "管理订阅",
"manage_discord_value": "Your 订阅 is through **Discord**.\nTo cancel, go to **User 设置 → Subscriptions** in Discord.",
"manage_website_field": "管理订阅",
"manage_website_value": "Your 订阅 is through the **网站**.\n管理it at [whop.com/计费](https://whop.com/计费).",
"coming_soon_field": "即将推出",
"coming_soon_value": "高级版 订阅s are not yet available. Check back soon!",
"current_tier": "你当前使用 **{tier}** 套餐。",
"upgrade_to": "升级到 {tier}",
"upgrade_to_value": "升级到 **{tier}** 可获得更高中队上限和更多功能。"
},
"dev": {
"restricted_dev_team": "此命令仅限开发团队使用。",
"restricted_bot_owner": "❌ 此命令仅限机器人所有者使用。",
"invalid_server_id": "❌ Invalid 服务器 ID. Must be a 17-19 digit Discord 服务器 ID.",
"expiry_too_soon": "❌ 到期时间戳必须至少是一个月以后。\n> 当前:<t:{now}:F>\n> 最早:<t:{min}:F>\n> 你提供的是:<t:{provided}:F>",
"entitlement_write_failed": "❌ 写入权益失败:{error}",
"entitlement_created_title": "✅ 已创建手动权益",
"entitlement_created_desc": "**服务器:** {guild_name} (`{server_id}`)\n**到期:** <t:{unix_ts}:F> (<t:{unix_ts}:R>)\n**创建:** <t:{now}:F>",
"query_failed": "查询失败:{error}",
"health_title": "机器人健康仪表盘",
"health_uptime": "运行时间",
"health_guilds": "服务器",
"health_games_processed": "已处理比赛",
"health_tasks": "任务",
"health_websocket": "WebSocket",
"health_never": "从未",
"health_errors": "{count} 个错误)",
"health_last_msg": "上一条消息 {ago}(共 {count} 条)",
"health_avg_ttl": "平均 TTL (最近 30 局)",
"entitlements_title": "有效权益(共 {count} 个)",
"entitlements_no_entries": "没有权益。",
"entitlements_empty_title": "有效权益",
"entitlements_empty_desc": "未找到有效权益。",
"entitlements_tag_discord": "标题",
"entitlements_tag_whop": "标题",
"entitlements_tag_manual": "手动",
"query_prefix": "查询:{name}"
},
"stacks": {
"stack_title": "{leader} 的车队",
"stack_named_title": "{name}",
"no_members": "暂无成员。",
"members_field": "成员 ({count}/{max})",
"queue_field": "队列({count}/{max}",
"manage_title": "管理车队",
"no_pending_requests": "没有待处理申请。",
"disbanded_title": "车队 [已解散]",
"disbanded_desc": "此车队已被队长解散。",
"expired_title": "车队 [已过期]",
"expired_desc": "此车队已过期。",
"join_modal_title": "申请加入车队",
"join_vehicle_label": "你要使用什么载具?",
"join_vehicle_placeholder": "e.g. F-16C, WZ305...",
"ping_modal_title": "提醒消息",
"ping_message_label": "自定义消息(可选)",
"ping_message_placeholder": "请输入或选择…",
"rename_modal_title": "重命名车队",
"rename_label": "车队名称",
"rename_placeholder": "请输入或选择…",
"select_new_leader": "选择new 队长…",
"select_applicants": "选择applicants…",
"no_pending_applications": "没有pending applications.",
"select_to_remove": "选择people to remove…",
"no_members_or_applicants": "没有成员s or applicants.",
"select_to_ping": "选择people to ping individually…",
"stack_not_found": "❌ 找不到车队。",
"no_longer_exists": "❌ 此车队已不存在。",
"member_not_exists": "❌ 该成员已不存在。",
"already_has_stack": "❌ 该玩家已有活跃车队。",
"already_member": "❌ 你已经是此车队成员。",
"already_applied": "❌ 你已经有此车队的待处理申请。",
"queue_full": "❌ 队列已满({max}/{max})。请稍后再试。",
"application_sent": "✅ 申请已发送!车队队长会进行审核。",
"stack_disbanded": "✅ 车队已解散。",
"cancelled": "已取消。",
"select_member_transfer": "❌ 请选择要转让所有权的成员。",
"ownership_transferred": "✅ 所有权已转让给 {nick}。你已离开车队。",
"select_applicant_first": "❌ 请先至少选择一名申请者。",
"stack_full": "❌ 车队已满({max}/{max} 名成员)。",
"select_person_first": "❌ 请先至少选择一个人。",
"no_one_to_ping": "❌ 没有one to ping.",
"ping_footer": "{leader} 为 {stack} 发起提醒。",
"pinged": "✅ 已提醒!",
"select_from_dropdown": "❌ 请先从下拉菜单中至少选择一个人。",
"stack_renamed": "✅ 车队已重命名为 **{name}**。",
"only_member_use_disband": "❌ 你是唯一成员。请使用 **解散车队** 结束它。",
"select_transfer_prompt": "选择a 成员 to transfer ownership to before leaving:",
"left_stack": "✅ 你已离开车队。",
"application_withdrawn": "✅ 你的申请已撤回。",
"not_member_or_applicant": "❌ 你不是此车队的成员或申请者。",
"leader_only_manage": "❌ 只有车队队长可以管理此车队。",
"leader_only_disband": "❌ 只有车队队长可以解散此车队。",
"confirm_disband": "确定要解散此车队吗?此操作无法撤销。",
"already_active_stack": "⚠️ 你已经有一个活跃车队。如果原始嵌入消息丢失(例如机器人重启后),可以强制解散并重新创建。",
"force_created": "✅ 之前的车队已解散。新车队已创建。",
"no_active_stack": "❌ 你没有活跃车队。使用 `/stack-create` 创建一个。",
"could_not_parse_channel": "⚠️ 无法解析已存储的频道 ID。"
},
"commands": {
"common": {
"season": "要生成卡片的赛季",
"theme": "卡片配色主题",
"squadron_short": "战队简称",
"player_username": "玩家用户名",
"choice_dark": "深色",
"choice_light": "浅色"
},
"comp": {
"description": "查找某队最近已知阵容",
"squadron_short": "敌方队伍简称"
},
"quick_log": {
"description": "在此频道为该战队设置提醒",
"squadron_name": "要监控的战队简称",
"type": "选择 Logs、Points、排行榜、周BR 或 全部",
"choice_logs": "日志",
"choice_points": "分数",
"choice_leaderboard": "排行榜",
"choice_both": "两者(日志 + 分数)",
"choice_weekly_br": "周BR"
},
"sq_info": {
"description": "获取战队信息"
},
"sq_info_graph": {
"description": "按当前赛季的活跃度与胜率显示战队阵容构成图表"
},
"sq_card": {
"description": "为战队生成赛季总结卡",
"squadron": "战队简称"
},
"sq_stats": {
"description": "显示战队分数随时间变化"
},
"loss_calculator": {
"description": "计算玩家离队后的战队分数损失",
"player1": "离队玩家",
"player_optional": "离队玩家(可选)"
},
"website": {
"description": "获取 SRE Bot 网站链接"
},
"card": {
"description": "为玩家生成赛季总结卡"
},
"player_stats": {
"description": "查看玩家的详细载具统计",
"username": "用于查询统计的 WT 用户名",
"uid": "用于查询统计的 WT UID"
},
"view_player_games": {
"description": "查看玩家最近 20 场比赛"
},
"view_match": {
"description": "按 ID 或玩家查看比赛记分板",
"match_id": "比赛的十六进制会话 ID",
"player_name": "用于浏览近期比赛的玩家名"
},
"compare": {
"description": "比较玩家的 SQB 汇总统计",
"player1": "第一个玩家用户名",
"player2": "第二个玩家用户名",
"player_optional": "其他玩家用户名(可选)"
},
"leaderboard": {
"description": "获取 SRE Bot 全球排行榜"
},
"set_squadron": {
"description": "设置此服务器的战队标签",
"abbreviated_name": "要设置的战队简称"
},
"setup": {
"description": "为此服务器设置机器人"
},
"meta_management": {
"description": "管理此服务器的 meta 数据访问"
},
"meta": {
"description": "按载具名称搜索战队 meta 名单",
"vehicle": "要搜索的载具名称"
},
"top": {
"description": "查看排名前 20 的战队及详细统计"
},
"language": {
"description": "更改机器人语言。"
},
"translate_message": {
"name": "翻译消息"
},
"sq_track": {
"description": "追踪战队并与上次检查对比",
"squadron_short_name": "要追踪的战队简称"
},
"analytics": {
"description": "查看战队的高级 SQB 分析",
"view": "要显示的分析视图",
"choice_maps": "地图胜率",
"choice_comps": "队伍阵容",
"choice_consistency": "玩家稳定性",
"choice_time": "时段表现",
"choice_matchups": "交手历史"
},
"recent": {
"description": "显示战队近期比赛",
"length": "要显示的比赛数量"
},
"vs": {
"description": "两个战队的交手记录",
"squadron_a": "第一个战队",
"squadron_b": "第二个战队"
},
"autolog_management": {
"description": "管理自动日志通知并诊断权限",
"diagnose_perms": "诊断此频道的自动日志权限"
},
"diagnose_perms": {
"description": "诊断此频道的自动日志权限"
},
"unlock": {
"description": "为此服务器解锁 Premium 功能"
},
"credits": {
"description": "查看本项目制作团队"
},
"schedule": {
"description": "查看当前赛季 BR 日程"
},
"news": {
"description": "查看最新 SRE Bot 新闻和公告"
},
"help": {
"description": "查看指南、服务条款和支持链接"
},
"donate": {
"description": "支持 SRE Bot 开发"
},
"stack_create": {
"description": "创建玩家组队",
"vehicle": "你要用什么载具开局?"
},
"stack_manage": {
"description": "将你的活动组队重新发布到此频道"
},
"bot_status": {
"description": "查看机器人状态:最近接收的对局与平均 TTL"
}
},
"permission": {
"blacklisted_title": "❌ 已加入黑名单",
"blacklisted_desc": "你已被禁止使用此命令。",
"reason_line": "**原因:** {reason}",
"access_denied_title": "⛔ 访问被拒绝",
"no_permission_desc": "你没有权限使用此命令。",
"unexpected_error_title": "❗ 出错了,请上报...."
},
"weekly_br": {
"title_wildcard": "周BR报告 — {br} BR",
"title_squadron": "周BR报告 — [{tag}] {long} • {br} BR",
"window_label": "时段:{start} → {end}",
"wildcard_desc_first": "按ELO排序前 {count} 中队 • 排名 {low}{high}",
"wildcard_desc_second": "按ELO排序前 {count} 中队 • 排名 {low}{high}",
"squadron_stats_line": "- {games} 场 • K/D {kdr} • 胜率 {wr}%",
"top_players_inline_header": "🥇 顶尖玩家:",
"player_line_short": " {rank}. {nick} ⭐ {score} ({games}场)",
"top_players_header": "**按ELO排序前 {count} 名玩家:**",
"player_line_full": "{rank}. **{nick}** ⭐ {score} • {games} 场 • K/D {kdr}",
"squadron_header_line": "中队 ELO:{score} • {games} 场 • 胜率 {wr}% • K/D {kdr}",
"squadron_header_no_aggregate": "中队 ELO:本周团队活动不足,无法评分。",
"no_data": "本次BR轮换中 [{tag}] 没有比赛记录。"
}
}
+438
View File
@@ -0,0 +1,438 @@
"""
lux_apis.py
Async client for the Spectra gaming API. Provides a WebSocket listener for real-time
replay streaming with auto-reconnect, HTTP functions for fetching squadron leaderboards,
and data transformation utilities to convert API responses into the local format.
"""
# Standard Library Imports
import asyncio
import json
import logging
import os
from typing import Any, Awaitable, Callable, Dict, List, Optional
# Third-Party Library Imports
import aiohttp
import pygob
import zstandard as zstd
from dotenv import load_dotenv
from websockets.asyncio.client import connect as wsconnect
# Local Module Imports
try:
from .data_parser import LangTableReader
from .utils import REPLAYS_DIR
except ImportError:
LangTableReader = None # Running directly, not as module
REPLAYS_DIR = None
# Load environment variables
load_dotenv()
logger = logging.getLogger(__name__)
# Global replay queue for WebSocket messages
_replay_queue: asyncio.Queue = asyncio.Queue()
# Constants from environment
WS_URL = os.getenv("SPECTRA_WS_SQB_URL", "")
API_KEY = os.getenv("SPECTRA_API_KEY", "")
SPECTRA_API_URL = os.getenv("SPECTRA_API_URL", "")
WS_GOB_URL = os.getenv("SPECTRA_WS_GOB_URL", "")
LEADERBOARD_PATH = "/v1/game/leaderboard"
REPLAY_SORT_PATH = "/v1/replays/sort"
# Initialize translation reader for vehicle names
translate = LangTableReader("English") if LangTableReader else None
def _gob_to_dict(obj: object) -> Any:
"""Recursively convert pygob namedtuples to plain dicts for JSON serialization."""
fields = getattr(obj, '_fields', None)
if isinstance(obj, tuple) and fields is not None:
return {f: _gob_to_dict(getattr(obj, f)) for f in fields}
elif isinstance(obj, list):
return [_gob_to_dict(i) for i in obj]
elif isinstance(obj, dict):
return {
(k.decode('utf-8', errors='replace') if isinstance(k, bytes) else k): _gob_to_dict(v)
for k, v in obj.items()
}
elif isinstance(obj, bytes):
return obj.decode('utf-8', errors='replace')
return obj
def normalize_ws_message(data: Any) -> Optional[List[Dict[str, Any]]]:
"""
Normalize WebSocket message to list of replay dicts.
Handles: full replay data, array of replays, or wrapped containers.
Returns:
List of replay dicts, or None if format is unknown
"""
# Case 1: Already a list of full replay objects
if isinstance(data, list) and data and isinstance(data[0], dict):
return data
# Case 2: Single replay object (WS sends '_id', not 'id')
if isinstance(data, dict) and ('_id' in data or 'id' in data) and ('teams' in data or 'players' in data):
return [data]
# Case 3: Wrapped in container (like fetch_replays response)
if isinstance(data, dict) and 'completed' in data:
return data['completed']
logger.warning(f"Unknown WS message format: {type(data)}")
return None
def get_replay_queue() -> asyncio.Queue:
"""Get the global replay queue for external access."""
return _replay_queue
async def ws_replay_listener(callback: Callable[[List[Dict[str, Any]]], Awaitable[None]]) -> None:
"""
Maintain persistent WebSocket connection to Spectra replay endpoint.
Queues incoming replays for sequential processing by the callback.
Auto-reconnects on disconnect with exponential backoff.
Args:
callback: Async function to call with normalized replay data
"""
headers = {'Authorization': API_KEY}
reconnect_delay = 1
# Start queue processor as background task
processor_task = asyncio.create_task(_process_replay_queue(callback))
async def _connect_and_listen(url: str, label: str):
logger.info(f"WS attempting connect → {url}")
async with wsconnect(url, additional_headers=headers) as ws:
logger.info(f"WebSocket connected to {label}")
async for message in ws:
try:
data = json.loads(message)
replays = normalize_ws_message(data)
if replays:
await _replay_queue.put(replays)
except json.JSONDecodeError:
logger.warning(f"Invalid JSON from WS: {message[:100]}")
except Exception as e:
logger.error(f"Error processing WS message: {e}")
try:
while True:
primary_url = WS_URL
primary_label = "Spectra"
try:
await _connect_and_listen(primary_url, primary_label)
except Exception as e:
logger.error(f"WebSocket error ({primary_label}): {e}")
# Reconnect with exponential backoff
logger.info(f"Reconnecting in {reconnect_delay}s...")
await asyncio.sleep(reconnect_delay)
reconnect_delay = min(reconnect_delay * 2, 30) # Cap at 30s
finally:
processor_task.cancel()
async def _process_replay_queue(callback: Callable[[List[Dict[str, Any]]], Awaitable[None]]) -> None:
"""
Process queued replays in batches.
Waits for at least one message, then drains queue and processes all at once.
"""
while True:
try:
# Wait for at least one message
replays = await _replay_queue.get()
batch = list(replays)
_replay_queue.task_done()
# Drain any others that accumulated while processing
while not _replay_queue.empty():
try:
more = _replay_queue.get_nowait()
batch.extend(more)
_replay_queue.task_done()
except asyncio.QueueEmpty:
break
if batch:
try:
await callback(batch)
except Exception as e:
logger.error(f"Error in replay callback: {e}")
except asyncio.CancelledError:
logger.info("Replay queue processor cancelled")
break
except Exception as e:
logger.error(f"Error in queue processor: {e}")
async def fetch_leaderboard(
count: int = 200,
start: int = 0,
short: bool = False
) -> Optional[List[Dict[str, Any]]]:
"""
Fetch squadron leaderboard data from Spectra API.
Args:
count: Number of clans to fetch (default 200)
start: Start position in leaderboard (default 0)
short: If True, exclude player data from response (default False)
Returns:
JSON response with leaderboard data, or None on failure
"""
url = f"{SPECTRA_API_URL}{LEADERBOARD_PATH}"
# API_KEY may or may not include "Bearer " prefix - handle both cases
auth_value = API_KEY if API_KEY.startswith("Bearer ") else f"Bearer {API_KEY}"
headers = {
"accept": "application/json",
"count": str(count),
"start": str(start),
"short": str(short).lower(),
"Authorization": auth_value
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
return data
else:
logger.error(f"Leaderboard API error: {response.status} - {await response.text()}")
except Exception as e:
logger.error(f"Failed to fetch leaderboard: {e}")
return None
async def fetch_leaderboard_bulk(
total_count: int = 1000,
batch_size: int = 200,
short: bool = False
) -> List[Dict[str, Any]]:
"""
Fetch multiple batches of leaderboard data.
Args:
total_count: Total number of clans to fetch (default 1000)
batch_size: Clans per request (default 200)
short: If True, exclude player data (default False)
Returns:
Combined list of all clan data from all batches
"""
all_clans = []
num_batches = (total_count + batch_size - 1) // batch_size
for i in range(num_batches):
start = i * batch_size
count = min(batch_size, total_count - start)
data = await fetch_leaderboard(count=count, start=start, short=short)
if data and isinstance(data, list):
all_clans.extend(data)
elif data:
logger.warning(f"Unexpected response format for batch {i+1}")
return all_clans
async def fetch_replay_by_id(
replay_id: str,
sort_field: str = "sqb"
) -> Optional[Dict[str, Any]]:
"""
Fetch a single replay by ID from Spectra API.
Args:
replay_id: The replay ID to fetch
sort_field: Game type sort field (default "sqb")
Returns:
Raw API response dict, or None on failure
"""
url = f"{SPECTRA_API_URL}{REPLAY_SORT_PATH}"
auth_value = API_KEY if API_KEY.startswith("Bearer ") else f"Bearer {API_KEY}"
headers = {
"accept": "application/json",
"Authorization": auth_value,
"sortField": sort_field,
"id": replay_id,
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers) as response:
if response.status == 200:
return await response.json()
else:
logger.error(f"fetch_replay_by_id error: {response.status} - {await response.text()}")
except Exception as e:
logger.error(f"Failed to fetch replay {replay_id}: {e}")
return None
async def test_leaderboard():
"""Test function to fetch leaderboard data and print results."""
print(f"Fetching leaderboard from {SPECTRA_API_URL}...")
print(f"API Key configured: {'Yes' if API_KEY else 'No'}")
if API_KEY:
# Show first/last few chars for debugging without exposing full key
masked = f"{API_KEY[:15]}...{API_KEY[-8:]}" if len(API_KEY) > 25 else "[short key]"
print(f"API Key format: {masked}")
print()
# Fetch 100 clans with player data
print("=== Fetching 100 clans in batches of 25 ===")
all_data = await fetch_leaderboard_bulk(total_count=100, batch_size=25, short=False)
print(f"Total clans fetched: {len(all_data)}")
# Write to JSON file
output_file = "leaderboard_test_output.json"
with open(output_file, "w") as f:
json.dump(all_data, f, indent=2, default=str)
print(f"Data written to {output_file}")
async def test_fetch_replay_by_id():
"""Test function to fetch a single replay by ID and print results."""
test_id = input("Enter replay ID to fetch: ").strip()
if not test_id:
print("No ID provided, aborting.")
return
print(f"Fetching replay {test_id} from {SPECTRA_API_URL}...")
data = await fetch_replay_by_id(test_id)
if data:
output_file = f"replay_{test_id}_output.json"
with open(output_file, "w") as f:
json.dump(data, f, indent=2, default=str)
print(f"Data written to {output_file}")
else:
print("No data returned.")
async def ws_gob_listener(callback: Callable[[bytes, bytes], Awaitable[None]]) -> None:
"""
Maintain persistent WebSocket connection to the Spectra SQB .gob endpoint.
Server pushes raw zstd-compressed .gob binary after each SQB replay is parsed.
Client does not send messages.
Args:
callback: Async function called with (compressed_bytes, decompressed_bytes)
"""
auth_value = API_KEY if API_KEY.startswith("Bearer ") else f"Bearer {API_KEY}"
headers = {"Authorization": auth_value}
decompressor = zstd.ZstdDecompressor()
reconnect_delay = 1
async def _connect_gob(url: str, label: str):
logger.info(f"GOB WS attempting connect → {url}")
async with wsconnect(url, additional_headers=headers) as ws:
logger.info(f"WebSocket connected to {label}")
reconnect_delay_ref = 1 # noqa: F841 — reset handled by caller
async for message in ws:
try:
raw = bytes(message) if isinstance(message, (bytes, bytearray, memoryview)) else message.encode()
data = decompressor.decompress(raw)
await callback(raw, data)
except zstd.ZstdError as e:
logger.error(f"zstd decompression failed: {e}")
except Exception as e:
logger.error(f"Error processing GOB message: {e}")
while True:
primary_url = WS_GOB_URL
primary_label = "Spectra GOB endpoint"
try:
await _connect_gob(primary_url, primary_label)
except Exception as e:
logger.error(f"GOB WebSocket error ({primary_label}): {e}")
logger.info(f"GOB WS reconnecting in {reconnect_delay}s...")
await asyncio.sleep(reconnect_delay)
reconnect_delay = min(reconnect_delay * 2, 30)
async def test_gob_ws():
"""
Connect to the SQB GOB WebSocket and dump received messages to files.
Each decompressed .gob blob is written to STORAGE/REPLAYS/<session_id>.gob for inspection.
"""
from pathlib import Path
if REPLAYS_DIR is None:
raise RuntimeError("REPLAYS_DIR is not configured")
replays_dir = REPLAYS_DIR
replays_dir.mkdir(parents=True, exist_ok=True)
auth_value = API_KEY if API_KEY.startswith("Bearer ") else f"Bearer {API_KEY}"
print(f"Connecting to {WS_GOB_URL}")
print(f"API Key configured: {'Yes' if API_KEY else 'No'}")
print(f"Saving to {replays_dir}")
print("Waiting for messages (Ctrl+C to stop)...\n")
decompressor = zstd.ZstdDecompressor()
count = 0
async with wsconnect(WS_GOB_URL, additional_headers={"Authorization": auth_value}) as ws:
print("Connected.")
async for message in ws:
raw = bytes(message) if isinstance(message, (bytes, bytearray, memoryview)) else message.encode()
print(f"[{count}] Received {len(raw)} bytes (compressed)")
data = b""
try:
data = decompressor.decompress(raw)
print(f"[{count}] Decompressed to {len(data)} bytes")
replay = pygob.load(data)
d = _gob_to_dict(replay)
session_id = d.get("SessionID", count)
out = replays_dir / f"{session_id}.json"
out.write_text(json.dumps(d, indent=2, default=str), encoding="utf-8")
print(f"[{count}] Decoded and written to {out}\n")
except zstd.ZstdError as e:
print(f"[{count}] zstd decompression failed: {e}")
out = replays_dir / f"gob_{count}_raw.bin"
out.write_bytes(raw)
print(f"[{count}] Raw bytes written to {out}\n")
except Exception as e:
print(f"[{count}] gob decode failed: {e}")
if data:
out = replays_dir / f"gob_{count}.gob"
out.write_bytes(data)
print(f"[{count}] Raw gob written to {out}\n")
count += 1
if __name__ == "__main__":
# Setup for direct execution
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
# Re-load env vars since module import was skipped
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("SPECTRA_API_KEY", "")
logging.basicConfig(level=logging.INFO)
mode = sys.argv[1] if len(sys.argv) > 1 else "replay"
if mode == "replay":
asyncio.run(test_fetch_replay_by_id())
elif mode == "gob":
asyncio.run(test_gob_ws())
+1180
View File
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
"""
receiver_bridge.py
Bridge helpers for external SREBOT transfer.
This module provides two pieces:
1. A formal SREBOT API client that external consumers can use to query the
SREBOT HTTP API.
2. A persistent outbox for replay and GOB payloads so the external bridge
service can fan them out over websocket.
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import quote
from typing import Any, Optional
import aiofiles
import aiohttp
logger = logging.getLogger(__name__)
def _env(name: str, default: str = "") -> str:
value = os.getenv(name, default)
return value.strip()
_storage_root_raw = _env("SREBOT_STORAGE_VOL_PATH")
if not _storage_root_raw:
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
_STORAGE_ROOT = Path(_storage_root_raw)
SREBOT_API_BASE_URL = _env("SREBOT_API_BASE_URL", _env("SREBOT_HTTP_URL", "http://127.0.0.1:6000")).rstrip("/")
SREBOT_API_BEARER_TOKEN = _env("SREBOT_API_BEARER_TOKEN")
EXTERNAL_OUTBOX_PATH = Path(_env("SREBOT_EXTERNAL_OUTBOX_PATH", str(_STORAGE_ROOT / "external_bridge_outbox.jsonl")))
EXTERNAL_OUTBOX_PATH.parent.mkdir(parents=True, exist_ok=True)
@dataclass(slots=True)
class SREBOTApiClient:
"""Typed HTTP client for the SREBOT read-only API."""
base_url: str = SREBOT_API_BASE_URL
bearer_token: str = SREBOT_API_BEARER_TOKEN
timeout_seconds: float = 30.0
def _headers(self) -> dict[str, str]:
headers = {"Accept": "application/json"}
if self.bearer_token:
headers["Authorization"] = f"Bearer {self.bearer_token}"
return headers
async def _request(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
url = f"{self.base_url}/{path.lstrip('/')}"
timeout = aiohttp.ClientTimeout(total=self.timeout_seconds)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=self._headers(), params=params) as response:
response.raise_for_status()
return await response.json()
async def get_info(self) -> Any:
return await self._request("/api/info")
async def get_player(self, uid: str, **params: Any) -> Any:
return await self._request(f"/api/player/{quote(str(uid), safe='')}", params=params or None)
async def get_player_games(self, uid: str, **params: Any) -> Any:
return await self._request(f"/api/player/{quote(str(uid), safe='')}/games", params=params or None)
async def get_player_history(self, uid: str) -> Any:
return await self._request(f"/api/player/{quote(str(uid), safe='')}/history")
async def search_players(self, nickname: str) -> Any:
return await self._request(f"/api/search/{quote(str(nickname), safe='')}")
async def get_live(self, **params: Any) -> Any:
return await self._request("/api/live", params=params or None)
async def get_match(self, session_id: str) -> Any:
return await self._request(f"/api/match/{quote(str(session_id), safe='')}")
async def get_match_replay(self, session_id: str) -> Any:
return await self._request(f"/api/match/{quote(str(session_id), safe='')}/replay")
async def search_games(self, **params: Any) -> Any:
return await self._request("/api/games/search", params=params or None)
async def get_maps(self) -> Any:
return await self._request("/api/maps")
async def get_squadron(self, squadron_name: str, **params: Any) -> Any:
return await self._request(f"/api/squadrons/{quote(str(squadron_name), safe='')}", params=params or None)
async def get_leaderboard_players(self, **params: Any) -> Any:
return await self._request("/api/leaderboard/players", params=params or None)
async def get_leaderboard_squadrons(self, **params: Any) -> Any:
return await self._request("/api/leaderboard/squadrons", params=params or None)
async def get_leaderboard_vehicles(self, **params: Any) -> Any:
return await self._request("/api/leaderboard/vehicles", params=params or None)
async def get_leaderboard_stats(self) -> Any:
return await self._request("/api/leaderboard/stats")
_default_api_client = SREBOTApiClient()
async def fetch_api_info() -> Any:
return await _default_api_client.get_info()
async def fetch_player(uid: str, **params: Any) -> Any:
return await _default_api_client.get_player(uid, **params)
async def fetch_player_games(uid: str, **params: Any) -> Any:
return await _default_api_client.get_player_games(uid, **params)
async def fetch_player_history(uid: str) -> Any:
return await _default_api_client.get_player_history(uid)
async def search_players(nickname: str) -> Any:
return await _default_api_client.search_players(nickname)
async def fetch_live(**params: Any) -> Any:
return await _default_api_client.get_live(**params)
async def fetch_match(session_id: str) -> Any:
return await _default_api_client.get_match(session_id)
async def fetch_match_replay(session_id: str) -> Any:
return await _default_api_client.get_match_replay(session_id)
async def search_games(**params: Any) -> Any:
return await _default_api_client.search_games(**params)
async def fetch_maps() -> Any:
return await _default_api_client.get_maps()
async def fetch_squadron(squadron_name: str, **params: Any) -> Any:
return await _default_api_client.get_squadron(squadron_name, **params)
async def fetch_leaderboard_players(**params: Any) -> Any:
return await _default_api_client.get_leaderboard_players(**params)
async def fetch_leaderboard_squadrons(**params: Any) -> Any:
return await _default_api_client.get_leaderboard_squadrons(**params)
async def fetch_leaderboard_vehicles(**params: Any) -> Any:
return await _default_api_client.get_leaderboard_vehicles(**params)
async def fetch_leaderboard_stats() -> Any:
return await _default_api_client.get_leaderboard_stats()
_EXTERNAL_OUTBOX_LOCK: asyncio.Lock | None = None
def _get_external_outbox_lock() -> asyncio.Lock:
global _EXTERNAL_OUTBOX_LOCK
if _EXTERNAL_OUTBOX_LOCK is None:
_EXTERNAL_OUTBOX_LOCK = asyncio.Lock()
return _EXTERNAL_OUTBOX_LOCK
async def _append_external_envelope(envelope: dict[str, Any]) -> None:
line = json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
async with _get_external_outbox_lock():
async with aiofiles.open(EXTERNAL_OUTBOX_PATH, "a", encoding="utf-8") as handle:
await handle.write(line + "\n")
logger.info(
"Bridge envelope queued",
extra={
"event_type": envelope.get("type"),
"outbox_path": str(EXTERNAL_OUTBOX_PATH),
},
)
async def publish_replay_batch(replays: list[dict[str, Any]]) -> None:
"""Queue a replay batch for websocket delivery by the external bridge."""
if not replays:
return
envelope = {
"type": "spectra.replay_batch",
"version": 1,
"source": "srebot",
"payload": {"replays": replays},
}
await _append_external_envelope(envelope)
async def publish_gob_payload(payload: dict[str, Any]) -> None:
"""Queue a GOB payload for websocket delivery by the external bridge."""
envelope = {
"type": "spectra.gob",
"version": 1,
"source": "srebot",
"payload": payload,
}
await _append_external_envelope(envelope)
async def publish_event(event_type: str, payload: dict[str, Any]) -> None:
"""Generic queue helper for future bridge events."""
envelope = {
"type": event_type,
"version": 1,
"source": "srebot",
"payload": payload,
}
await _append_external_envelope(envelope)
+1805
View File
File diff suppressed because it is too large Load Diff
+1272
View File
File diff suppressed because it is too large Load Diff
+360
View File
@@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""External SREBOT bridge service.
This PM2-managed process does two things:
1. Proxies read-only SREBOT queries on the external port.
2. Broadcasts SREBOT replay/GOB envelopes over websocket to any connected
client.
"""
from __future__ import annotations
import asyncio
import json
import logging
import os
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import aiohttp
from aiohttp import web
from dotenv import load_dotenv
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
load_dotenv()
from BOT.receiver_bridge import EXTERNAL_OUTBOX_PATH # noqa: E402
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] [%(levelname)s] [srebot-external] %(message)s",
)
logger = logging.getLogger("srebot-external")
def _env(name: str, default: str = "") -> str:
return os.getenv(name, default).strip()
@dataclass(slots=True)
class ExternalSettings:
host: str = _env("SREBOT_EXTERNAL_HOST", "0.0.0.0")
port: int = int(_env("SREBOT_EXTERNAL_PORT", "18081"))
bearer_token: str = _env("SREBOT_EXTERNAL_BEARER_TOKEN", _env("SREBOT_API_BEARER_TOKEN"))
upstream_url: str = _env("SREBOT_EXTERNAL_UPSTREAM_URL", "http://127.0.0.1:6000").rstrip("/")
upstream_bearer_token: str = _env("SREBOT_EXTERNAL_UPSTREAM_BEARER_TOKEN", _env("SREBOT_API_BEARER_TOKEN"))
outbox_path: Path = Path(_env("SREBOT_EXTERNAL_OUTBOX_PATH", str(EXTERNAL_OUTBOX_PATH)))
offset_path: Path = Path(_env("SREBOT_EXTERNAL_OFFSET_PATH", str(Path(str(EXTERNAL_OUTBOX_PATH)).with_suffix(".offset"))))
poll_interval_seconds: float = float(_env("SREBOT_EXTERNAL_POLL_INTERVAL", "0.5"))
reconnect_delay_seconds: float = float(_env("SREBOT_EXTERNAL_RECONNECT_DELAY", "1.0"))
SETTINGS = ExternalSettings()
SETTINGS.outbox_path.parent.mkdir(parents=True, exist_ok=True)
SETTINGS.offset_path.parent.mkdir(parents=True, exist_ok=True)
HOP_BY_HOP_HEADERS = {
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
}
CONNECTED_WEBSOCKETS: set[web.WebSocketResponse] = set()
CONNECTED_LOCK = asyncio.Lock()
def _auth_ok(request: web.Request) -> bool:
if not SETTINGS.bearer_token:
return True
return request.headers.get("Authorization", "") == f"Bearer {SETTINGS.bearer_token}"
@web.middleware
async def auth_middleware(request: web.Request, handler):
if request.path in {"/health", "/"} or request.path.startswith("/ws/"):
return await handler(request)
if not _auth_ok(request):
logger.warning("Unauthorized request", extra={"path": request.rel_url.path_qs})
return web.json_response({"error": "Unauthorized"}, status=401)
return await handler(request)
def _upstream_headers() -> dict[str, str]:
headers = {"Accept": "application/json"}
if SETTINGS.upstream_bearer_token:
headers["Authorization"] = f"Bearer {SETTINGS.upstream_bearer_token}"
return headers
def _read_offset() -> int:
try:
return int(SETTINGS.offset_path.read_text(encoding="utf-8").strip())
except Exception:
return 0
def _write_offset(offset: int) -> None:
SETTINGS.offset_path.write_text(str(offset), encoding="utf-8")
async def health(_: web.Request) -> web.Response:
return web.json_response(
{
"status": "ok",
"service": "srebot-external",
"http": SETTINGS.upstream_url,
"websocket": "/ws/srebot",
}
)
async def proxy_api(request: web.Request) -> web.StreamResponse:
target = f"{SETTINGS.upstream_url}{request.rel_url.path_qs}"
request_start = time.monotonic()
logger.info(
"AXBot query in",
extra={
"method": request.method,
"path": request.rel_url.path_qs,
},
)
body = await request.read() if request.can_read_body else b""
async with request.app["http_session"].request(
request.method,
target,
headers=_upstream_headers(),
data=body if body else None,
) as upstream:
payload = await upstream.read()
duration_ms = round((time.monotonic() - request_start) * 1000, 1)
logger.info(
"AXBot query out",
extra={
"method": request.method,
"path": request.rel_url.path_qs,
"status": upstream.status,
"bytes": len(payload),
"duration_ms": duration_ms,
},
)
headers = {
key: value
for key, value in upstream.headers.items()
if key.lower() not in HOP_BY_HOP_HEADERS
and key.lower() not in {"content-length", "content-encoding"}
}
return web.Response(body=payload, status=upstream.status, headers=headers)
async def root(_: web.Request) -> web.Response:
return web.json_response(
{
"service": "srebot-external",
"message": "Use /api/* for queries and /ws/srebot for replay/gob events.",
}
)
async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
if not _auth_ok(request):
logger.warning("Unauthorized websocket", extra={"path": request.rel_url.path_qs})
ws = web.WebSocketResponse()
await ws.prepare(request)
await ws.close(code=1008, message=b"Unauthorized")
return ws
ws = web.WebSocketResponse(heartbeat=20)
await ws.prepare(request)
async with CONNECTED_LOCK:
CONNECTED_WEBSOCKETS.add(ws)
logger.info("Websocket connected", extra={"clients": len(CONNECTED_WEBSOCKETS)})
try:
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
logger.info("Websocket recv", extra={"bytes": len(msg.data)})
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.warning("Websocket error", extra={"error": str(ws.exception())})
finally:
async with CONNECTED_LOCK:
CONNECTED_WEBSOCKETS.discard(ws)
logger.info("Websocket disconnected", extra={"clients": len(CONNECTED_WEBSOCKETS)})
return ws
async def _broadcast(envelope: dict[str, Any]) -> None:
serialized = json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
async with CONNECTED_LOCK:
targets = list(CONNECTED_WEBSOCKETS)
if not targets:
logger.info(
"No websocket clients connected",
extra={"event_type": envelope.get("type")},
)
return
dead: list[web.WebSocketResponse] = []
for ws in targets:
try:
await ws.send_str(serialized)
except Exception as exc:
logger.warning(
"Failed to send websocket envelope",
extra={"event_type": envelope.get("type"), "error": str(exc)},
)
dead.append(ws)
if dead:
async with CONNECTED_LOCK:
for ws in dead:
CONNECTED_WEBSOCKETS.discard(ws)
logger.info(
"Websocket broadcast",
extra={
"event_type": envelope.get("type"),
"clients": len(targets) - len(dead),
"payload_keys": list((envelope.get("payload") or {}).keys())[:8],
},
)
# Truncate the outbox once we've consumed past this many bytes. The file is
# append-only and previously grew unbounded — we observed it at 1.9 GB on disk
# with all data already relayed and offset matching size. Truncating when
# fully caught up keeps disk usage flat without cooperating with the writer.
# Race: a writer in the BOT process may append between the size check and
# the truncate call. Those envelopes would be lost, but envelopes here are
# best-effort match-replay events; rare loss during a 100 MB-scale rotation
# is acceptable.
_OUTBOX_TRUNCATE_THRESHOLD_BYTES = int(_env("SREBOT_EXTERNAL_TRUNCATE_BYTES", str(100 * 1024 * 1024)))
def _maybe_truncate_outbox(position: int) -> int:
try:
current_size = SETTINGS.outbox_path.stat().st_size
if (
position >= _OUTBOX_TRUNCATE_THRESHOLD_BYTES
and position == current_size
):
with SETTINGS.outbox_path.open("r+b") as handle:
handle.truncate(0)
_write_offset(0)
logger.info(
"Outbox caught up; truncated",
extra={"reclaimed_bytes": position},
)
return 0
except FileNotFoundError:
pass
except Exception as exc:
logger.warning("Outbox truncate failed", extra={"error": str(exc)})
return position
async def relay_outbox_loop(app: web.Application) -> None:
reconnect_delay = SETTINGS.reconnect_delay_seconds
position = _read_offset()
while True:
try:
if not SETTINGS.outbox_path.exists():
await asyncio.sleep(1.0)
continue
current_size = SETTINGS.outbox_path.stat().st_size
if position > current_size:
logger.info(
"Outbox truncated; resetting offset",
extra={"old_offset": position, "current_size": current_size},
)
position = 0
_write_offset(position)
with SETTINGS.outbox_path.open("r", encoding="utf-8") as handle:
handle.seek(position)
line = handle.readline()
if not line:
position = _maybe_truncate_outbox(position)
await asyncio.sleep(SETTINGS.poll_interval_seconds)
continue
try:
envelope = json.loads(line)
except json.JSONDecodeError:
position = handle.tell()
_write_offset(position)
logger.warning("Skipping malformed outbox line", extra={"offset": position})
continue
position = handle.tell()
_write_offset(position)
await _broadcast(envelope)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.warning(
"Bridge loop error",
extra={"error": str(exc), "retry_in_seconds": reconnect_delay},
)
await asyncio.sleep(reconnect_delay)
reconnect_delay = min(reconnect_delay * 2, 30.0)
async def create_http_session(app: web.Application):
app["http_session"] = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30))
try:
yield
finally:
await app["http_session"].close()
async def start_relay_task(app: web.Application):
task = asyncio.create_task(relay_outbox_loop(app))
app["relay_task"] = task
try:
yield
finally:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
def create_app() -> web.Application:
app = web.Application(middlewares=[auth_middleware])
app.router.add_get("/", root)
app.router.add_get("/health", health)
app.router.add_get("/ws/srebot", websocket_handler)
app.router.add_route("*", "/api/{tail:.*}", proxy_api)
app.cleanup_ctx.append(create_http_session)
app.cleanup_ctx.append(start_relay_task)
return app
def main() -> None:
web.run_app(create_app(), host=SETTINGS.host, port=SETTINGS.port)
if __name__ == "__main__":
main()
+1244
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+543
View File
@@ -0,0 +1,543 @@
"""
tasks.py
Scheduler definitions using discord.ext.tasks loops.
Thin wrappers that invoke the corresponding execute_* functions from task_executors.py
on configured intervals.
"""
# Standard Library Imports
import asyncio
import json
import logging
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
# Third-Party Library Imports
from discord.ext import tasks
# Local Module Imports
from . import lux_apis
from .autologging import handle_ws_replays, handle_gob_message
from .health import record_task_run, write_heartbeat
from .meta_manager import process_all_players, sync_all_guild_metas
from .task_executors import (
execute_ldb_alarm_task,
execute_points_alarm_task,
execute_leave_alarm_task,
execute_squadron_stats_tracker,
execute_update_squadrons_db_task,
execute_sync_squadron_members_bulk,
execute_sync_squadron_members_points_loop,
execute_cleanup_stale_squadrons,
execute_weekly_br_report_task,
init_squadrons_points_table,
cleanup_replays,
)
from .utils import (
get_bot,
STACKS_DIR,
refresh_entitled_guilds,
SQB_STATS_TRACKER_WINDOWS,
SQB_BOUNDARY_TIMES,
)
async def _record(task_name: str, success: bool, error: str = ""):
"""Record task execution for health dashboard."""
await record_task_run(task_name, success, error)
# ============================================================================
# LEADERBOARD ALARM TASK
# ============================================================================
@tasks.loop(seconds=60)
async def ldb_alarm_task():
"""Checks time and triggers leaderboard alarm at 22:35 and 7:35 UTC."""
now_utc = datetime.now(timezone.utc).time()
if (now_utc.hour, now_utc.minute) in [(22, 35), (7, 35)]:
try:
await execute_ldb_alarm_task()
await _record("ldb_alarm", True)
except Exception as e:
await _record("ldb_alarm", False, str(e))
raise
@ldb_alarm_task.before_loop
async def before_ldb_alarm_task():
await get_bot().wait_until_ready()
# ============================================================================
# SQUADRON STATS TRACKER TASKS
# ============================================================================
@tasks.loop(hours=1)
async def squadron_stats_tracker_task():
"""Track squadron points every hour during SQB timeslot hours."""
now_utc = datetime.now(timezone.utc).time()
if any(start <= now_utc <= end for _, start, end in SQB_STATS_TRACKER_WINDOWS):
try:
await execute_squadron_stats_tracker()
await _record("squadron_stats_tracker", True)
except Exception as e:
await _record("squadron_stats_tracker", False, str(e))
raise
@tasks.loop(seconds=60)
async def squadron_stats_boundary_task():
"""Track squadron points at timeslot boundaries (see SQB_BOUNDARY_TIMES in utils)."""
now_utc = datetime.now(timezone.utc)
if now_utc.time().replace(second=0, microsecond=0) in SQB_BOUNDARY_TIMES:
try:
await execute_squadron_stats_tracker()
await _record("squadron_stats_boundary", True)
except Exception as e:
await _record("squadron_stats_boundary", False, str(e))
raise
@squadron_stats_tracker_task.before_loop
async def before_squadron_stats_tracker_task():
await get_bot().wait_until_ready()
await init_squadrons_points_table()
@squadron_stats_boundary_task.before_loop
async def before_squadron_stats_boundary_task():
await get_bot().wait_until_ready()
# ============================================================================
# ENTITLEMENT CACHE REFRESH (hourly fallback)
# ============================================================================
@tasks.loop(hours=1)
async def entitlement_cache_task():
"""Refresh the entitlement cache hourly so new subscriptions are picked up
even when no autolog batches or points alarms are running."""
try:
await refresh_entitled_guilds(force=True)
await _record("entitlement_cache", True)
except Exception as e:
await _record("entitlement_cache", False, str(e))
raise
@entitlement_cache_task.before_loop
async def before_entitlement_cache():
await get_bot().wait_until_ready()
# ============================================================================
# POINTS ALARM TASK
# ============================================================================
@tasks.loop(seconds=60)
async def points_alarm_task():
"""Checks time and triggers points alarm at 22:25 and 7:25 UTC."""
now_utc = datetime.now(timezone.utc).time()
region = None
if now_utc.hour == 22 and now_utc.minute == 25:
region = "EU"
elif now_utc.hour == 7 and now_utc.minute == 25:
region = "NA"
if region:
try:
await execute_points_alarm_task(region)
await _record("points_alarm", True)
except Exception as e:
await _record("points_alarm", False, str(e))
raise
@points_alarm_task.before_loop
async def before_points_alarm_task():
await get_bot().wait_until_ready()
# ============================================================================
# REPLAY CLEANUP TASK
# ============================================================================
@tasks.loop(hours=4)
async def replay_cleanup_task():
"""Cleans up replay directories every 4 hours."""
try:
await cleanup_replays()
await _record("replay_cleanup", True)
except Exception as e:
await _record("replay_cleanup", False, str(e))
raise
@replay_cleanup_task.before_loop
async def before_replay_cleanup_task():
await get_bot().wait_until_ready()
# ============================================================================
# LEAVE ALARM TASK
# ============================================================================
@tasks.loop(minutes=30)
async def leave_alarm_task():
"""Runs leave alarm every 30 minutes."""
try:
await execute_leave_alarm_task()
await _record("leave_alarm", True)
except Exception as e:
await _record("leave_alarm", False, str(e))
raise
@leave_alarm_task.before_loop
async def before_leave_alarm_task():
await get_bot().wait_until_ready()
# ============================================================================
# UPDATE SQUADRONS DB TASK
# ============================================================================
_startup_db_done = False
@tasks.loop(hours=1)
async def update_squadrons_db_task():
"""
Every 1 hour:
1. Refresh the main squadrons.db via Game API.
2. Sync squadron member UIDs for top 1000 squadrons (bulk API).
3. Update the Bot's status
"""
global _startup_db_done
if not _startup_db_done:
_startup_db_done = True
return # Skip — already ran in _startup_heavy_init
try:
await execute_update_squadrons_db_task(count=1000)
await execute_sync_squadron_members_bulk(count=1000)
await execute_cleanup_stale_squadrons()
await _record("update_squadrons_db", True)
except Exception as e:
await _record("update_squadrons_db", False, str(e))
raise
@update_squadrons_db_task.before_loop
async def before_update_squadrons_db_task():
await get_bot().wait_until_ready()
# ============================================================================
# UPDATE META DATA TASK
# ============================================================================
@tasks.loop(hours=12)
async def update_meta_data_task():
"""
Every 12 hours:
Update all player vehicle data in Meta.db from the game API.
Skips players that were updated within 2 days.
"""
try:
await process_all_players(
limit=None,
skip_existing=True,
batch_size=50,
max_concurrent=5
)
await _record("update_meta_data", True)
except Exception as e:
await _record("update_meta_data", False, str(e))
logging.error(f"[META-DB] Error during meta data update: {e}")
@update_meta_data_task.before_loop
async def before_update_meta_data_task():
await get_bot().wait_until_ready()
# ============================================================================
# WEBSOCKET AUTOLOG TASK
# ============================================================================
@tasks.loop(count=1)
async def ws_autolog_task():
"""
Single-run task that maintains persistent WebSocket connection.
Replaces the polling-based auto_logging_task.
"""
await lux_apis.ws_replay_listener(handle_ws_replays)
@ws_autolog_task.before_loop
async def before_ws_autolog():
await get_bot().wait_until_ready()
@ws_autolog_task.after_loop
async def after_ws_autolog():
if ws_autolog_task.failed():
logging.error("[WS] ws_autolog_task died, restarting in 10s...")
await asyncio.sleep(10)
ws_autolog_task.start()
# ============================================================================
# WEBSOCKET GOB LISTENER TASK
# ============================================================================
@tasks.loop(count=1)
async def ws_gob_task():
"""
Single-run task that maintains persistent WebSocket connection to the GOB endpoint.
Saves incoming compressed GOB replays to disk for on-demand video generation.
"""
await lux_apis.ws_gob_listener(handle_gob_message)
@ws_gob_task.before_loop
async def before_ws_gob():
await get_bot().wait_until_ready()
@ws_gob_task.after_loop
async def after_ws_gob():
if ws_gob_task.failed():
logging.error("[GOB] ws_gob_task died, restarting in 10s...")
await asyncio.sleep(10)
ws_gob_task.start()
# ============================================================================
# SQUADRON POINTS CONTINUOUS UPDATER
# ============================================================================
@tasks.loop(count=1)
async def squadron_points_loop_task():
"""
Continuously update squadron member points forever.
Very slow (3s per clan), runs indefinitely in the background.
"""
await execute_sync_squadron_members_points_loop(count=1000, delay=3.0)
@squadron_points_loop_task.before_loop
async def before_squadron_points_loop():
await get_bot().wait_until_ready()
@squadron_points_loop_task.after_loop
async def after_squadron_points_loop():
if squadron_points_loop_task.failed():
logging.error("[SQ-POINTS] squadron_points_loop_task died, restarting in 10s...")
await asyncio.sleep(10)
squadron_points_loop_task.start()
# ============================================================================
# STACKS CLEANUP TASK
# ============================================================================
# Fires at 07:30 and 22:30 UTC — 20 minutes after each SQB timeslot ends.
_STACKS_CLEANUP_TIMES = [(7, 30), (22, 30)]
@tasks.loop(seconds=60)
async def stacks_cleanup_task():
"""Wipe all active stacks at the end of each SQB timeslot."""
now_utc = datetime.now(timezone.utc)
if (now_utc.hour, now_utc.minute) in _STACKS_CLEANUP_TIMES:
try:
if STACKS_DIR.exists():
shutil.rmtree(STACKS_DIR)
STACKS_DIR.mkdir(parents=True, exist_ok=True)
logging.info("[STACKS] Wiped STACKS_DIR at timeslot end.")
await _record("stacks_cleanup", True)
except Exception as e:
await _record("stacks_cleanup", False, str(e))
logging.error(f"[STACKS] Failed to wipe STACKS_DIR: {e}")
@stacks_cleanup_task.before_loop
async def before_stacks_cleanup_task():
await get_bot().wait_until_ready()
# ============================================================================
# HEALTH HEARTBEAT TASK
# ============================================================================
@tasks.loop(seconds=30)
async def health_heartbeat_task():
"""Write bot health snapshot every 30 seconds."""
await write_heartbeat()
@health_heartbeat_task.before_loop
async def before_health_heartbeat_task():
await get_bot().wait_until_ready()
# ============================================================================
# GUILD META MEMBER SYNC TASK
# ============================================================================
@tasks.loop(hours=24)
async def sync_guild_metas_task():
"""
Every 24 hours: for each configured guild, fetch the current squadron roster
from the Game API and reconcile Guild_Metas — adding new members and removing
players who have left the squadron.
"""
try:
await sync_all_guild_metas()
await _record("sync_guild_metas", True)
except Exception as e:
await _record("sync_guild_metas", False, str(e))
logging.error(f"[META-SYNC] Error during daily guild meta sync: {e}")
@sync_guild_metas_task.before_loop
async def before_sync_guild_metas_task():
await get_bot().wait_until_ready()
# ============================================================================
# WEEKLY BR REPORT TASK
# ============================================================================
# Fires ~10 min after each BR window ends (10 * 60 = 600s).
_WEEKLY_BR_FIRE_OFFSET_S = 600
_WEEKLY_BR_FIRE_TOLERANCE_S = 60 # tolerance window for the 60s loop tick
_SCHEDULE_PATH = Path(__file__).parent / "SCHEDULE.json"
def _just_ended_br_window(now_ts: int) -> Optional[Dict[str, Any]]:
"""Return the SCHEDULE.json entry whose end is `_WEEKLY_BR_FIRE_OFFSET_S`
seconds in the past (within `_WEEKLY_BR_FIRE_TOLERANCE_S`), or None.
"""
try:
with open(_SCHEDULE_PATH, "r", encoding="utf-8") as fp:
schedule = json.load(fp)
except Exception as e:
logging.error("[WBR] Failed to read SCHEDULE.json: %s", e)
return None
target = now_ts - _WEEKLY_BR_FIRE_OFFSET_S
for entry in schedule:
try:
end_ts = int(entry["end"])
except (KeyError, TypeError, ValueError):
continue
if target - _WEEKLY_BR_FIRE_TOLERANCE_S <= end_ts <= target:
return entry
return None
@tasks.loop(seconds=60)
async def weekly_br_report_task():
"""Fire the Weekly BR Report ~10 min after each BR window ends."""
now_ts = int(datetime.now(timezone.utc).timestamp())
window = _just_ended_br_window(now_ts)
if window is None:
return
try:
await execute_weekly_br_report_task(window)
await _record("weekly_br_report", True)
except Exception as e:
await _record("weekly_br_report", False, str(e))
raise
@weekly_br_report_task.before_loop
async def before_weekly_br_report_task():
await get_bot().wait_until_ready()
# ============================================================================
# TASK STARTER FUNCTION
# ============================================================================
async def _startup_heavy_init():
"""Run heavy DB/API tasks at startup, then start recurring tasks."""
try:
await execute_update_squadrons_db_task(count=1000)
except Exception as e:
logging.error(f"[STARTUP] squadrons.db update failed: {e}")
try:
await execute_sync_squadron_members_bulk(count=1000)
except Exception as e:
logging.error(f"[STARTUP] squadron members sync failed: {e}")
try:
await execute_cleanup_stale_squadrons()
except Exception as e:
logging.error(f"[STARTUP] stale squadron cleanup failed: {e}")
await asyncio.sleep(5)
# Recurring tasks (started after heavy init completes)
update_squadrons_db_task.start()
squadron_stats_tracker_task.start()
leave_alarm_task.start()
# Continuous background tasks
squadron_points_loop_task.start()
update_meta_data_task.start()
sync_guild_metas_task.start()
async def start_all_tasks():
"""
Start all background tasks. Heavy DB init runs in a background task
so on_ready returns quickly.
Order:
1. Lightweight time-check tasks (just check clock, no heavy work)
2. WebSocket listeners (persistent connections)
3. Heavy DB/API tasks (background — does not block on_ready)
"""
# Phase 1: Lightweight time-check tasks
entitlement_cache_task.start()
ldb_alarm_task.start()
squadron_stats_boundary_task.start()
points_alarm_task.start()
stacks_cleanup_task.start()
replay_cleanup_task.start()
health_heartbeat_task.start()
weekly_br_report_task.start()
# Phase 2: WebSocket listeners
ws_autolog_task.start()
ws_gob_task.start()
# Phase 3: Heavy DB tasks (background — doesn't block on_ready)
asyncio.create_task(_startup_heavy_init())
def stop_all_tasks():
"""Stop all background tasks. Call this on shutdown."""
entitlement_cache_task.cancel()
ldb_alarm_task.cancel()
squadron_stats_tracker_task.cancel()
squadron_stats_boundary_task.cancel()
points_alarm_task.cancel()
stacks_cleanup_task.cancel()
replay_cleanup_task.cancel()
leave_alarm_task.cancel()
update_squadrons_db_task.cancel()
update_meta_data_task.cancel()
ws_autolog_task.cancel()
ws_gob_task.cancel()
squadron_points_loop_task.cancel()
sync_guild_metas_task.cancel()
health_heartbeat_task.cancel()
weekly_br_report_task.cancel()
+81
View File
@@ -0,0 +1,81 @@
"""
Smoke test for BOT/render_recap.py in --mode player.
Usage:
source .venv/bin/activate && python BOT/tests/smoke_player_recap.py
"""
import sqlite3
import os
import subprocess
import sys
import tempfile
from pathlib import Path
_storage_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
if not _storage_env:
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
STORAGE = Path(_storage_env)
SEASON_START = "1772348400"
SEASON_END = "1777852799"
SEASON = "2026-II"
def pick_uid() -> str:
conn = sqlite3.connect(f"file:{STORAGE/'sq_battles.db'}?mode=ro", uri=True)
cur = conn.execute(
"SELECT UID FROM player_games_hist "
"WHERE endtime_unix BETWEEN ? AND ? "
"GROUP BY UID ORDER BY COUNT(*) DESC LIMIT 1",
(int(SEASON_START), int(SEASON_END)),
)
row = cur.fetchone()
conn.close()
return row[0] if row else ""
def run_smoke(uid: str, theme: str = "dark", lang: str = "en") -> int:
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / f"player-{theme}-{lang}.png"
cmd = [
sys.executable,
"BOT/render_recap.py",
"--mode", "player",
"--uid", uid,
"--season", SEASON,
"--season-start", SEASON_START,
"--season-end", SEASON_END,
"--theme", theme,
"--lang", lang,
"--out", str(out),
]
print("Running:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True)
print("stdout:", result.stdout)
print("stderr:", result.stderr)
if result.returncode != 0:
print(f"FAIL: exit={result.returncode}")
return result.returncode
size = out.stat().st_size if out.exists() else 0
if size < 10_000:
print(f"FAIL: suspicious size {size} bytes at {out}")
return 1
print(f"OK: wrote {size} bytes ({theme}/{lang})")
return 0
def main() -> int:
uid = pick_uid()
if not uid:
print("FAIL: no UID with season activity available for smoke test")
return 1
print(f"Using UID {uid}")
code = 0
for theme in ("light", "dark"):
for lang in ("en", "ru"):
code |= run_smoke(uid, theme=theme, lang=lang)
return code
if __name__ == "__main__":
sys.exit(main())
+50
View File
@@ -0,0 +1,50 @@
"""
Smoke test for BOT/render_recap.py.
Runs the renderer end-to-end against the live HC storage volume databases
and verifies a PNG lands at the --out path.
Usage:
source .venv/bin/activate && python BOT/tests/smoke_recap.py
"""
import subprocess
import sys
import tempfile
from pathlib import Path
def run_smoke(clan_id: int = 123456, season: str = "2026-II") -> int:
"""Run the renderer for a given clan/season; return exit code."""
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "card.png"
cmd = [
sys.executable,
"BOT/render_recap.py",
"--mode", "squadron",
"--clan-id", str(clan_id),
"--season", season,
"--season-start", "1772348400",
"--season-end", "1777852799",
"--out", str(out),
]
print("Running:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True)
print("stdout:", result.stdout)
print("stderr:", result.stderr)
if result.returncode != 0:
print(f"FAIL: exit={result.returncode}")
return result.returncode
if not out.exists():
print(f"FAIL: {out} does not exist")
return 1
if out.stat().st_size == 0:
print(f"FAIL: {out} is empty")
return 1
print(f"OK: wrote {out.stat().st_size} bytes")
return 0
if __name__ == "__main__":
code = run_smoke()
sys.exit(code)
+2016
View File
File diff suppressed because it is too large Load Diff
+450
View File
@@ -0,0 +1,450 @@
"""
weekly_br_elo.py
Window-scoped player & squadron ELO scoring. Python port of the
computePerformanceScore + ratingCtes pipeline from server.js, used by the
Weekly BR Report executor.
The percentile distribution (benchmark) is built from the same in-window
dataset, so scores represent ranking against peers who also played that week.
"""
# Standard Library Imports
import logging
import math
from typing import Any, Dict, List, Optional, Tuple
# Third-Party Library Imports
import aiosqlite
# Local Module Imports
from .utils import SQ_BATTLES_DB_PATH
# Score weights (mirror server.js:631 computePerformanceScore)
_W_KDR = 0.32
_W_HEAVY_RATE = 0.23
_W_KILLS_PER_GAME = 0.14
_W_GAMES_LOG = 0.10
_W_WIN_RATE = 0.06
_W_ASSISTS_PER_GAME = 0.06
_W_CAPTURES_PER_GAME = 0.04
_W_DEATHS_PER_GAME = 0.05 # lower-is-better
_METRIC_KEYS: Tuple[str, ...] = (
"kdr",
"kills_per_game",
"heavy_rate",
"win_rate",
"assists_per_game",
"captures_per_game",
"deaths_per_game",
"games_log",
)
def _rating_ctes(entity_expr: str) -> str:
"""Return the WITH-clause CTEs that produce per-session heavy scores.
Direct port of server.js:647 ratingCtes(). The window predicate uses
`endtime_unix BETWEEN ? AND ?` -- params order: (start_ts, end_ts).
"""
return f"""
WITH player_sessions AS (
SELECT
{entity_expr} AS entity_key,
p.UID,
p.session_id,
SUM(p.ground_kills + p.air_kills) AS kills,
SUM(p.assists) AS assists,
SUM(p.captures) AS captures,
SUM(p.deaths) AS deaths,
MAX(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END) AS win
FROM player_games_hist p
WHERE p.UID IS NOT NULL
AND p.nick NOT LIKE 'coop/%'
AND p.endtime_unix >= ?
AND p.endtime_unix <= ?
GROUP BY entity_key, p.UID, p.session_id
),
leader_stats AS (
SELECT ps.session_id, MAX(ps.kills) AS max_kills
FROM player_sessions ps
GROUP BY ps.session_id
),
leader_counts AS (
SELECT ps.session_id, COUNT(*) AS top_count
FROM player_sessions ps
JOIN leader_stats ls ON ls.session_id = ps.session_id
AND ls.max_kills = ps.kills
GROUP BY ps.session_id
),
second_stats AS (
SELECT ps.session_id, MAX(ps.kills) AS second_kills
FROM player_sessions ps
JOIN leader_stats ls ON ls.session_id = ps.session_id
WHERE ps.kills < ls.max_kills
GROUP BY ps.session_id
),
scored_sessions AS (
SELECT
ps.*,
CASE
WHEN ps.kills >= 3
AND ps.kills = ls.max_kills
AND lc.top_count = 1
AND (ps.kills - COALESCE(ss.second_kills, 0)) >= 2
THEN MIN(1.0, (ps.kills - 2) / 2.0)
WHEN ps.kills >= 3
AND ps.kills = ls.max_kills
AND lc.top_count = 1
THEN 0.6 * MIN(1.0, (ps.kills - 2) / 2.0)
ELSE 0
END AS heavy_score
FROM player_sessions ps
JOIN leader_stats ls ON ls.session_id = ps.session_id
JOIN leader_counts lc ON lc.session_id = ps.session_id
LEFT JOIN second_stats ss ON ss.session_id = ps.session_id
)
"""
async def _fetch_rows(sql: str, params: Tuple[Any, ...]) -> List[Dict[str, Any]]:
"""Run a read-only query against sq_battles.db and return list-of-dicts."""
async with aiosqlite.connect(
f"file:{SQ_BATTLES_DB_PATH}?mode=ro", uri=True, timeout=30.0
) as db:
await db.execute("PRAGMA busy_timeout=30000;")
async with db.execute(sql, params) as cursor:
cols = [c[0] for c in (cursor.description or [])]
rows = await cursor.fetchall()
return [dict(zip(cols, r)) for r in rows]
async def _query_player_aggregates(
start_ts: int, end_ts: int
) -> List[Dict[str, Any]]:
sql = _rating_ctes("p.UID") + """
SELECT
entity_key AS uid,
COUNT(*) AS games,
SUM(kills) AS total_kills,
SUM(assists) AS total_assists,
SUM(captures) AS total_captures,
SUM(deaths) AS total_deaths,
SUM(win) AS wins,
SUM(heavy_score) AS heavy_score
FROM scored_sessions
GROUP BY entity_key
"""
return await _fetch_rows(sql, (start_ts, end_ts))
async def _query_squadron_aggregates(
start_ts: int, end_ts: int
) -> List[Dict[str, Any]]:
sql = _rating_ctes("p.squadron_name") + """,
squadron_sessions AS (
SELECT
entity_key,
session_id,
SUM(kills) AS kills,
SUM(assists) AS assists,
SUM(captures) AS captures,
SUM(deaths) AS deaths,
MAX(win) AS win,
SUM(heavy_score) AS heavy_score
FROM scored_sessions
WHERE entity_key IS NOT NULL
AND entity_key != 'UNKNOWN'
AND entity_key != ''
GROUP BY entity_key, session_id
)
SELECT
entity_key AS squadron_name,
COUNT(*) AS games,
SUM(kills) AS total_kills,
SUM(assists) AS total_assists,
SUM(captures) AS total_captures,
SUM(deaths) AS total_deaths,
SUM(win) AS wins,
SUM(heavy_score) AS heavy_score
FROM squadron_sessions
GROUP BY entity_key
"""
return await _fetch_rows(sql, (start_ts, end_ts))
async def _query_uid_to_meta(
start_ts: int, end_ts: int
) -> Dict[str, Tuple[str, str]]:
"""Map UID -> (most-recent nick, most-recent squadron_name) within window."""
sql = """
SELECT
p.UID AS uid,
p.nick AS nick,
p.squadron_name AS squadron_name,
MAX(p.endtime_unix) AS last_seen
FROM player_games_hist p
WHERE p.UID IS NOT NULL
AND p.nick NOT LIKE 'coop/%'
AND p.endtime_unix >= ?
AND p.endtime_unix <= ?
GROUP BY p.UID
"""
rows = await _fetch_rows(sql, (start_ts, end_ts))
out: Dict[str, Tuple[str, str]] = {}
for r in rows:
uid = str(r.get("uid") or "")
if not uid:
continue
out[uid] = (str(r.get("nick") or ""), str(r.get("squadron_name") or ""))
return out
def _normalize_rating_stats(row: Dict[str, Any]) -> Dict[str, float]:
games = max(0, int(row.get("games") or 0))
kills = max(0, int(row.get("total_kills") or 0))
deaths = max(0, int(row.get("total_deaths") or 0))
assists = max(0, int(row.get("total_assists") or 0))
captures = max(0, int(row.get("total_captures") or 0))
wins = max(0, int(row.get("wins") or 0))
heavy = max(0.0, float(row.get("heavy_score") or 0))
return {
"games": float(games),
"kdr": (kills / deaths) if deaths > 0 else float(kills),
"kills_per_game": (kills / games) if games > 0 else 0.0,
"heavy_rate": (heavy / games) if games > 0 else 0.0,
"win_rate": (wins / games * 100.0) if games > 0 else 0.0,
"assists_per_game": (assists / games) if games > 0 else 0.0,
"captures_per_game": (captures / games) if games > 0 else 0.0,
"deaths_per_game": (deaths / games) if games > 0 else 0.0,
"games_log": math.log1p(games),
}
def _build_benchmark(rows: List[Dict[str, Any]]) -> Dict[str, List[float]]:
"""Build sorted percentile distributions for each scoring metric."""
normalized = [_normalize_rating_stats(r) for r in rows]
return {m: sorted(float(n[m]) for n in normalized) for m in _METRIC_KEYS}
def _upper_bound(values: List[float], v: float) -> int:
lo, hi = 0, len(values)
while lo < hi:
mid = (lo + hi) // 2
if values[mid] <= v:
lo = mid + 1
else:
hi = mid
return lo
def _lower_bound(values: List[float], v: float) -> int:
lo, hi = 0, len(values)
while lo < hi:
mid = (lo + hi) // 2
if values[mid] < v:
lo = mid + 1
else:
hi = mid
return lo
def _metric_percentile(
value: float, values: List[float], lower_is_better: bool = False
) -> float:
if not values:
return 0.0
if lower_is_better:
return (len(values) - _lower_bound(values, value)) / len(values)
return _upper_bound(values, value) / len(values)
def _compute_performance_score(
row: Dict[str, Any], benchmark: Dict[str, List[float]]
) -> float:
s = _normalize_rating_stats(row)
weighted = (
_W_KDR * _metric_percentile(s["kdr"], benchmark["kdr"])
+ _W_HEAVY_RATE * _metric_percentile(s["heavy_rate"], benchmark["heavy_rate"])
+ _W_KILLS_PER_GAME
* _metric_percentile(s["kills_per_game"], benchmark["kills_per_game"])
+ _W_GAMES_LOG * _metric_percentile(s["games_log"], benchmark["games_log"])
+ _W_WIN_RATE * _metric_percentile(s["win_rate"], benchmark["win_rate"])
+ _W_ASSISTS_PER_GAME
* _metric_percentile(s["assists_per_game"], benchmark["assists_per_game"])
+ _W_CAPTURES_PER_GAME
* _metric_percentile(s["captures_per_game"], benchmark["captures_per_game"])
+ _W_DEATHS_PER_GAME
* _metric_percentile(
s["deaths_per_game"], benchmark["deaths_per_game"], lower_is_better=True
)
)
sample = max(0.0, min(1.0, math.sqrt(s["games"] / 10.0)))
return round(max(0.0, min(5.0, 5.0 * weighted * sample)), 2)
async def player_scores(start_ts: int, end_ts: int) -> Dict[str, Dict[str, Any]]:
"""Return {uid: {nick, squadron_name, score, games, kdr, win_rate, kills_per_game}}."""
rows = await _query_player_aggregates(start_ts, end_ts)
if not rows:
return {}
benchmark = _build_benchmark(rows)
meta = await _query_uid_to_meta(start_ts, end_ts)
out: Dict[str, Dict[str, Any]] = {}
for r in rows:
uid = str(r.get("uid") or "")
if not uid:
continue
norm = _normalize_rating_stats(r)
score = _compute_performance_score(r, benchmark)
nick, squadron = meta.get(uid, ("", ""))
out[uid] = {
"uid": uid,
"nick": nick,
"squadron_name": squadron,
"score": score,
"games": int(norm["games"]),
"kdr": norm["kdr"],
"win_rate": norm["win_rate"],
"kills_per_game": norm["kills_per_game"],
}
return out
async def squadron_scores(start_ts: int, end_ts: int) -> Dict[str, Dict[str, Any]]:
"""Return {squadron_name: {score, games, kdr, win_rate, kills_per_game}}."""
rows = await _query_squadron_aggregates(start_ts, end_ts)
if not rows:
return {}
benchmark = _build_benchmark(rows)
out: Dict[str, Dict[str, Any]] = {}
for r in rows:
sq = str(r.get("squadron_name") or "")
if not sq:
continue
norm = _normalize_rating_stats(r)
score = _compute_performance_score(r, benchmark)
out[sq] = {
"squadron_name": sq,
"score": score,
"games": int(norm["games"]),
"kdr": norm["kdr"],
"win_rate": norm["win_rate"],
"kills_per_game": norm["kills_per_game"],
}
return out
def top_n_squadrons_with_top_k_players_from_maps(
sq_map: Dict[str, Dict[str, Any]],
pl_map: Dict[str, Dict[str, Any]],
n: int = 20,
k: int = 5,
) -> List[Dict[str, Any]]:
"""Sync helper: top N squadrons + top K players from already-computed maps.
Lets a caller pay the cost of `squadron_scores`/`player_scores` once and
then synthesize multiple report payloads (wildcard + per-squadron) from
the same data without re-hitting the DB.
"""
ranked = sorted(
sq_map.values(),
key=lambda r: (-float(r["score"]), -int(r["games"]), str(r["squadron_name"])),
)[:n]
by_squadron: Dict[str, List[Dict[str, Any]]] = {}
for p in pl_map.values():
s = str(p.get("squadron_name") or "")
if not s:
continue
by_squadron.setdefault(s, []).append(p)
payload: List[Dict[str, Any]] = []
for r in ranked:
s = str(r["squadron_name"])
roster = sorted(
by_squadron.get(s, []),
key=lambda p: (-float(p["score"]), -int(p["games"]), str(p.get("nick") or "")),
)[:k]
payload.append({**r, "top_players": roster})
return payload
def squadron_report_for_variants_from_maps(
sq_map: Dict[str, Dict[str, Any]],
pl_map: Dict[str, Dict[str, Any]],
name_variants: List[str],
k: int = 15,
) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]:
"""Sync helper: per-squadron payload from already-computed maps."""
sq_row: Optional[Dict[str, Any]] = None
for v in name_variants:
if not v:
continue
if v in sq_map:
sq_row = sq_map[v]
break
variant_set = {v for v in name_variants if v}
roster = [p for p in pl_map.values() if str(p.get("squadron_name") or "") in variant_set]
roster.sort(
key=lambda p: (-float(p["score"]), -int(p["games"]), str(p.get("nick") or ""))
)
return sq_row, roster[:k]
async def top_n_squadrons_with_top_k_players(
start_ts: int, end_ts: int, n: int = 20, k: int = 5
) -> List[Dict[str, Any]]:
"""Wildcard-mode payload: top N squadrons by score, each with their top K players.
Convenience wrapper that fetches the maps then calls the `_from_maps` helper.
Prefer the explicit two-step (compute maps once, call from-maps multiple
times) when building reports for many subscribers.
"""
sq_map = await squadron_scores(start_ts, end_ts)
pl_map = await player_scores(start_ts, end_ts)
return top_n_squadrons_with_top_k_players_from_maps(sq_map, pl_map, n=n, k=k)
async def squadron_report_for_variants(
start_ts: int,
end_ts: int,
name_variants: List[str],
k: int = 15,
) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]:
"""Per-squadron mode payload: best-matching squadron row + its top K players.
Tries name_variants in order against the squadron name appearing in
player_games_hist (long_name, tag_name, short_name). Returns (None, [])
if no variant matched and no players in window carry any of the variants.
"""
sq_map = await squadron_scores(start_ts, end_ts)
pl_map = await player_scores(start_ts, end_ts)
return squadron_report_for_variants_from_maps(sq_map, pl_map, name_variants, k=k)
def br_color_int(max_br: float) -> int:
"""Map a BR value to an embed sidebar color (matches /schedule color bands)."""
if max_br >= 13.0:
return 0xE74C3C
if max_br >= 10.0:
return 0xE67E22
if max_br >= 7.0:
return 0xF1C40F
if max_br >= 4.0:
return 0x2ECC71
return 0x3498DB
__all__ = [
"player_scores",
"squadron_scores",
"top_n_squadrons_with_top_k_players",
"top_n_squadrons_with_top_k_players_from_maps",
"squadron_report_for_variants",
"squadron_report_for_variants_from_maps",
"br_color_int",
]
+490
View File
@@ -0,0 +1,490 @@
"""
wl.py
Win/Loss tracking database. Manages the wl.db SQLite database which stores
per-squadron win/loss/draw records and match history.
Post clan_id migration: wl_standings is keyed by clan_id (stable) with the
squadron short tag denormalized for display and joins. Callers still pass
short squadron tags from the replay JSON; this module resolves them to
clan_id via squadrons.db internally and returns dicts keyed by the input
squadron text so the renderer doesn't need to change.
"""
# Standard Library Imports
import asyncio
import hashlib
import json
import logging
import os
import sqlite3
import time
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from .utils import STORAGE_DIR
WL_DB = STORAGE_DIR / "wl.db"
SQUADRONS_DB = STORAGE_DIR / "squadrons.db"
_DB_PATH: str = str(WL_DB)
_INITIALIZED = False
# Bounded cache for squadron-name → clan_id lookups. Squadrons rarely change
# clan_id, so a 5-minute TTL is plenty and keeps the hot path off disk.
_CLAN_ID_CACHE: dict[str, Optional[int]] = {}
_CLAN_ID_CACHE_AT: float = 0.0
_CLAN_ID_CACHE_TTL = 300.0
# ============================================================================
# DATABASE INITIALIZATION
# ============================================================================
def init(db_path: Optional[str] = None) -> None:
"""Initialize the W/L database."""
global _DB_PATH, _INITIALIZED
_DB_PATH = db_path or str(WL_DB)
_ensure_db()
_INITIALIZED = True
def wl_bootstrap():
"""Bootstrap the W/L database (alias for init)."""
init(str(WL_DB))
def _conn() -> sqlite3.Connection:
"""Create a database connection."""
con = sqlite3.connect(_DB_PATH, timeout=10, isolation_level=None)
con.execute("PRAGMA journal_mode=WAL;")
con.execute("PRAGMA foreign_keys=ON;")
return con
def _ensure_db() -> None:
"""Ensure database tables exist (clan_id-keyed schema)."""
with _conn() as con:
con.executescript("""
CREATE TABLE IF NOT EXISTS wl_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
external_id TEXT UNIQUE,
session_id TEXT,
winner TEXT NOT NULL,
winner_clan_id INTEGER,
squads_json TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS wl_standings (
clan_id INTEGER PRIMARY KEY,
squadron TEXT NOT NULL,
wins INTEGER NOT NULL DEFAULT 0,
losses INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_wl_standings_squadron
ON wl_standings(squadron);
""")
# Best-effort ALTER for installs that predate the winner_clan_id column.
cols = {r[1] for r in con.execute("PRAGMA table_info(wl_events)").fetchall()}
if "winner_clan_id" not in cols:
try:
con.execute("ALTER TABLE wl_events ADD COLUMN winner_clan_id INTEGER")
except sqlite3.OperationalError:
pass
# ============================================================================
# INTERNAL HELPERS
# ============================================================================
def _normalize(s: Optional[str]) -> Optional[str]:
"""Normalize squadron name for consistency."""
if not s:
return None
s = s.strip()
return s if s and s.upper() != "UNKNOWN" else None
def _resolve_clan_id(name: str) -> Optional[int]:
"""Resolve a squadron short_name / long_name / tag_name → clan_id.
Returns None if the squadron can't be found in squadrons_data (orphan).
Cached with a TTL since squadrons_data is the source of truth.
"""
global _CLAN_ID_CACHE, _CLAN_ID_CACHE_AT
now = time.time()
if now - _CLAN_ID_CACHE_AT > _CLAN_ID_CACHE_TTL:
_CLAN_ID_CACHE = {}
_CLAN_ID_CACHE_AT = now
key = (name or "").strip().lower()
if not key or key == "unknown":
return None
if key in _CLAN_ID_CACHE:
return _CLAN_ID_CACHE[key]
cid: Optional[int] = None
try:
with sqlite3.connect(str(SQUADRONS_DB), timeout=5) as con:
row = con.execute(
"SELECT clan_id FROM squadrons_data "
"WHERE LOWER(short_name) = ? OR LOWER(long_name) = ? OR LOWER(tag_name) = ? "
"LIMIT 1",
(key, key, key),
).fetchone()
if row and row[0] is not None:
cid = int(row[0])
except Exception:
cid = None
_CLAN_ID_CACHE[key] = cid
return cid
def _idempotency_key(session_id: Optional[str], winner: str, squads: List[str]) -> str:
"""Generate a unique key to prevent duplicate entries."""
base = f"{session_id or ''}|{winner}|{','.join(sorted(squads))}"
return hashlib.sha1(base.encode("utf-8")).hexdigest()[:20]
def _upsert_standing(
con: sqlite3.Connection, *,
clan_id: int,
squadron: str,
wins_delta: int,
losses_delta: int,
) -> None:
"""Upsert a wl_standings row keyed by clan_id, refreshing the squadron text."""
con.execute(
"""
INSERT INTO wl_standings (clan_id, squadron, wins, losses)
VALUES (?, ?, ?, ?)
ON CONFLICT(clan_id) DO UPDATE SET
squadron = excluded.squadron,
wins = wl_standings.wins + excluded.wins,
losses = wl_standings.losses + excluded.losses
""",
(int(clan_id), squadron, int(wins_delta), int(losses_delta)),
)
def _tx_upsert_event_and_apply(
con: sqlite3.Connection, *,
external_id: str,
session_id: Optional[str],
winner: str,
squads: List[str],
) -> None:
"""Insert event and update standings within a transaction.
Squadrons that can't be resolved to a clan_id (orphan / not in
squadrons_data) are skipped for the standings upsert but still recorded
on the event so a later rebuild can replay them.
"""
winner_cid = _resolve_clan_id(winner)
con.execute(
"INSERT OR IGNORE INTO wl_events "
"(external_id, session_id, winner, winner_clan_id, squads_json) "
"VALUES (?, ?, ?, ?, ?)",
(
external_id,
session_id,
winner,
winner_cid,
json.dumps(squads, ensure_ascii=False),
),
)
if con.execute("SELECT changes()").fetchone()[0] == 0:
return # duplicate, already applied
if winner_cid is not None:
_upsert_standing(con, clan_id=winner_cid, squadron=winner, wins_delta=1, losses_delta=0)
else:
logging.warning("[W/L] winner '%s' has no clan_id; skipping standings update", winner)
for los in (s for s in squads if s != winner):
los_cid = _resolve_clan_id(los)
if los_cid is None:
logging.warning("[W/L] loser '%s' has no clan_id; skipping standings update", los)
continue
_upsert_standing(con, clan_id=los_cid, squadron=los, wins_delta=0, losses_delta=1)
# ============================================================================
# PUBLIC API - ASYNC
# ============================================================================
async def record_result(
winner: str,
squadrons: List[str],
session_id: Optional[str] = None
) -> Dict[str, Dict[str, int]]:
"""
Record a match result (async).
Returns updated standings for all squadrons involved (keyed by squadron text).
"""
if not _INITIALIZED:
_ensure_db()
w = _normalize(winner)
sqs = [s for s in (_normalize(x) for x in squadrons) if s]
if w is None or len(sqs) < 2 or w not in sqs:
return {}
external_id = _idempotency_key(session_id, w, sqs)
def _work() -> Dict[str, Dict[str, int]]:
with _conn() as con:
con.execute("BEGIN IMMEDIATE")
try:
_tx_upsert_event_and_apply(
con, external_id=external_id,
session_id=session_id, winner=w, squads=sqs
)
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
return _read_standings_for(con, sqs)
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _work)
async def record_draw(
squadrons: List[str],
session_id: Optional[str] = None
) -> Dict[str, Dict[str, int]]:
"""
Record a draw (async) — counts as a loss for all involved squadrons.
Returns updated standings (keyed by squadron text).
"""
if not _INITIALIZED:
_ensure_db()
sqs = [s for s in (_normalize(x) for x in squadrons) if s]
if len(sqs) < 2:
return {}
external_id = _idempotency_key(session_id, "DRAW", sqs)
def _work() -> Dict[str, Dict[str, int]]:
with _conn() as con:
con.execute("BEGIN IMMEDIATE")
try:
con.execute(
"INSERT OR IGNORE INTO wl_events "
"(external_id, session_id, winner, winner_clan_id, squads_json) "
"VALUES (?, ?, ?, ?, ?)",
(external_id, session_id, "DRAW", None,
json.dumps(sqs, ensure_ascii=False)),
)
if con.execute("SELECT changes()").fetchone()[0] > 0:
for sq in sqs:
cid = _resolve_clan_id(sq)
if cid is None:
logging.warning(
"[W/L] draw participant '%s' has no clan_id; "
"skipping standings update", sq
)
continue
_upsert_standing(
con, clan_id=cid, squadron=sq,
wins_delta=0, losses_delta=1,
)
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
return _read_standings_for(con, sqs)
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _work)
async def schedule_wl_update(
winner: str,
squadrons: List[str],
session_id: Optional[str] = None
) -> Dict[str, Dict[str, int]]:
"""Alias for record_result (for backwards compatibility)."""
return await record_result(winner, list(squadrons), session_id)
# ============================================================================
# PUBLIC API - SYNC
# ============================================================================
def record_result_sync(
winner: str,
squadrons: List[str],
session_id: Optional[str] = None
) -> Dict[str, Dict[str, int]]:
"""
Record a match result (sync).
Returns updated standings for all squadrons involved (keyed by squadron text).
"""
if not _INITIALIZED:
_ensure_db()
w = _normalize(winner)
sqs = [s for s in (_normalize(x) for x in squadrons) if s]
if w is None or len(sqs) < 2 or w not in sqs:
return {}
external_id = _idempotency_key(session_id, w, sqs)
with _conn() as con:
con.execute("BEGIN IMMEDIATE")
try:
_tx_upsert_event_and_apply(
con, external_id=external_id,
session_id=session_id, winner=w, squads=sqs
)
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
return _read_standings_for(con, sqs)
def _wl_update_sync(
winner: str,
squadrons: List[str],
session_id: Optional[str] = None
) -> Dict[str, Dict[str, int]]:
"""Alias for record_result_sync (for backwards compatibility)."""
return record_result_sync(winner, list(squadrons), session_id)
# ============================================================================
# PUBLIC API - QUERIES
# ============================================================================
def _read_standings_for(
con: sqlite3.Connection, squadrons: List[str]
) -> Dict[str, Dict[str, int]]:
"""Return {squadron_text: {wins, losses}} for the given input squadrons.
Looks up each squadron's clan_id then reads by clan_id so a squadron that
just renamed still finds its row. Falls back to a squadron-text match for
orphans.
"""
out: Dict[str, Dict[str, int]] = {sq: {"wins": 0, "losses": 0} for sq in squadrons}
if not squadrons:
return out
cid_by_input: Dict[str, Optional[int]] = {sq: _resolve_clan_id(sq) for sq in squadrons}
cids = [c for c in cid_by_input.values() if c is not None]
if cids:
rows = con.execute(
f"SELECT clan_id, wins, losses FROM wl_standings "
f"WHERE clan_id IN ({','.join(['?']*len(cids))})",
cids,
).fetchall()
by_cid = {int(r[0]): (int(r[1]), int(r[2])) for r in rows}
for sq, cid in cid_by_input.items():
if cid is not None and cid in by_cid:
w, l = by_cid[cid]
out[sq] = {"wins": w, "losses": l}
# Fallback by squadron text for orphans (no clan_id resolved).
orphans = [sq for sq, cid in cid_by_input.items() if cid is None]
if orphans:
rows = con.execute(
f"SELECT squadron, wins, losses FROM wl_standings "
f"WHERE squadron IN ({','.join(['?']*len(orphans))})",
orphans,
).fetchall()
for s, w, l in rows:
out[s] = {"wins": int(w), "losses": int(l)}
return out
def get_standings(squadrons: List[str]) -> Dict[str, Dict[str, int]]:
"""Get W/L standings for a list of squadrons (keyed by squadron text)."""
if not _INITIALIZED:
_ensure_db()
if not squadrons:
return {}
with _conn() as con:
return _read_standings_for(con, list(squadrons))
def get_wl_counts(squadrons: List[str]) -> Dict[str, Dict[str, int]]:
"""Alias for get_standings (for backwards compatibility)."""
return get_standings(list(squadrons))
def get_leaderboard(limit: int = 20) -> List[Tuple[str, int, int]]:
"""Get the W/L leaderboard sorted by win difference."""
if not _INITIALIZED:
_ensure_db()
with _conn() as con:
return [
(str(s), int(w), int(l))
for s, w, l in con.execute("""
SELECT squadron, wins, losses
FROM wl_standings
ORDER BY (wins - losses) DESC, wins DESC, squadron ASC
LIMIT ?
""", (limit,)).fetchall()
]
def build_wl_leaderboard(limit: int = 20) -> List[Tuple[str, int, int]]:
"""Alias for get_leaderboard (for backwards compatibility)."""
return get_leaderboard(int(limit))
# ============================================================================
# MAINTENANCE
# ============================================================================
def rebuild_from_events() -> None:
"""Rebuild standings from events (for data recovery)."""
if not _INITIALIZED:
_ensure_db()
with _conn() as con:
con.execute("BEGIN IMMEDIATE")
try:
con.execute("DELETE FROM wl_standings")
for ext_id, session_id, winner, squads_json in con.execute(
"SELECT external_id, session_id, winner, squads_json "
"FROM wl_events ORDER BY id ASC"
):
squads = json.loads(squads_json)
if winner == "DRAW":
for sq in squads:
cid = _resolve_clan_id(sq)
if cid is None:
continue
_upsert_standing(
con, clan_id=cid, squadron=sq,
wins_delta=0, losses_delta=1,
)
else:
_tx_upsert_event_and_apply(
con, external_id=ext_id,
session_id=session_id, winner=winner,
squads=squads,
)
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
def clean_WL() -> None:
"""Clear all W/L data (events and standings)."""
con = sqlite3.connect(str(_DB_PATH), timeout=10, isolation_level=None)
try:
con.execute("BEGIN IMMEDIATE")
con.execute("DELETE FROM wl_events")
con.execute("DELETE FROM wl_standings")
con.execute("COMMIT")
except Exception:
con.execute("ROLLBACK")
raise
finally:
con.close()
+70
View File
@@ -0,0 +1,70 @@
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/Sop-rs/SREBOT_MEOW.git
cd SREBOT_MEOW
```
2. **Set up Python virtual environment**
```bash
python3 -m venv .venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. **Install dependencies**
```bash
pip install -r requirements.txt
```
4. **Configure environment variables**
```bash
nano .env
```
Edit the existing `.env` in the repo root and keep the storage path there:
```env
SREBOT_DEPLOY_PATH=/absolute/path/to/SREBOT_MEOW
DISCORD_KEY=your_discord_bot_token_here
DEEPL_KEY=your_deepl_api_key_here # Optional
GITHUB_WEBHOOK_SECRET=your_webhook_secret # For auto-deployment
SREBOT_STORAGE_VOL_PATH=/absolute/path/to/storage
SREBOT_API_BEARER_TOKEN=your_internal_api_token # Optional, protects /api/*
SREBOT_EXTERNAL_PORT=18081 # External bridge port
SREBOT_EXTERNAL_BEARER_TOKEN=your_external_bridge_token # Optional, protects the bridge API and websocket
SREBOT_EXTERNAL_UPSTREAM_URL=http://127.0.0.1:6000 # Internal SREBOT API to proxy
```
5. **Run the bot**
```bash
python BotScript.py
```
### AXBot bridge process
`ecosystem.config.js` now includes a dedicated PM2 app named `srebot-axbot`.
It proxies read-only SREBOT queries and broadcasts replay/GOB envelopes over
websocket on the same external port.
Its outbox/state files live under the shared storage volume configured in
`.env` via `SREBOT_STORAGE_VOL_PATH`.
Useful commands:
```bash
pm2 start ecosystem.config.js --only srebot-api
pm2 start ecosystem.config.js --only srebot-axbot
pm2 logs srebot-axbot
```
Clients should point their query client at:
```env
SREBOT_API_BASE_URL=http://<srebot-host>:18081
```
The bridge app logs both sides of the transfer:
- incoming client HTTP requests
- outgoing proxy responses
- websocket envelopes broadcast to connected clients
# test
+71
View File
@@ -0,0 +1,71 @@
require('dotenv').config();
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const STORAGE_ROOT = (process.env.SREBOT_STORAGE_VOL_PATH || '').trim();
if (!STORAGE_ROOT) {
throw new Error('SREBOT_STORAGE_VOL_PATH must be set');
}
const DB_PATH = path.join(STORAGE_ROOT, 'sq_battles.db');
console.log(`Opening database at: ${DB_PATH}`);
const db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READWRITE, (err) => {
if (err) {
console.error('Failed to open database:', err.message);
process.exit(1);
}
console.log('Connected to database successfully');
const checkIndexQuery = `
SELECT name FROM sqlite_master
WHERE type='index' AND name='idx_endtime_unix'
`;
db.get(checkIndexQuery, [], (err, row) => {
if (err) {
console.error('Error checking for existing index:', err.message);
db.close();
process.exit(1);
}
if (row) {
console.log('Index idx_endtime_unix already exists');
db.close();
console.log('Nothing to do - exiting');
process.exit(0);
}
console.log('Creating index idx_endtime_unix on player_games_hist(endtime_unix)...');
const createIndexQuery = `CREATE INDEX idx_endtime_unix ON player_games_hist(endtime_unix)`;
db.run(createIndexQuery, [], (err) => {
if (err) {
console.error('Failed to create index:', err.message);
db.close();
process.exit(1);
}
console.log('Index created successfully!');
db.get(checkIndexQuery, [], (err, row) => {
if (err) {
console.error('Error verifying index:', err.message);
} else if (row) {
console.log('Index verified successfully');
} else {
console.warn('Warning: Index not found after creation');
}
db.close((err) => {
if (err) {
console.error('Error closing database:', err.message);
process.exit(1);
}
console.log('Database connection closed');
console.log('\nIndex creation complete!');
process.exit(0);
});
});
});
});
});
+128
View File
@@ -0,0 +1,128 @@
require('dotenv').config();
const DEPLOY_PATH = process.env.SREBOT_DEPLOY_PATH || __dirname;
module.exports = {
apps: [
// Discord Bot
{
name: 'srebot',
script: 'start_bot.py',
interpreter: `${DEPLOY_PATH}/.venv/bin/python`,
cwd: DEPLOY_PATH,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '16000M',
env: {
NODE_ENV: 'production',
PYTHONUNBUFFERED: '1'
},
log_file: './logs/bot_combined.log',
out_file: './logs/bot_out.log',
error_file: './logs/bot_error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
kill_timeout: 5000,
restart_delay: 3000
},
// API Server
{
name: 'srebot-api',
script: 'server.js',
interpreter: 'node',
node_args: '--max-old-space-size=6144',
cwd: DEPLOY_PATH,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '4G',
env: {
NODE_ENV: 'production',
PORT: 6000
},
log_file: './logs/api_combined.log',
out_file: './logs/api_out.log',
error_file: './logs/api_error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
kill_timeout: 5000,
restart_delay: 2000
},
// External bridge for AXBot traffic:
// - Proxies read-only API queries to the internal SREBOT API
// - Streams bridge envelopes to AXBot over websocket
{
name: 'srebot-axbot',
script: 'BOT/srebot_external.py',
interpreter: `${DEPLOY_PATH}/.venv/bin/python`,
cwd: DEPLOY_PATH,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PYTHONUNBUFFERED: '1',
SREBOT_EXTERNAL_HOST: '0.0.0.0',
SREBOT_EXTERNAL_PORT: 18081,
SREBOT_EXTERNAL_UPSTREAM_URL: 'http://127.0.0.1:6000'
},
log_file: './logs/axbot_combined.log',
out_file: './logs/axbot_out.log',
error_file: './logs/axbot_error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
kill_timeout: 5000,
restart_delay: 2000
},
// GitHub Webhook Receiver (auto-deploy on push to main)
{
name: 'srebot-webhook',
script: 'github_webhook_updater.py',
interpreter: `${DEPLOY_PATH}/.venv/bin/python`,
cwd: DEPLOY_PATH,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
PYTHONUNBUFFERED: '1',
WEBHOOK_PORT: 9000
},
log_file: './logs/webhook_combined.log',
out_file: './logs/webhook_out.log',
error_file: './logs/webhook_error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
kill_timeout: 3000,
restart_delay: 2000
},
// Website (Next.js)
{
name: 'srebot-web',
script: 'server.js',
cwd: `${DEPLOY_PATH}/web`,
instances: 3,
exec_mode: 'cluster',
autorestart: true,
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'production',
PORT: 3001
},
log_file: './logs/web_combined.log',
out_file: './logs/web_out.log',
error_file: './logs/web_error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
kill_timeout: 5000
}
]
};
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""
GitHub Webhook Handler for auto-deploying SREBOT on main branch pushes.
Listens for push events, pulls changes, and restarts pm2.
"""
import hmac
import hashlib
import logging
import subprocess
import os
from flask import Flask, request, jsonify
from dotenv import load_dotenv
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('GITHUB_WEBHOOK_SECRET', '')
REPO_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WEBHOOK_FILENAME = 'SREBOT/github_webhook_updater.py'
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature."""
if not WEBHOOK_SECRET:
logger.warning("No webhook secret configured")
return False
if not signature:
logger.warning("No signature provided in request")
return False
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def pull_and_restart(changed_files: list[str]) -> tuple[bool, str]:
"""Pull latest changes from main and restart pm2 processes."""
try:
# Change to repo directory
os.chdir(REPO_PATH)
logger.info(f"Changed to repo directory: {REPO_PATH}")
# Fetch and pull main branch
logger.info("Fetching from origin/main...")
subprocess.run(
['git', 'fetch', 'origin', 'main'],
capture_output=True, text=True, timeout=30
)
logger.info("Pulling from origin/main...")
pull_result = subprocess.run(
['git', 'pull', 'origin', 'main'],
capture_output=True, text=True, timeout=60
)
if pull_result.returncode != 0:
logger.error(f"Git pull failed: {pull_result.stderr}")
return False, f"Git pull failed: {pull_result.stderr}"
# Check if there were actual changes
if 'Already up to date' in pull_result.stdout:
logger.info("Already up to date, no restart needed")
return True, "Already up to date, no restart needed"
logger.info(f"Pull successful: {pull_result.stdout.strip()}")
# Determine which processes to restart based on changed files
processes_to_restart = []
# Bot: restart if SREBOT/BOT/, SHARED/, SREBOT/start_bot.py, ecosystem.config.js,
# or SREBOT root .py files changed
bot_changed = any(
f.startswith('SREBOT/BOT/')
or f.startswith('SHARED/')
or f == 'SREBOT/start_bot.py'
or f == 'SREBOT/ecosystem.config.js'
or (f.startswith('SREBOT/') and f.endswith('.py') and f.count('/') == 1 and not f.endswith('/' + WEBHOOK_FILENAME))
for f in changed_files
)
if bot_changed:
processes_to_restart.append('srebot')
logger.info("Bot files changed, will restart srebot")
# API server: restart if SREBOT/server.js changed
api_changed = any(f == 'SREBOT/server.js' for f in changed_files)
if api_changed:
processes_to_restart.append('srebot-api')
logger.info("API server.js changed, will restart srebot-api")
# Web frontend: restart if SREBOT/web/ files changed
web_changed = any(f.startswith('SREBOT/web/') for f in changed_files)
if web_changed:
# Rebuild CSS if tailwind source or config changed
css_changed = any(
f in ('SREBOT/web/public/css/tailwind.css', 'SREBOT/web/tailwind.config.js')
for f in changed_files
)
if css_changed:
logger.info("Tailwind source changed, rebuilding CSS...")
build_result = subprocess.run(
['npm', 'run', 'build:css'],
capture_output=True, text=True, timeout=60,
cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
)
if build_result.returncode != 0:
logger.error(f"CSS build failed: {build_result.stderr}")
return False, f"CSS build failed: {build_result.stderr}"
logger.info("CSS build successful")
# Rebuild obfuscated JS if any JS source files changed
js_changed = any(
f.startswith('SREBOT/web/public/js/') and f.endswith('.js') and '/dist/' not in f
for f in changed_files
)
if js_changed:
logger.info("JS source files changed, rebuilding obfuscated JS...")
build_result = subprocess.run(
['node', 'build.js'],
capture_output=True, text=True, timeout=120,
cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
)
if build_result.returncode != 0:
logger.warning(f"JS build failed (non-fatal): {build_result.stderr}")
logger.warning("Continuing deploy — unobfuscated JS will be served as fallback")
else:
logger.info("JS build successful")
processes_to_restart.append('srebot-web')
logger.info("Web files changed, will restart srebot-web")
# Webhook: restart if this file changed
webhook_changed = any(WEBHOOK_FILENAME in f for f in changed_files)
if webhook_changed:
processes_to_restart.append('srebot-webhook')
logger.info(f"Webhook file changed, will restart srebot-webhook")
if not processes_to_restart:
logger.info(f"No relevant process files changed, skipping restart. Files: {changed_files}")
return True, "Pulled but no restart needed"
# Flush pm2 logs before restarting to free disk space
logger.info("Flushing PM2 logs...")
subprocess.run(
['pm2', 'flush'],
capture_output=True, text=True, timeout=30
)
# Restart pm2 processes
restarted = []
for process in processes_to_restart:
logger.info(f"Restarting PM2 process: {process}")
restart_result = subprocess.run(
['pm2', 'restart', process],
capture_output=True, text=True, timeout=30
)
if restart_result.returncode != 0:
logger.error(f"PM2 restart failed for {process}: {restart_result.stderr}")
return False, f"PM2 restart failed for {process}: {restart_result.stderr}"
restarted.append(process)
logger.info(f"PM2 restart successful: {restarted}")
return True, f"Pulled and restarted {restarted}"
except subprocess.TimeoutExpired:
logger.error("Command timed out")
return False, "Command timed out"
except Exception as e:
logger.exception(f"Unexpected error during pull_and_restart: {e}")
return False, f"Error: {str(e)}"
@app.route('/webhook', methods=['POST'])
def webhook():
"""Handle GitHub webhook POST requests."""
logger.info(f"Received webhook request from {request.remote_addr}")
# Verify signature
signature = request.headers.get('X-Hub-Signature-256', '')
if not verify_signature(request.data, signature):
logger.warning(f"Invalid signature from {request.remote_addr}")
return jsonify({'error': 'Invalid signature'}), 403
# Parse event type
event = request.headers.get('X-GitHub-Event', '')
logger.info(f"GitHub event type: {event}")
if event == 'ping':
logger.info("Received ping event, responding with pong")
return jsonify({'message': 'pong'}), 200
if event != 'push':
logger.info(f"Ignoring non-push event: {event}")
return jsonify({'message': f'Ignoring event: {event}'}), 200
# Parse JSON payload with error handling
try:
payload = request.get_json()
if payload is None:
logger.error("Failed to parse JSON payload (returned None)")
return jsonify({'error': 'Invalid JSON payload'}), 400
except Exception as e:
logger.error(f"Failed to parse JSON payload: {e}")
return jsonify({'error': 'Invalid JSON payload'}), 400
# Check if it's the main branch
ref = payload.get('ref', '')
logger.info(f"Push to ref: {ref}")
if ref != 'refs/heads/main':
logger.info(f"Ignoring push to non-main branch: {ref}")
return jsonify({'message': f'Ignoring branch: {ref}'}), 200
# Extract changed files from commits
commits = payload.get('commits', [])
pusher = payload.get('pusher', {}).get('name', 'unknown')
logger.info(f"Push from {pusher} with {len(commits)} commit(s)")
changed_files = []
for commit in commits:
changed_files.extend(commit.get('added', []))
changed_files.extend(commit.get('modified', []))
changed_files.extend(commit.get('removed', []))
logger.info(f"Changed files: {changed_files}")
# Pull and restart
success, message = pull_and_restart(changed_files)
if success:
logger.info(f"Webhook update successful: {message}")
return jsonify({'success': True, 'message': message}), 200
else:
logger.error(f"Webhook update failed: {message}")
return jsonify({'success': False, 'error': message}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint."""
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
port = int(os.environ.get('WEBHOOK_PORT', 9000))
logger.info(f"Starting webhook server on port {port}")
logger.info(f"Repo path: {REPO_PATH}")
app.run(host='0.0.0.0', port=port)
+2342
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "srebot-player-api",
"version": "1.0.0",
"description": "Node.js API server for SREBOT player statistics",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"dependencies": {
"dotenv": "^16.3.1",
"express": "^4.18.2",
"sqlite3": "^5.1.6",
"cors": "^2.8.5"
},
"engines": {
"node": ">=14.0.0"
}
}
+21
View File
@@ -0,0 +1,21 @@
[project]
name = "WtFileUtils"
version = "0.1.5"
authors = [
{ name="LivingTheDagor", email="LivingTheDagor@gmail.com" },
]
description = "A VROMFs and Binary BLK Reader"
readme = "README.md"
requires-python = ">=3.12"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://github.com/LivingTheDagor/WtFileUtils"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+14
View File
@@ -0,0 +1,14 @@
{
"pythonVersion": "3.14",
"extraPaths": [
".venv/lib/python3.14/site-packages",
"../SHARED"
],
"exclude": [
".venv",
"../SHARED/DAGOR_FILES",
"web/node_modules",
"node_modules",
"**/__pycache__"
]
}
+22
View File
@@ -0,0 +1,22 @@
discord.py>=2.4.0,<3.0.0
requests>=2.32.3,<3.0.0
beautifulsoup4>=4.12.3,<5.0.0
lxml>=5.0.0
zstandard
pygob @ git+https://github.com/mgeisler/pygob.git
lz4==4.3.3
aiofiles
aiohttp
aiohttp-socks
websockets>=13.0
aiosqlite
deepl
python-dotenv>=1.0.0
flask>=2.0.0
Pillow>=10.0.0
WtFileUtils
dotenv
numpy
wcwidth
fontTools
matplotlib
+170
View File
@@ -0,0 +1,170 @@
"""
Diagnostic: query Discord's entitlements API directly and report what it returns
right now. Compare against the local entitlements.db cache.
Usage (on the server):
cd ~/GitHub/SREBOT_MEOW
source .venv/bin/activate
python scripts/diag_entitlements.py
Targeted guild: 1379510072815779961 (bot owner's max-tier server).
"""
from __future__ import annotations
import asyncio
import os
import sqlite3
import sys
import time
from pathlib import Path
import aiohttp
from dotenv import load_dotenv
load_dotenv()
APPLICATION_ID = "1254679514466877540"
TARGET_GUILD = "1379510072815779961"
TOKEN = os.environ.get("DISCORD_KEY", "")
_storage_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
if not _storage_env:
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
STORAGE = Path(_storage_env)
DB_PATH = STORAGE / "entitlements.db"
async def fetch_all_entitlements() -> tuple[list[dict], list[tuple[int, int, str, str]]]:
"""
Returns (entitlements, page_logs).
page_logs: list of (page_num, count, status, after_cursor)
"""
if not TOKEN:
print("ERROR: DISCORD_KEY env var not set", file=sys.stderr)
sys.exit(1)
headers = {"Authorization": f"Bot {TOKEN}"}
url = f"https://discord.com/api/v10/applications/{APPLICATION_ID}/entitlements"
params: dict[str, str] = {"exclude_ended": "true", "limit": "100"}
all_ents: list[dict] = []
page_logs: list[tuple[int, int, str, str]] = []
page = 0
after: str | None = None
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession(timeout=timeout) as session:
while True:
page += 1
q = dict(params)
if after:
q["after"] = after
t0 = time.monotonic()
async with session.get(url, headers=headers, params=q) as resp:
elapsed = time.monotonic() - t0
status = f"{resp.status} ({elapsed:.2f}s)"
if resp.status != 200:
body = await resp.text()
page_logs.append((page, 0, status, after or ""))
print(f"[!] page {page} failed: {status}\n {body[:300]}")
break
batch = await resp.json()
page_logs.append((page, len(batch), status, after or ""))
all_ents.extend(batch)
if len(batch) < 100:
break
# paginate forward by id
after = batch[-1]["id"]
# safety break
if page > 50:
print("[!] hit 50-page safety limit, stopping")
break
return all_ents, page_logs
def read_db_state() -> dict[str, int]:
if not DB_PATH.exists():
return {"db_missing": 1}
conn = sqlite3.connect(str(DB_PATH))
try:
cur = conn.cursor()
out: dict[str, int] = {}
out["guild_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM guild_entitlements").fetchone()[0]
out["guild_entitlements_active"] = cur.execute("SELECT COUNT(*) FROM guild_entitlements WHERE status='active'").fetchone()[0]
out["manual_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM manual_entitlements").fetchone()[0]
out["discord_entitlements_total"] = cur.execute("SELECT COUNT(*) FROM discord_entitlements").fetchone()[0]
row = cur.execute("SELECT * FROM discord_entitlements WHERE guild_id=?", (TARGET_GUILD,)).fetchone()
out["target_in_discord_table"] = 1 if row else 0
return out
finally:
conn.close()
async def main() -> None:
print("=" * 70)
print(f"Discord Entitlements Diagnostic — {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
print("=" * 70)
print(f"\n[1] Local DB state ({DB_PATH}):")
for k, v in read_db_state().items():
print(f" {k}: {v}")
print("\n[2] Discord API: GET /applications/{app_id}/entitlements?exclude_ended=true")
ents, pages = await fetch_all_entitlements()
print("\n Page log:")
for p, n, status, after in pages:
print(f" page {p:>2} count={n:>3} status={status:<18} after={after}")
print(f"\n Total entitlements returned: {len(ents)}")
# Group by guild
by_guild: dict[str, list[dict]] = {}
for e in ents:
gid = e.get("guild_id")
if gid:
by_guild.setdefault(str(gid), []).append(e)
print(f" Unique guild_ids: {len(by_guild)}")
print(f" Entitlements with NULL guild_id: {sum(1 for e in ents if not e.get('guild_id'))}")
# Status breakdown
deleted = sum(1 for e in ents if e.get("deleted"))
consumed = sum(1 for e in ents if e.get("consumed"))
starts_in_future = sum(1 for e in ents if e.get("starts_at") and e["starts_at"] > time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()))
ends_in_past = sum(1 for e in ents if e.get("ends_at") and e["ends_at"] < time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()))
print(f" deleted={deleted} consumed={consumed} starts_in_future={starts_in_future} ends_in_past={ends_in_past}")
print(f"\n[3] Target guild {TARGET_GUILD}:")
target_ents = by_guild.get(TARGET_GUILD, [])
if not target_ents:
print(f" *** NOT FOUND in Discord API response ***")
# Show sample of what we DID get to confirm we're not just paginating wrong
sample_guilds = list(by_guild.keys())[:5]
print(f" Sample of guild_ids returned: {sample_guilds}")
else:
for e in target_ents:
print(f" id={e.get('id')} sku_id={e.get('sku_id')} type={e.get('type')} "
f"deleted={e.get('deleted')} starts_at={e.get('starts_at')} ends_at={e.get('ends_at')}")
print("\n[4] SKU breakdown across all returned entitlements:")
sku_counts: dict[str, int] = {}
for e in ents:
sku = str(e.get("sku_id") or "")
sku_counts[sku] = sku_counts.get(sku, 0) + 1
for sku, c in sorted(sku_counts.items(), key=lambda x: -x[1]):
print(f" {sku}: {c}")
print("\n[5] Recommendation:")
if not target_ents:
print(" Discord is NOT returning the target guild's entitlement.")
print(" Either Discord's API is degraded right now, OR the entitlement no longer exists.")
print(" Re-run this script in a few minutes to see if results change.")
else:
print(" Discord IS returning the target guild's entitlement.")
print(" The bot's cache must be stale or wholesale-replaced by an empty result.")
print(" Force a refresh: restart bot OR call refresh_entitled_guilds(force=True).")
if __name__ == "__main__":
asyncio.run(main())
+123
View File
@@ -0,0 +1,123 @@
"""
Diagnostic: figure out why /meta shows no points per player.
Compares Guild_Metas userIDs against the uid keys returned by
obtain_clan_new_points() for the guild's bound squadron, and reports each
plausible failure mode (missing Guilds row, empty API response, uid format
mismatch, squadron-name mismatch).
Usage (on the server):
cd ~/SREBOT/SREBOT_MEOW
source .venv/bin/activate
python scripts/diag_meta_points.py 1378960248118841507
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
import aiosqlite
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from BOT.game_api import ClanInfoError, obtain_clan_new_points
from BOT.utils import STORAGE_DIR
async def diag(guild_id: str) -> None:
meta_db = STORAGE_DIR / "Meta.db"
print(f"== Meta.db: {meta_db} (exists={meta_db.exists()})")
if not meta_db.exists():
print("Meta.db missing — wrong STORAGE_DIR?")
return
async with aiosqlite.connect(meta_db) as db:
cur = await db.execute(
"SELECT squadron_name, squadron_clanID FROM Guilds WHERE guild_id=?",
(guild_id,),
)
guilds_row = await cur.fetchone()
print(f"== Guilds row for {guild_id}: {guilds_row}")
cur = await db.execute(
"SELECT COUNT(*) FROM Guild_Metas WHERE guild_id=?", (guild_id,)
)
gm_count_row = await cur.fetchone()
gm_count = gm_count_row[0] if gm_count_row else 0
print(f"== Guild_Metas count for {guild_id}: {gm_count}")
cur = await db.execute(
"SELECT userID, nick, clanName FROM Guild_Metas "
"WHERE guild_id=? ORDER BY nick LIMIT 5",
(guild_id,),
)
gm_rows = await cur.fetchall()
print("== Guild_Metas sample:")
for uid, nick, clan in gm_rows:
print(f" uid={uid!r} type={type(uid).__name__} nick={nick!r} clan={clan!r}")
if not guilds_row:
print("\nNo Guilds row — /meta-management was never run for this guild.")
return
sq_name = guilds_row[0]
print(f"\n== Calling obtain_clan_new_points({sq_name!r})...")
try:
members, total = await obtain_clan_new_points(sq_name)
except ClanInfoError as e:
print(f" ClanInfoError: {e}")
return
except Exception as e:
print(f" {type(e).__name__}: {e}")
return
print(f"== API returned: {len(members)} members, total_score={total}")
if not members:
print(" Empty members dict — JWT was likely refreshed; rerun the script.")
return
sample = list(members.items())[:3]
print("== API sample:")
for uid, info in sample:
print(f" uid={uid!r} type={type(uid).__name__} info={info}")
gm_rows_list = list(gm_rows)
if gm_rows_list:
gm_uid = gm_rows_list[0][0]
print(
f"\n== Membership test for first Guild_Metas uid {gm_uid!r}:\n"
f" raw in api_map -> {gm_uid in members}\n"
f" str(uid) in api_map -> {str(gm_uid) in members}"
)
api_uids = set(members.keys())
gm_uids_all = set()
async with aiosqlite.connect(meta_db) as db:
cur = await db.execute(
"SELECT userID FROM Guild_Metas WHERE guild_id=?", (guild_id,)
)
gm_uids_all = {str(r[0]) for r in await cur.fetchall()}
api_uids_str = {str(u) for u in api_uids}
overlap = gm_uids_all & api_uids_str
only_gm = gm_uids_all - api_uids_str
only_api = api_uids_str - gm_uids_all
print(
f"\n== Set overlap (str-cast both sides):\n"
f" in both: {len(overlap)}\n"
f" only in Guild_Metas: {len(only_gm)} (left squadron / wrong sq_name?)\n"
f" only in API: {len(only_api)} (never bulk-added)"
)
def main() -> None:
if len(sys.argv) != 2:
print("Usage: python scripts/diag_meta_points.py <guild_id>")
sys.exit(2)
asyncio.run(diag(sys.argv[1]))
if __name__ == "__main__":
main()
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Probe the War Thunder clan leaderboard for a specific squadron.
This mirrors the bot's ``obtain_clans_leaderboard()`` flow:
1. Load the JWT from the storage auth file.
2. Call the clan leaderboard endpoint with ``action=cln_clan_get_leaderboard``.
3. Decode the BLK payload.
4. Print leaderboard counts and the first matching squadron entry.
Usage:
python3 scripts/leaderboard_score_probe.py
python3 scripts/leaderboard_score_probe.py --needle SCORE --count 1000
python3 scripts/leaderboard_score_probe.py --auth-file /mnt/.../STORAGE/auth_JWT.json --char-url "$WT_CHAR_URL"
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Any
from dotenv import load_dotenv
def _infer_storage_root(auth_file: Path) -> Path:
if auth_file.parent.name == "AUTH":
return auth_file.parent.parent
return auth_file.parent
def _prepare_env(storage_root: Path) -> None:
os.environ["SREBOT_STORAGE_VOL_PATH"] = str(storage_root)
def _score_hit(clan: dict[str, Any], needle: str) -> bool:
needle_u = needle.upper()
values = (
str(clan.get("short_name", "")).upper(),
str(clan.get("tag", "")).upper(),
str(clan.get("long_name", "")).upper(),
)
return any(v == needle_u or needle_u in v for v in values)
async def _main() -> int:
repo_root = Path(__file__).resolve().parents[1]
env_path = repo_root / ".env"
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
parser = argparse.ArgumentParser()
parser.add_argument("--needle", default="SCORE", help="Squadron name to search for")
parser.add_argument("--start", type=int, default=0, help="Leaderboard offset")
parser.add_argument("--count", type=int, default=1000, help="Leaderboard page size")
parser.add_argument(
"--auth-file",
default="",
help="Path to the JWT auth JSON file. If omitted, the script will use a temp file under the storage root.",
)
parser.add_argument(
"--char-url",
default="",
help="War Thunder char API URL (defaults to WT_CHAR_URL env var)",
)
args = parser.parse_args()
if args.auth_file:
auth_file = Path(args.auth_file).expanduser().resolve()
storage_root = _infer_storage_root(auth_file)
else:
storage_root_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
if not storage_root_env:
print(
"SREBOT_STORAGE_VOL_PATH is not set. Export it or pass --auth-file.",
file=sys.stderr,
)
return 2
storage_root = Path(storage_root_env)
auth_file = storage_root / "AUTH" / "auth_JWT.json"
_prepare_env(storage_root)
auth_file.parent.mkdir(parents=True, exist_ok=True)
load_dotenv(dotenv_path=env_path)
if not args.char_url:
args.char_url = os.environ.get("WT_CHAR_URL", "")
if args.char_url:
os.environ["WT_CHAR_URL"] = args.char_url
elif not os.environ.get("WT_CHAR_URL"):
print(
"WT_CHAR_URL is not set. Pass --char-url or export WT_CHAR_URL first.",
file=sys.stderr,
)
return 2
# Import after env setup so BOT.game_api picks up the correct paths.
from BOT import game_api # type: ignore
import aiohttp
game_api.AUTH_FILE = auth_file
bin_blk_to_json = game_api.bin_blk_to_json
if not auth_file.exists():
await game_api.get_JWT()
with auth_file.open("r", encoding="utf-8") as f:
jwt = json.load(f)["jwt"]
headers = {
"action": "cln_clan_get_leaderboard",
"token": jwt,
"start": str(args.start),
"count": str(args.count),
"sortField": "dr_era5_hist",
"shortMode": "off",
}
url = os.environ["WT_CHAR_URL"]
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as res:
raw = await res.read()
print(f"http_status={res.status}")
if res.status != 200:
print(raw[:1000].decode("utf-8", "replace"), file=sys.stderr)
return 1
data = await bin_blk_to_json(raw)
if "root" in data:
data = data["root"]
clans = data.get("clan") or []
print(f"returned_clans={len(clans)}")
positive = sum(1 for clan in clans if int((clan.get("astat") or {}).get("dr_era5_hist") or 0) > 0)
print(f"positive_clans={positive}")
matches = []
for idx, clan in enumerate(clans, start=1):
if _score_hit(clan, args.needle):
astat = clan.get("astat") or {}
matches.append(
{
"index": idx,
"clan_id": clan.get("_id"),
"tag": clan.get("tag"),
"name": clan.get("name"),
"short_name": clan.get("tag", "")[1:-1] if clan.get("tag") else "",
"clanrating": astat.get("dr_era5_hist"),
"members": clan.get("members_cnt"),
"position": clan.get("pos"),
}
)
if not matches:
print(f"needle={args.needle!r} not found")
return 0
print("matches=" + json.dumps(matches[:10], ensure_ascii=False))
first = matches[0]["index"]
lo = max(1, first - 3)
hi = min(len(clans), first + 3)
window = []
for idx in range(lo, hi + 1):
clan = clans[idx - 1]
astat = clan.get("astat") or {}
window.append(
{
"index": idx,
"tag": clan.get("tag"),
"name": clan.get("name"),
"clanrating": astat.get("dr_era5_hist"),
"position": clan.get("pos"),
}
)
print("window=" + json.dumps(window, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(_main()))
+739
View File
@@ -0,0 +1,739 @@
"""
One-shot migration: switch SREBOT storage to use clan_id (numeric squadron UID)
as the canonical identifier across DB tables and preference JSON files.
Goals
-----
1. Add `clan_id` to historical tables (player_games_hist, match_summary).
2. Rebuild points.db tables (profile_member_points, profile_totals,
game_cache) so their PKs include `clan_id` instead of squadron text.
3. Rebuild wl.db tables to use clan_id (currently empty so trivial).
4. Add `squadron_name_history` table to squadrons.db (for old-name → clan_id
redirects when a squadron renames). Seed with current squadrons_data.
5. Re-key every PREFERENCES/<guild_id>-preferences.json so squadron entries
are keyed by `str(clan_id)` instead of long_name. Special wildcard keys
("Global", "everything", "all", "*") are preserved.
6. Backups: write a tarball of STORAGE/ before any change. Each preferences
file gets copied to PREFERENCES_BACKUP_<timestamp>/.
Run with `--dry-run` first. The script prints exactly what it would do.
Usage
-----
source .venv/bin/activate
python scripts/migrate_clan_id.py --dry-run
python scripts/migrate_clan_id.py --apply
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import shutil
import sqlite3
import sys
import tarfile
import time
from pathlib import Path
from typing import Any, Optional
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("migrate_clan_id")
# Auto-load the repo's .env so SREBOT_STORAGE_VOL_PATH and friends resolve when
# the script is run directly (PM2/services already get them from .env).
try:
from dotenv import load_dotenv
_here = Path(__file__).resolve()
for candidate in (_here.parent / ".env", _here.parent.parent / ".env"):
if candidate.exists():
load_dotenv(candidate)
break
except ImportError:
pass
_storage_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
if not _storage_env:
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
STORAGE_DIR = Path(_storage_env)
SQ_BATTLES_DB = STORAGE_DIR / "sq_battles.db"
SQUADRONS_DB = STORAGE_DIR / "squadrons.db"
POINTS_DB = STORAGE_DIR / "points.db"
WL_DB = STORAGE_DIR / "wl.db"
PREFS_DIR = STORAGE_DIR / "PREFERENCES"
WILDCARD_KEYS = {"global", "everything", "all", "*"}
PGH_BATCH = 50_000
def backup_storage() -> Path:
ts = time.strftime("%Y%m%d-%H%M%S")
backup_path = STORAGE_DIR.parent / f"STORAGE_BACKUP_{ts}.tar.gz"
log.info("Creating tarball backup at %s (this may take a while)...", backup_path)
with tarfile.open(backup_path, "w:gz") as tar:
for db in (SQ_BATTLES_DB, SQUADRONS_DB, POINTS_DB, WL_DB):
if db.exists():
tar.add(db, arcname=db.name)
if PREFS_DIR.exists():
tar.add(PREFS_DIR, arcname=PREFS_DIR.name)
log.info("Backup complete: %s", backup_path)
return backup_path
def load_squadron_index() -> tuple[dict[str, int], dict[str, int], dict[int, dict[str, Any]]]:
"""Return (long_name_lower -> clan_id, short_name_lower -> clan_id, clan_id -> row)."""
long_to_id: dict[str, int] = {}
short_to_id: dict[str, int] = {}
by_id: dict[int, dict[str, Any]] = {}
con = sqlite3.connect(SQUADRONS_DB)
try:
con.row_factory = sqlite3.Row
for row in con.execute(
"SELECT clan_id, long_name, short_name, tag_name FROM squadrons_data"
):
cid = int(row["clan_id"])
by_id[cid] = dict(row)
if row["long_name"]:
long_to_id[row["long_name"].lower()] = cid
if row["short_name"]:
short_to_id[row["short_name"].lower()] = cid
finally:
con.close()
log.info("Loaded %d squadrons from squadrons_data", len(by_id))
return long_to_id, short_to_id, by_id
def ensure_squadron_name_history(apply: bool, by_id: dict[int, dict[str, Any]]) -> None:
"""Create squadron_name_history table and seed from current squadrons_data.
Schema:
clan_id INTEGER NOT NULL
long_name TEXT NOT NULL
first_seen INTEGER NOT NULL (unix)
last_seen INTEGER NOT NULL (unix)
PRIMARY KEY (clan_id, long_name)
"""
con = sqlite3.connect(SQUADRONS_DB)
try:
existing = con.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='squadron_name_history'"
).fetchone()
if existing:
log.info("squadron_name_history already exists; skipping create")
else:
log.info("Creating squadron_name_history table")
if apply:
con.executescript(
"""
CREATE TABLE squadron_name_history (
clan_id INTEGER NOT NULL,
long_name TEXT NOT NULL,
first_seen INTEGER NOT NULL,
last_seen INTEGER NOT NULL,
PRIMARY KEY (clan_id, long_name)
);
CREATE INDEX idx_snh_long_name ON squadron_name_history(long_name COLLATE NOCASE);
CREATE INDEX idx_snh_clan_id ON squadron_name_history(clan_id);
"""
)
now = int(time.time())
rows = [
(cid, row["long_name"], now, now)
for cid, row in by_id.items()
if row.get("long_name")
]
log.info("Seeding squadron_name_history with %d (clan_id, long_name) pairs", len(rows))
if apply and rows:
con.executemany(
"""
INSERT INTO squadron_name_history (clan_id, long_name, first_seen, last_seen)
VALUES (?, ?, ?, ?)
ON CONFLICT(clan_id, long_name) DO UPDATE SET last_seen = excluded.last_seen
""",
rows,
)
con.commit()
finally:
con.close()
def add_squadrons_points_index(apply: bool) -> None:
con = sqlite3.connect(SQUADRONS_DB)
try:
log.info("Adding clan_id index on squadrons_points")
if apply:
con.executescript(
"""
CREATE INDEX IF NOT EXISTS idx_squadrons_points_clanid_time
ON squadrons_points(clan_id, unix_time);
"""
)
con.commit()
finally:
con.close()
def migrate_player_games_hist(
apply: bool, long_to_id: dict[str, int]
) -> None:
"""Add clan_id INTEGER column + backfill via long_name lookup. Add index."""
con = sqlite3.connect(SQ_BATTLES_DB)
try:
con.execute("PRAGMA journal_mode=WAL")
cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
if "clan_id" not in cols:
log.info("Adding clan_id column to player_games_hist")
if apply:
con.execute("ALTER TABLE player_games_hist ADD COLUMN clan_id INTEGER")
con.commit()
else:
log.info("player_games_hist.clan_id already present")
# Refresh after the conditional ALTER.
cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
total = con.execute("SELECT COUNT(*) FROM player_games_hist").fetchone()[0]
if "clan_id" in cols:
unbackfilled = con.execute(
"SELECT COUNT(*) FROM player_games_hist WHERE clan_id IS NULL"
).fetchone()[0]
else:
unbackfilled = total
log.info(
"player_games_hist: %d total rows, %d need backfill",
total,
unbackfilled,
)
if apply and unbackfilled:
# Faster path: pre-build a name → clan_id map in Python and run one
# indexed UPDATE per distinct squadron_name. Each UPDATE hits the
# idx_pgh_squadron_name index. ~1500 small ops vs one giant
# correlated subquery. squadron_name is the WT short_name (after
# alphanum strip), but very rarely a long_name leaks through, so
# we accept matches against either.
log.info("Building name → clan_id map in Python...")
sq_con = sqlite3.connect(f"file:{SQUADRONS_DB}?mode=ro", uri=True)
try:
sq_con.row_factory = sqlite3.Row
name_to_id: dict[str, int] = {}
for row in sq_con.execute(
"SELECT clan_id, long_name, short_name FROM squadrons_data"
):
cid = int(row["clan_id"])
if row["long_name"]:
name_to_id.setdefault(row["long_name"].lower(), cid)
if row["short_name"]:
name_to_id.setdefault(row["short_name"].lower(), cid)
finally:
sq_con.close()
log.info("Fetching distinct squadron_name values from player_games_hist...")
distinct_names = [
r[0] for r in con.execute(
"SELECT DISTINCT squadron_name FROM player_games_hist WHERE clan_id IS NULL"
).fetchall()
if r[0]
]
log.info("Distinct unbackfilled squadron_names: %d", len(distinct_names))
updates = [
(name_to_id[n.lower()], n)
for n in distinct_names
if n.lower() in name_to_id
]
log.info("Will UPDATE %d distinct names that resolve to clan_ids", len(updates))
if updates:
cur = con.executemany(
"UPDATE player_games_hist SET clan_id = ? "
"WHERE squadron_name = ? AND clan_id IS NULL",
updates,
)
con.commit()
log.info("Backfill: %s row-updates committed", cur.rowcount)
cols = [r[1] for r in con.execute("PRAGMA table_info(player_games_hist)").fetchall()]
if "clan_id" in cols:
still_null = con.execute(
"SELECT COUNT(*) FROM player_games_hist WHERE clan_id IS NULL"
).fetchone()[0]
log.info("player_games_hist orphans (clan_id NULL after backfill): %d", still_null)
else:
log.info("player_games_hist orphans: n/a (column absent in dry-run)")
log.info("Ensuring index on player_games_hist(clan_id, endtime_unix)")
if apply:
con.execute(
"CREATE INDEX IF NOT EXISTS idx_pgh_clanid_endtime "
"ON player_games_hist(clan_id, endtime_unix)"
)
con.commit()
finally:
con.close()
def migrate_match_summary(apply: bool) -> None:
"""Add winning_clan_id + losing_clan_id, backfill via squadrons_data."""
con = sqlite3.connect(SQ_BATTLES_DB)
try:
con.execute("PRAGMA journal_mode=WAL")
cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
for new_col in ("winning_clan_id", "losing_clan_id"):
if new_col not in cols:
log.info("Adding %s column to match_summary", new_col)
if apply:
con.execute(f"ALTER TABLE match_summary ADD COLUMN {new_col} INTEGER")
con.commit()
else:
log.info("match_summary.%s already present", new_col)
# Refresh after potential ALTERs.
cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
total = con.execute("SELECT COUNT(*) FROM match_summary").fetchone()[0]
if "winning_clan_id" in cols and "losing_clan_id" in cols:
unbackfilled = con.execute(
"SELECT COUNT(*) FROM match_summary "
"WHERE winning_clan_id IS NULL OR losing_clan_id IS NULL"
).fetchone()[0]
else:
# Dry-run path: columns don't exist yet, so every row is "to be backfilled".
unbackfilled = total
log.info(
"match_summary: %d total rows, %d need backfill",
total,
unbackfilled,
)
if apply and unbackfilled:
con.execute(f"ATTACH DATABASE '{SQUADRONS_DB}' AS sq")
try:
# winning_sq / losing_sq can be either short_name or tag_name
# depending on replay metadata. Try short_name first since that's
# what the autologger writes most often.
for col_in, col_out in (
("winning_sq", "winning_clan_id"),
("losing_sq", "losing_clan_id"),
):
log.info("Backfilling %s%s via short_name then long_name", col_out, col_in)
con.execute(
f"""
UPDATE match_summary
SET {col_out} = (
SELECT clan_id FROM sq.squadrons_data
WHERE LOWER(squadrons_data.short_name) = LOWER(match_summary.{col_in})
LIMIT 1
)
WHERE {col_out} IS NULL AND {col_in} IS NOT NULL
"""
)
con.execute(
f"""
UPDATE match_summary
SET {col_out} = (
SELECT clan_id FROM sq.squadrons_data
WHERE LOWER(squadrons_data.long_name) = LOWER(match_summary.{col_in})
LIMIT 1
)
WHERE {col_out} IS NULL AND {col_in} IS NOT NULL
"""
)
con.commit()
finally:
con.execute("DETACH DATABASE sq")
cols = [r[1] for r in con.execute("PRAGMA table_info(match_summary)").fetchall()]
for col, label in (("winning_clan_id", "winning"), ("losing_clan_id", "losing")):
if col not in cols:
log.info("match_summary orphans (%s NULL): n/a (column absent in dry-run)", label)
continue
n = con.execute(
f"SELECT COUNT(*) FROM match_summary WHERE {col} IS NULL"
).fetchone()[0]
log.info("match_summary orphans (%s NULL): %d", label, n)
log.info("Ensuring indexes on match_summary clan_id columns")
if apply:
con.execute(
"CREATE INDEX IF NOT EXISTS idx_ms_winning_clanid ON match_summary(winning_clan_id)"
)
con.execute(
"CREATE INDEX IF NOT EXISTS idx_ms_losing_clanid ON match_summary(losing_clan_id)"
)
con.commit()
finally:
con.close()
def _resolve_squadron_to_clan_id(name: Optional[str], name_to_id: dict[str, int]) -> int:
if not name:
return -1
return name_to_id.get(name.lower(), -1)
def rebuild_points_db(apply: bool, long_to_id: dict[str, int]) -> None:
"""Rebuild profile_member_points, profile_totals, game_cache with clan_id columns.
The squadron text column is preserved for reference but the new tables key
off clan_id for primary keys. Resolution happens in Python so we don't have
to ATTACH squadrons.db (avoids the cross-db lock issue we hit earlier).
"""
# Build name → clan_id map once (long_name AND short_name).
sq_con = sqlite3.connect(f"file:{SQUADRONS_DB}?mode=ro", uri=True)
try:
sq_con.row_factory = sqlite3.Row
name_to_id: dict[str, int] = {}
for row in sq_con.execute(
"SELECT clan_id, long_name, short_name FROM squadrons_data"
):
cid = int(row["clan_id"])
if row["long_name"]:
name_to_id.setdefault(row["long_name"].lower(), cid)
if row["short_name"]:
name_to_id.setdefault(row["short_name"].lower(), cid)
finally:
sq_con.close()
con = sqlite3.connect(POINTS_DB)
try:
con.execute("PRAGMA journal_mode=WAL")
# profile_member_points -------------------------------------------------
cols = [r[1] for r in con.execute("PRAGMA table_info(profile_member_points)").fetchall()]
if "clan_id" in cols:
log.info("profile_member_points already migrated; skipping rebuild")
else:
log.info("Rebuilding profile_member_points with clan_id PK")
if apply:
# Idempotency: a previous half-finished run may have left _new behind.
con.execute("DROP TABLE IF EXISTS profile_member_points_new")
con.execute(
"""CREATE TABLE profile_member_points_new (
clan_id INTEGER NOT NULL,
squadron TEXT NOT NULL,
uid TEXT NOT NULL,
points INTEGER NOT NULL,
PRIMARY KEY (clan_id, uid)
)"""
)
src = con.execute(
"SELECT squadron, uid, points FROM profile_member_points"
).fetchall()
rows = [
(_resolve_squadron_to_clan_id(s, name_to_id), s, u, p)
for (s, u, p) in src
]
con.executemany(
"INSERT OR IGNORE INTO profile_member_points_new "
"(clan_id, squadron, uid, points) VALUES (?, ?, ?, ?)",
rows,
)
con.commit()
log.info("profile_member_points: copied %d rows", len(rows))
con.execute("DROP TABLE profile_member_points")
con.execute("ALTER TABLE profile_member_points_new RENAME TO profile_member_points")
con.execute("CREATE INDEX IF NOT EXISTS idx_pmp_squadron ON profile_member_points(squadron)")
con.commit()
# profile_totals --------------------------------------------------------
cols = [r[1] for r in con.execute("PRAGMA table_info(profile_totals)").fetchall()]
if "clan_id" in cols:
log.info("profile_totals already migrated; skipping rebuild")
else:
log.info("Rebuilding profile_totals with clan_id PK")
if apply:
con.execute("DROP TABLE IF EXISTS profile_totals_new")
con.execute(
"""CREATE TABLE profile_totals_new (
clan_id INTEGER PRIMARY KEY,
squadron TEXT NOT NULL,
total INTEGER NOT NULL
)"""
)
src = con.execute("SELECT squadron, total FROM profile_totals").fetchall()
rows = [
(_resolve_squadron_to_clan_id(s, name_to_id), s, t)
for (s, t) in src
]
con.executemany(
"INSERT OR IGNORE INTO profile_totals_new (clan_id, squadron, total) VALUES (?, ?, ?)",
rows,
)
con.commit()
log.info("profile_totals: copied %d rows", len(rows))
con.execute("DROP TABLE profile_totals")
con.execute("ALTER TABLE profile_totals_new RENAME TO profile_totals")
con.commit()
# game_cache ------------------------------------------------------------
cols = [r[1] for r in con.execute("PRAGMA table_info(game_cache)").fetchall()]
if "clan_id" in cols:
log.info("game_cache already migrated; skipping rebuild")
else:
log.info("Rebuilding game_cache with clan_id in PK")
if apply:
con.execute("DROP TABLE IF EXISTS game_cache_new")
con.execute(
"""CREATE TABLE game_cache_new (
game_id TEXT NOT NULL,
clan_id INTEGER NOT NULL,
squadron TEXT NOT NULL,
diffs_json TEXT NOT NULL,
diff_total INTEGER NOT NULL,
updated_json TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT (strftime('%s','now')),
PRIMARY KEY (game_id, clan_id)
)"""
)
src = con.execute(
"SELECT game_id, squadron, diffs_json, diff_total, updated_json, created_at FROM game_cache"
).fetchall()
rows = [
(
gid,
_resolve_squadron_to_clan_id(s, name_to_id),
s,
dj,
dt,
uj,
ca,
)
for (gid, s, dj, dt, uj, ca) in src
]
con.executemany(
"INSERT OR IGNORE INTO game_cache_new "
"(game_id, clan_id, squadron, diffs_json, diff_total, updated_json, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
rows,
)
con.commit()
log.info("game_cache: copied %d rows", len(rows))
con.execute("DROP TABLE game_cache")
con.execute("ALTER TABLE game_cache_new RENAME TO game_cache")
con.commit()
finally:
con.close()
def rebuild_wl_db(apply: bool) -> None:
"""wl_events / wl_standings are empty - swap schemas to clan_id keyed tables."""
con = sqlite3.connect(WL_DB)
try:
# wl_standings
cols = [r[1] for r in con.execute("PRAGMA table_info(wl_standings)").fetchall()]
if "clan_id" in cols:
log.info("wl_standings already migrated")
else:
log.info("Recreating wl_standings keyed by clan_id (was empty)")
if apply:
con.executescript(
"""
DROP TABLE IF EXISTS wl_standings;
CREATE TABLE wl_standings (
clan_id INTEGER PRIMARY KEY,
squadron TEXT NOT NULL,
wins INTEGER NOT NULL DEFAULT 0,
losses INTEGER NOT NULL DEFAULT 0
);
"""
)
con.commit()
# wl_events: store winner_clan_id alongside winner text
cols = [r[1] for r in con.execute("PRAGMA table_info(wl_events)").fetchall()]
if "winner_clan_id" in cols:
log.info("wl_events already migrated")
else:
log.info("Adding winner_clan_id column to wl_events (table is empty)")
if apply:
con.execute("ALTER TABLE wl_events ADD COLUMN winner_clan_id INTEGER")
con.commit()
finally:
con.close()
def migrate_preferences(
apply: bool,
long_to_id: dict[str, int],
short_to_id: dict[str, int],
by_id: dict[int, dict[str, Any]],
) -> None:
"""Re-key every PREFERENCES/<guild_id>-preferences.json from long_name → str(clan_id).
Special wildcard keys (Global, everything, all, *) are preserved as-is.
Each migrated entry gets a `Long` field with the squadron's current
long_name so display fallback works if squadrons_data is unavailable.
Original files are copied to PREFERENCES_BACKUP_<timestamp>/.
"""
if not PREFS_DIR.exists():
log.warning("PREFERENCES dir missing at %s; skipping prefs migration", PREFS_DIR)
return
ts = time.strftime("%Y%m%d-%H%M%S")
backup_dir = STORAGE_DIR / f"PREFERENCES_BACKUP_{ts}"
if apply:
backup_dir.mkdir(parents=True, exist_ok=True)
pref_files = sorted(PREFS_DIR.glob("*-preferences.json"))
log.info("Found %d preference files to migrate", len(pref_files))
migrated_count = 0
orphan_count = 0
skipped_count = 0
files_changed = 0
for pref_file in pref_files:
try:
data = json.loads(pref_file.read_text(encoding="utf-8"))
except Exception as e:
log.warning("Could not read %s: %s", pref_file.name, e)
continue
if not isinstance(data, dict):
log.warning("Skipping %s: not an object", pref_file.name)
continue
new_data: dict[str, Any] = {}
any_changes = False
for key, value in data.items():
if not isinstance(value, dict):
new_data[key] = value
continue
key_lower = str(key).lower()
# Preserve wildcards / Global
if key_lower in WILDCARD_KEYS:
new_data[key] = value
continue
# Already a numeric clan_id (running migration twice)
if str(key).isdigit():
new_data[key] = value
continue
# Try long_name then short_name
cid = long_to_id.get(key_lower) or short_to_id.get(key_lower)
# Maybe entry has a "Short" hint we can use as fallback
if cid is None and value.get("Short"):
cid = short_to_id.get(str(value["Short"]).lower())
if cid is None:
log.warning(
" [%s] orphan key %r — squadron not found in squadrons_data",
pref_file.name,
key,
)
# Keep the original key so the user doesn't lose their data;
# a future load_guild_preferences will surface it for cleanup.
new_data[key] = value
orphan_count += 1
continue
new_key = str(cid)
row = by_id.get(cid, {})
merged = dict(value)
# Preserve display fields - the bot uses them when squadrons_data is stale
merged.setdefault("Long", row.get("long_name") or key)
if row.get("short_name"):
merged["Short"] = row["short_name"]
if new_key in new_data:
# Two prefs entries for the same squadron (rare). Merge,
# preferring values from this iteration.
existing = new_data[new_key]
if isinstance(existing, dict):
existing.update(merged)
new_data[new_key] = existing
else:
new_data[new_key] = merged
else:
new_data[new_key] = merged
if new_key != key:
any_changes = True
migrated_count += 1
if not any_changes:
skipped_count += 1
continue
if apply:
backup_path = backup_dir / pref_file.name
shutil.copy2(pref_file, backup_path)
tmp = pref_file.with_suffix(".json.tmp")
tmp.write_text(json.dumps(new_data, ensure_ascii=False), encoding="utf-8")
os.replace(tmp, pref_file)
files_changed += 1
log.info(
"Preferences migration: %d files changed, %d unchanged, %d entries migrated, %d orphans",
files_changed,
skipped_count,
migrated_count,
orphan_count,
)
if apply:
log.info("Preference backups saved to %s", backup_dir)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--apply", action="store_true", help="Actually apply changes")
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would happen without writing anything (default)",
)
parser.add_argument(
"--skip-backup",
action="store_true",
help="Skip the tarball backup step (use only if you already have one)",
)
args = parser.parse_args()
if args.apply and args.dry_run:
log.error("--apply and --dry-run are mutually exclusive")
return 2
apply = args.apply
log.info("STORAGE_DIR=%s", STORAGE_DIR)
log.info("apply=%s", apply)
for db in (SQ_BATTLES_DB, SQUADRONS_DB, POINTS_DB, WL_DB):
if not db.exists():
log.error("Required DB missing: %s", db)
return 1
if apply and not args.skip_backup:
backup_storage()
long_to_id, short_to_id, by_id = load_squadron_index()
ensure_squadron_name_history(apply, by_id)
add_squadrons_points_index(apply)
migrate_player_games_hist(apply, long_to_id)
migrate_match_summary(apply)
rebuild_points_db(apply, long_to_id)
rebuild_wl_db(apply)
migrate_preferences(apply, long_to_id, short_to_id, by_id)
if not apply:
log.info("DRY RUN complete. Re-run with --apply to actually write changes.")
else:
log.info("Migration applied successfully.")
return 0
if __name__ == "__main__":
sys.exit(main())
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Move legacy repo-root replays into STORAGE/REPLAYS.
Legacy directories were named replays/0<hex_session_id>. The new canonical
layout is STORAGE/REPLAYS/<hex_session_id>.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = REPO_ROOT / "replays"
_storage_env = os.environ.get("SREBOT_STORAGE_VOL_PATH", "").strip()
if not _storage_env:
raise RuntimeError("SREBOT_STORAGE_VOL_PATH must be set")
DEFAULT_STORAGE = Path(_storage_env)
LEGACY_REPLAY_DIR = re.compile(r"^0([0-9a-fA-F]+)$")
HEX_REPLAY_DIR = re.compile(r"^[0-9a-fA-F]+$")
def canonical_name(name: str) -> str | None:
legacy = LEGACY_REPLAY_DIR.fullmatch(name)
if legacy:
return legacy.group(1).lower()
if HEX_REPLAY_DIR.fullmatch(name):
return name.lower()
return None
def iter_files(root: Path) -> list[Path]:
return [p for p in root.rglob("*") if p.is_file()]
def copy_replay_dir(source: Path, dest: Path, dry_run: bool) -> tuple[int, int]:
copied = 0
skipped = 0
for src_file in iter_files(source):
rel = src_file.relative_to(source)
dst_file = dest / rel
if dst_file.exists() and dst_file.stat().st_size == src_file.stat().st_size:
skipped += 1
continue
copied += 1
if dry_run:
continue
dst_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dst_file)
return copied, skipped
def copied_completely(source: Path, dest: Path) -> bool:
for src_file in iter_files(source):
dst_file = dest / src_file.relative_to(source)
if not dst_file.exists() or dst_file.stat().st_size != src_file.stat().st_size:
return False
return True
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE, help="Legacy replays directory.")
parser.add_argument(
"--storage-dir",
type=Path,
default=DEFAULT_STORAGE,
help="Storage root. Destination defaults to <storage-dir>/REPLAYS.",
)
parser.add_argument("--dest", type=Path, help="Destination replay directory.")
parser.add_argument("--execute", action="store_true", help="Actually copy files. Default is dry-run.")
parser.add_argument("--move", action="store_true", help="Remove source dirs after a successful executed copy.")
args = parser.parse_args()
source = args.source.expanduser().resolve()
dest_root = (args.dest or (args.storage_dir / "REPLAYS")).expanduser().resolve()
dry_run = not args.execute
if args.move and dry_run:
parser.error("--move requires --execute")
if not source.exists():
raise SystemExit(f"Source does not exist: {source}")
planned = 0
copied_total = 0
skipped_total = 0
print(f"Source: {source}")
print(f"Destination: {dest_root}")
print(f"Mode: {'dry-run' if dry_run else 'execute'}")
for entry in sorted(source.iterdir()):
if not entry.is_dir():
continue
new_name = canonical_name(entry.name)
if new_name is None:
print(f"skip non-replay dir: {entry.name}")
continue
planned += 1
dest = dest_root / new_name
copied, skipped = copy_replay_dir(entry, dest, dry_run)
copied_total += copied
skipped_total += skipped
print(f"{entry.name} -> {new_name}: copy {copied}, skip {skipped}")
if args.move and copied_completely(entry, dest):
shutil.rmtree(entry)
print(f"removed source dir: {entry}")
print(f"Replay dirs: {planned}; files to copy: {copied_total}; already present: {skipped_total}")
if dry_run:
print("Dry-run only. Re-run with --execute to copy files.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Restarts srebot-api + srebot-web and times the recovery milestones.
# Usage: bash scripts/restart-test.sh
set -u
T0=$(date +%s.%N)
echo "=== T0: $(date +%H:%M:%S.%3N) restarting srebot-api + srebot-web ==="
pm2 restart srebot-api srebot-web >/dev/null
echo "--- polling /health (every 0.5s until ready:true) ---"
for i in $(seq 1 120); do
R=$(curl -sS --max-time 2 http://127.0.0.1:6000/health 2>/dev/null || true)
if echo "$R" | grep -q '"ready":true'; then
echo "ready at +$(echo "$(date +%s.%N) - $T0" | bc)s : $R"
break
fi
sleep 0.5
done
echo "--- /api/squadrons/1066957 (first cold hit, separate connection) ---"
curl -sS -o /dev/null -w " HTTP=%{http_code} time=%{time_total}s\n" "http://127.0.0.1:6000/api/squadrons/1066957"
echo "--- 4 leaderboards in parallel (heavyDb concurrent reads) ---"
for ep in stats squadrons players vehicles; do
(curl -sS -o /dev/null -w " $ep: HTTP=%{http_code} time=%{time_total}s\n" "http://127.0.0.1:6000/api/leaderboard/$ep") &
done
wait
echo "=== full warm at +$(echo "$(date +%s.%N) - $T0" | bc)s ==="
echo "--- pm2 status ---"
pm2 info srebot-api | grep -E "uptime|restart|status" | head -5
+5999
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
[Unit]
Description=SREBOT Discord Bot for War Thunder
After=network.target
Wants=network.target
[Service]
Type=simple
User=sre
Group=sre
WorkingDirectory=/home/sre/SREBOT/BOT
ExecStart=/home/sre/SREBOT/BOT/venv/bin/python bot_runner.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=srebot
# Environment
Environment=PYTHONUNBUFFERED=1
Environment=SREBOT_STORAGE_VOL_PATH=/mnt/HC_Volume_105581488/STORAGE
# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=false
ReadWritePaths=/home/sre/SREBOT /mnt/HC_Volume_105581488
[Install]
WantedBy=multi-user.target
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""Entry point for SREBOT Discord Bot."""
import sys
from pathlib import Path
# Ensure project root is in path
sys.path.insert(0, str(Path(__file__).parent))
from BOT.botscript import bot, TOKEN
if __name__ == "__main__":
if TOKEN:
bot.run(TOKEN)
else:
print("ERROR: DISCORD_KEY not set in environment")
sys.exit(1)
+69
View File
@@ -0,0 +1,69 @@
"""Quick test to check Spectra host connectivity — WS + HTTP."""
import asyncio
import os
import aiohttp
from dotenv import load_dotenv
from websockets.asyncio.client import connect
load_dotenv()
API_KEY = os.getenv("SPECTRA_API_KEY", "")
BASE_URL = os.getenv("SPECTRA_API_URL", "")
WS_URL = os.getenv("SPECTRA_WS_SQB_URL", "")
WS_GOB_URL = os.getenv("SPECTRA_WS_GOB_URL", "")
async def test_http():
"""Test HTTP API endpoint."""
url = f"{BASE_URL}/v1/replays/sort"
auth = API_KEY if API_KEY.startswith("Bearer ") else f"Bearer {API_KEY}"
headers = {"accept": "application/json", "Authorization": auth, "sortField": "sqb", "id": "463391108263081012"}
print(f"[HTTP] POST {url}")
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
print(f"[HTTP] Status: {resp.status}")
text = await resp.text()
print(f"[HTTP] Body: {text[:500]}")
except Exception as e:
print(f"[HTTP] Failed: {type(e).__name__}: {e}")
async def test_ws_sqb():
"""Test SQB WebSocket connection."""
url = WS_URL
print(f"\n[WS-SQB] Connecting to {url}")
try:
async with connect(url, additional_headers={"Authorization": API_KEY}, open_timeout=15) as ws:
print("[WS-SQB] Connected! Waiting for message (10s)...")
msg = await asyncio.wait_for(ws.recv(), timeout=10)
print(f"[WS-SQB] Got message: {str(msg)[:300]}")
except Exception as e:
print(f"[WS-SQB] Failed: {type(e).__name__}: {e}")
async def test_ws_gob():
"""Test GOB WebSocket connection."""
url = WS_GOB_URL
print(f"\n[WS-GOB] Connecting to {url}")
try:
async with connect(url, additional_headers={"Authorization": API_KEY}, open_timeout=15) as ws:
print("[WS-GOB] Connected! Waiting for message (10s)...")
msg = await asyncio.wait_for(ws.recv(), timeout=10)
print(f"[WS-GOB] Got message ({len(msg)} bytes)")
except Exception as e:
print(f"[WS-GOB] Failed: {type(e).__name__}: {e}")
async def main():
print(f"API Key configured: {'Yes' if API_KEY else 'No'}")
print(f"Base URL: {BASE_URL}")
print(f"WS URL: {WS_URL}")
print(f"WS GOB URL: {WS_GOB_URL}\n")
await test_http()
await test_ws_sqb()
await test_ws_gob()
if __name__ == "__main__":
asyncio.run(main())
+18
View File
@@ -0,0 +1,18 @@
# Environment
NODE_ENV=production
# Server Configuration
PORT=3000
# Domain Configuration (CORS)
PRODUCTION_DOMAIN=https://srebot-meow.ing
# External API Configuration
EXTERNAL_API_URL=http://localhost:6000
# Logging Configuration
LOG_LEVEL=info
# Rate Limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=100
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+55
View File
@@ -0,0 +1,55 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# Operating System Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Editor directories and files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp
temp/
# Debug and test files
debug-*.js
test-*.js
# Build output (obfuscated files and generated CSS)
public/js/dist/
public/css/output.css.map
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Some files were not shown because too many files have changed in this diff Show More