diff --git a/config.ini.example b/config.ini.example index 322c4df..4fbee27 100644 --- a/config.ini.example +++ b/config.ini.example @@ -85,6 +85,21 @@ bot_latitude = 40.7128 # Example: -74.0060 for New York City, -123.00 for Victoria BC bot_longitude = -74.0060 +[Localization] +# Language code for bot responses (en, es, es-MX, es-ES, fr, de, ja, etc.) +# Default: en (English) +# The bot will use translations from translations/{language}.json +# Supports locale codes: +# - Simple codes: en, es, fr, de, ja +# - Locale codes: es-MX (Mexican Spanish), es-ES (Spain Spanish), fr-CA (Canadian French) +# If locale-specific file not found, falls back to base language (e.g., es.json) +# If translation file is missing or key not found, falls back to English +language = en + +# Path to translation files directory (relative to bot root) +# Default: translations/ +translation_path = translations/ + [Admin_ACL] # Admin Access Control List (ACL) for restricted commands # Only users with public keys listed here can execute admin commands @@ -171,14 +186,16 @@ advert_interval_hours = 0 [Keywords] # Keyword-response pairs (keyword = response format) -# Available fields: {sender}, {connection_info}, {snr}, {timestamp}, {path} +# Available fields: {sender}, {connection_info}, {snr}, {timestamp}, {path}, {path_distance}, {firstlast_distance} # {sender}: Name/ID of message sender # {connection_info}: "Direct connection (0 hops)" or "Routed through X hops" # {snr}: Signal-to-noise ratio in dB # {timestamp}: Message timestamp in HH:MM:SS format # {path}: Message routing path (e.g., "01,5f (2 hops)") # {rssi}: Received Signal Strength Indicator in dBm -test = "ack {sender}{phrase_part} | {connection_info} | Received at: {timestamp}" +# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)") +# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing) +test = "ack @[{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}" ping = "Pong!" pong = "Ping!" help = "Bot Help: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, sun, moon, solar, hfcond, satpass, prefix, path, sports, dice, roll, stats | Use 'help ' for details" @@ -235,8 +252,10 @@ meshcore_log_level = INFO [Custom_Syntax] # Custom syntax patterns for special message formats # Format: pattern = "response_format" -# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path} +# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path}, {path_distance}, {firstlast_distance} # {phrase}: The text after the trigger (for custom syntax patterns) +# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)") +# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing) # # Note: The "t" command is now handled by the test command as an alias # "t phrase" works the same as "test phrase" - both use the test response format diff --git a/modules/command_manager.py b/modules/command_manager.py index 92090dc..da77555 100644 --- a/modules/command_manager.py +++ b/modules/command_manager.py @@ -145,19 +145,28 @@ class CommandManager: content_lower = content.lower() # Check for help requests first (special handling) - if content_lower.startswith('help '): - command_name = content_lower[5:].strip() # Remove "help " prefix - help_text = self.get_help_for_command(command_name, message) - # Format the help response with message data (same as other keywords) - help_text = self.format_keyword_response(help_text, message) - matches.append(('help', help_text)) - return matches - elif content_lower == 'help': - help_text = self.get_general_help() - # Format the help response with message data (same as other keywords) - help_text = self.format_keyword_response(help_text, message) - matches.append(('help', help_text)) - return matches + # Check both English "help" and translated help keywords + help_keywords = ['help'] + if 'help' in self.commands: + help_command = self.commands['help'] + if hasattr(help_command, 'keywords'): + help_keywords = [k.lower() for k in help_command.keywords] + + # Check if message starts with any help keyword + for help_keyword in help_keywords: + if content_lower.startswith(help_keyword + ' '): + command_name = content_lower[len(help_keyword):].strip() # Remove help keyword prefix + help_text = self.get_help_for_command(command_name, message) + # Format the help response with message data (same as other keywords) + help_text = self.format_keyword_response(help_text, message) + matches.append(('help', help_text)) + return matches + elif content_lower == help_keyword: + help_text = self.get_general_help() + # Format the help response with message data (same as other keywords) + help_text = self.format_keyword_response(help_text, message) + matches.append(('help', help_text)) + return matches # Check all loaded plugins for matches for command_name, command in self.commands.items(): @@ -360,6 +369,9 @@ class CommandManager: except TypeError: # Fallback for commands that don't accept message parameter help_text = command.get_help_text() + # Use translator if available + if hasattr(self.bot, 'translator'): + return self.bot.translator.translate('commands.help.specific', command=command_name, help_text=help_text) return f"Help {command_name}: {help_text}" # If not found, search through all commands and their keywords @@ -372,6 +384,9 @@ class CommandManager: except TypeError: # Fallback for commands that don't accept message parameter help_text = cmd_instance.get_help_text() + # Use translator if available + if hasattr(self.bot, 'translator'): + return self.bot.translator.translate('commands.help.specific', command=command_name, help_text=help_text) return f"Help {command_name}: {help_text}" # If still not found, return unknown command message with helpful suggestion @@ -381,7 +396,10 @@ class CommandManager: if hasattr(cmd_instance, 'keywords'): available_commands.extend(cmd_instance.keywords) - return f"Unknown: {command_name}. Available: {', '.join(sorted(set(available_commands)))}. Try 'help' for command list." + available_str = ', '.join(sorted(set(available_commands))) + if hasattr(self.bot, 'translator'): + return self.bot.translator.translate('commands.help.unknown', command=command_name, available=available_str) + return f"Unknown: {command_name}. Available: {available_str}. Try 'help' for command list." def get_general_help(self) -> str: """Get general help text from config (LoRa-friendly compact format)""" @@ -481,9 +499,11 @@ class CommandManager: # Check if command can execute (cooldown, DM requirements, etc.) if not command.can_execute_now(message): if command.requires_dm and not message.is_dm: - await self.send_response(message, f"Command '{command_name}' can only be used in DMs") + error_msg = command.translate('errors.dm_only', command=command_name) + await self.send_response(message, error_msg) elif command.requires_admin_access(): - await self.send_response(message, f"❌ Access denied: Command '{command_name}' requires admin privileges") + error_msg = command.translate('errors.access_denied', command=command_name) + await self.send_response(message, error_msg) elif hasattr(command, 'get_remaining_cooldown') and callable(command.get_remaining_cooldown): # Check if it's the per-user version (takes user_id parameter) import inspect @@ -494,7 +514,8 @@ class CommandManager: remaining = command.get_remaining_cooldown() if remaining > 0: - await self.send_response(message, f"Command '{command_name}' is on cooldown. Wait {remaining} seconds.") + error_msg = command.translate('errors.cooldown', command=command_name, seconds=remaining) + await self.send_response(message, error_msg) return try: @@ -536,7 +557,8 @@ class CommandManager: except Exception as e: self.logger.error(f"Error executing command '{command_name}': {e}") # Send error message to user - await self.send_response(message, f"Error executing {command_name}: {e}") + error_msg = command.translate('errors.execution_error', command=command_name, error=str(e)) + await self.send_response(message, error_msg) # Capture failed command for web viewer if (hasattr(self.bot, 'web_viewer_integration') and diff --git a/modules/commands/advert_command.py b/modules/commands/advert_command.py index 0c1aa5f..68660bc 100644 --- a/modules/commands/advert_command.py +++ b/modules/commands/advert_command.py @@ -21,7 +21,7 @@ class AdvertCommand(BaseCommand): category = "special" def get_help_text(self) -> str: - return self.description + return self.translate('commands.advert.description') def can_execute(self, message: MeshMessage) -> bool: """Check if advert command can be executed""" @@ -45,7 +45,7 @@ class AdvertCommand(BaseCommand): if hasattr(self.bot, 'last_advert_time') and self.bot.last_advert_time and (current_time - self.bot.last_advert_time) < 3600: remaining_time = 3600 - (current_time - self.bot.last_advert_time) remaining_minutes = int(remaining_time // 60) - response = f"Advert cooldown active. Please wait {remaining_minutes} more minutes before requesting another advert." + response = self.translate('commands.advert.cooldown_active', minutes=remaining_minutes) await self.send_response(message, response) return True @@ -58,14 +58,14 @@ class AdvertCommand(BaseCommand): if hasattr(self.bot, 'last_advert_time'): self.bot.last_advert_time = current_time - response = "Flood advert sent successfully!" + response = self.translate('commands.advert.success') self.logger.info("Flood advert sent successfully via DM command") await self.send_response(message, response) return True except Exception as e: - error_msg = f"Error sending flood advert: {e}" - self.logger.error(error_msg) + error_msg = self.translate('commands.advert.error', error=str(e)) + self.logger.error(f"Error sending flood advert: {e}") await self.send_response(message, error_msg) return False diff --git a/modules/commands/alternatives/wx_international.py b/modules/commands/alternatives/wx_international.py index 57a6467..b2bbc77 100644 --- a/modules/commands/alternatives/wx_international.py +++ b/modules/commands/alternatives/wx_international.py @@ -6,7 +6,7 @@ Provides worldwide weather information using Open-Meteo API import re import requests -from datetime import datetime +from datetime import datetime, timedelta from geopy.geocoders import Nominatim from ..base_command import BaseCommand from ...models import MeshMessage @@ -22,8 +22,8 @@ class GlobalWxCommand(BaseCommand): category = "weather" cooldown_seconds = 5 # 5 second cooldown per user to prevent API abuse - # Error constants - ERROR_FETCHING_DATA = "Error fetching weather data" + # Error constants - will use translations instead + ERROR_FETCHING_DATA = "ERROR_FETCHING_DATA" # Placeholder, will use translate() NO_ALERTS = "No weather alerts available" def __init__(self, bot): @@ -60,7 +60,7 @@ class GlobalWxCommand(BaseCommand): self.db_manager = bot.db_manager def get_help_text(self) -> str: - return "Usage: gwx - Get weather for any global location (city, country, or coordinates)" + return self.translate('commands.gwx.help') def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with a weather keyword""" @@ -115,20 +115,48 @@ class GlobalWxCommand(BaseCommand): """Execute the weather command""" content = message.content.strip() - # Parse the command to extract location - parts = content.split(maxsplit=1) + # Parse the command to extract location and forecast type + parts = content.split() if len(parts) < 2: - await self.send_response(message, "Usage: gwx - Example: gwx Tokyo or gwx Paris, France") + await self.send_response(message, self.translate('commands.gwx.usage')) return True - location = parts[1].strip() + # Check for forecast type options: "tomorrow", or a number 2-7 + forecast_type = "default" + num_days = 7 # Default for multi-day forecast + location_parts = parts[1:] + + # Check last part for forecast type + if len(location_parts) > 0: + last_part = location_parts[-1].lower() + if last_part == "tomorrow": + forecast_type = "tomorrow" + location_parts = location_parts[:-1] + elif last_part.isdigit(): + # Check if it's a number between 2-7 + days = int(last_part) + if 2 <= days <= 7: + forecast_type = "multiday" + num_days = days + location_parts = location_parts[:-1] + elif last_part in ["7day", "7-day"]: + forecast_type = "multiday" + num_days = 7 + location_parts = location_parts[:-1] + + # Join remaining parts to handle "city, country" format + location = ' '.join(location_parts).strip() + + if not location: + await self.send_response(message, self.translate('commands.gwx.usage')) + return True try: # Record execution for this user self._record_execution(message.sender_id) # Get weather data for the location - weather_data = await self.get_weather_for_location(location) + weather_data = await self.get_weather_for_location(location, forecast_type, num_days) # Check if we need to send multiple messages (for alerts) if isinstance(weather_data, tuple) and weather_data[0] == "multi_message": @@ -143,6 +171,9 @@ class GlobalWxCommand(BaseCommand): # Send alerts await self.send_response(message, weather_data[2]) + elif forecast_type == "multiday": + # Use message splitting for multi-day forecasts + await self._send_multiday_forecast(message, weather_data) else: await self.send_response(message, weather_data) @@ -150,40 +181,54 @@ class GlobalWxCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in global weather command: {e}") - await self.send_response(message, f"Error getting weather data: {e}") + await self.send_response(message, self.translate('commands.gwx.error', error=str(e))) return True - async def get_weather_for_location(self, location: str) -> str: - """Get weather data for any global location""" + async def get_weather_for_location(self, location: str, forecast_type: str = "default", num_days: int = 7) -> str: + """Get weather data for any global location + + Args: + location: The location (city name, etc.) + forecast_type: "default", "tomorrow", or "multiday" + num_days: Number of days for multiday forecast (2-7) + """ try: # Convert location to lat/lon with address details result = self.geocode_location(location) if not result or result[0] is None or result[1] is None: - return f"Could not find location '{location}'" + return self.translate('commands.gwx.no_location', location=location) lat, lon, address_info, geocode_result = result # Format location name for display location_display = self._format_location_display(address_info, geocode_result, location) - # Get weather forecast from Open-Meteo - weather_text = self.get_open_meteo_weather(lat, lon) - if weather_text == self.ERROR_FETCHING_DATA: - return "Error fetching weather data from Open-Meteo" + # Get weather forecast from Open-Meteo based on type + if forecast_type == "tomorrow": + weather_text = self.get_open_meteo_weather(lat, lon, forecast_type="tomorrow") + elif forecast_type == "multiday": + weather_text = self.get_open_meteo_weather(lat, lon, forecast_type="multiday", num_days=num_days) + else: + weather_text = self.get_open_meteo_weather(lat, lon) - # Check for severe weather warnings (Open-Meteo doesn't provide detailed alerts, - # but we can infer from extreme conditions) - alert_text = self._check_extreme_conditions(weather_text) + # Check if it's an error (translated error message) + error_fetching = self.translate('commands.gwx.error_fetching') + if weather_text == error_fetching or weather_text == self.ERROR_FETCHING_DATA: + return self.translate('commands.gwx.error_fetching_api') - if alert_text: - # Return multi-message format - return ("multi_message", f"{location_display}: {weather_text}", alert_text) + # Check for severe weather warnings (only for default forecast type) + if forecast_type == "default": + alert_text = self._check_extreme_conditions(weather_text) + + if alert_text: + # Return multi-message format + return ("multi_message", f"{location_display}: {weather_text}", alert_text) return f"{location_display}: {weather_text}" except Exception as e: self.logger.error(f"Error getting weather for {location}: {e}") - return f"Error getting weather data: {e}" + return self.translate('commands.gwx.error', error=str(e)) def geocode_location(self, location: str) -> tuple: """Convert location string to lat/lon with address details""" @@ -340,12 +385,27 @@ class GlobalWxCommand(BaseCommand): } return state_map.get(state, state) - def get_open_meteo_weather(self, lat: float, lon: float) -> str: - """Get weather forecast from Open-Meteo API""" + def get_open_meteo_weather(self, lat: float, lon: float, forecast_type: str = "default", num_days: int = 7) -> str: + """Get weather forecast from Open-Meteo API + + Args: + lat: Latitude + lon: Longitude + forecast_type: "default", "tomorrow", or "multiday" + num_days: Number of days for multiday forecast (2-7) + """ try: # Open-Meteo API endpoint with current weather and forecast api_url = "https://api.open-meteo.com/v1/forecast" + # Determine forecast_days based on type + if forecast_type == "multiday": + forecast_days = min(num_days, 7) # Open-Meteo supports up to 7 days + elif forecast_type == "tomorrow": + forecast_days = 2 # Need today and tomorrow + else: + forecast_days = 2 # Default + params = { 'latitude': lat, 'longitude': lon, @@ -356,14 +416,29 @@ class GlobalWxCommand(BaseCommand): 'wind_speed_unit': self.wind_speed_unit, 'precipitation_unit': self.precipitation_unit, 'timezone': 'auto', - 'forecast_days': 2 + 'forecast_days': forecast_days } + # For tomorrow or multiday, return raw data for formatting + if forecast_type in ["tomorrow", "multiday"]: + response = requests.get(api_url, params=params, timeout=self.url_timeout) + + if not response.ok: + self.logger.warning(f"Error fetching weather from Open-Meteo: {response.status_code}") + return self.translate('commands.gwx.error_fetching') + + data = response.json() + + if forecast_type == "tomorrow": + return self.format_tomorrow_forecast(data) + elif forecast_type == "multiday": + return self.format_multiday_forecast(data, num_days) + response = requests.get(api_url, params=params, timeout=self.url_timeout) if not response.ok: self.logger.warning(f"Error fetching weather from Open-Meteo: {response.status_code}") - return self.ERROR_FETCHING_DATA + return self.translate('commands.gwx.error_fetching') data = response.json() @@ -420,9 +495,9 @@ class GlobalWxCommand(BaseCommand): now = datetime.now() hour = now.hour if 6 <= hour < 18: - period_name = "Today" + period_name = self.translate('commands.gwx.periods.today') else: - period_name = "Tonight" + period_name = self.translate('commands.gwx.periods.tonight') # Build current weather string weather = f"{period_name}: {weather_emoji}{weather_desc} {temp}{temp_symbol}" @@ -482,7 +557,7 @@ class GlobalWxCommand(BaseCommand): tomorrow_emoji = self._get_weather_emoji(tomorrow_code) # Get tomorrow's period name - tomorrow_period = "Tomorrow" + tomorrow_period = self.translate('commands.gwx.periods.tomorrow') tomorrow_str = f" | {tomorrow_period}: {tomorrow_emoji}{tomorrow_high}{temp_symbol}/{tomorrow_low}{temp_symbol}" # Only add if we have space (leave room for potential precipitation) @@ -501,7 +576,188 @@ class GlobalWxCommand(BaseCommand): except Exception as e: self.logger.error(f"Error fetching Open-Meteo weather: {e}") - return self.ERROR_FETCHING_DATA + return self.translate('commands.gwx.error_fetching') + + def format_tomorrow_forecast(self, data: dict) -> str: + """Format a detailed forecast for tomorrow""" + try: + daily = data.get('daily', {}) + if not daily or len(daily.get('temperature_2m_max', [])) < 2: + return self.translate('commands.gwx.tomorrow_not_available') + + temp_symbol = "°F" if self.temperature_unit == 'fahrenheit' else "°C" + tomorrow_high = int(daily['temperature_2m_max'][1]) + tomorrow_low = int(daily['temperature_2m_min'][1]) + tomorrow_code = daily['weather_code'][1] + tomorrow_emoji = self._get_weather_emoji(tomorrow_code) + tomorrow_desc = self._get_weather_description(tomorrow_code) + + # Get wind info if available + wind_info = "" + if len(daily.get('wind_speed_10m_max', [])) > 1: + wind_speed = int(daily['wind_speed_10m_max'][1]) + if wind_speed >= 3: + wind_info = f" {wind_speed}" + if len(daily.get('wind_gusts_10m_max', [])) > 1: + wind_gusts = int(daily['wind_gusts_10m_max'][1]) + if wind_gusts > wind_speed + 3: + wind_info += f"G{wind_gusts}" + + # Get precipitation probability + precip_info = "" + if len(daily.get('precipitation_probability_max', [])) > 1: + precip_prob = daily['precipitation_probability_max'][1] + if precip_prob >= 30: + precip_info = f" 🌦️{precip_prob}%" + + tomorrow_period = self.translate('commands.gwx.periods.tomorrow') + return f"{tomorrow_period}: {tomorrow_emoji}{tomorrow_desc} {tomorrow_high}{temp_symbol}/{tomorrow_low}{temp_symbol}{wind_info}{precip_info}" + + except Exception as e: + self.logger.error(f"Error formatting tomorrow forecast: {e}") + return self.translate('commands.gwx.tomorrow_error') + + def format_multiday_forecast(self, data: dict, num_days: int = 7) -> str: + """Format a less detailed multi-day forecast summary""" + try: + daily = data.get('daily', {}) + if not daily: + return self.translate('commands.gwx.multiday_not_available', num_days=num_days) + + temp_symbol = "°F" if self.temperature_unit == 'fahrenheit' else "°C" + temps_max = daily.get('temperature_2m_max', []) + temps_min = daily.get('temperature_2m_min', []) + weather_codes = daily.get('weather_code', []) + + if len(temps_max) < num_days + 1: # +1 because index 0 is today + num_days = len(temps_max) - 1 + + # Map day names to 1-2 letter abbreviations + day_abbrev_map = { + 'Monday': 'M', + 'Tuesday': 'T', + 'Wednesday': 'W', + 'Thursday': 'Th', + 'Friday': 'F', + 'Saturday': 'Sa', + 'Sunday': 'Su' + } + + parts = [] + today = datetime.now() + + # Start from tomorrow (index 1) + for i in range(1, min(num_days + 1, len(temps_max))): + day_date = today + timedelta(days=i) + day_name = day_date.strftime('%A') + day_abbrev = day_abbrev_map.get(day_name, day_name[:2]) + + high = int(temps_max[i]) + low = int(temps_min[i]) + code = weather_codes[i] if i < len(weather_codes) else 0 + emoji = self._get_weather_emoji(code) + desc = self._get_weather_description(code) + + # Abbreviate description if needed + desc_short = desc + if len(desc) > 20: + desc_short = desc[:17] + "..." + + parts.append(f"{day_abbrev}: {emoji}{desc_short} {high}{temp_symbol}/{low}{temp_symbol}") + + if not parts: + return self.translate('commands.gwx.multiday_not_available', num_days=num_days) + + return "\n".join(parts) + + except Exception as e: + self.logger.error(f"Error formatting {num_days}-day forecast: {e}") + return self.translate('commands.gwx.multiday_error', num_days=num_days) + + def _count_display_width(self, text: str) -> int: + """Count display width of text, accounting for emojis which may take 2 display units""" + import re + # Count regular characters + width = len(text) + # Emojis typically take 2 display units in terminals/clients + # Count emoji characters (basic emoji pattern) + emoji_pattern = re.compile( + "[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F1E0-\U0001F1FF" # flags + "\U00002702-\U000027B0" # dingbats + "\U000024C2-\U0001F251" # enclosed characters + "]+", + flags=re.UNICODE + ) + emoji_matches = emoji_pattern.findall(text) + # Each emoji sequence adds 1 extra width unit (since len() already counts it as 1) + # So we add 1 for each emoji sequence to account for display width + width += len(emoji_matches) + return width + + async def _send_multiday_forecast(self, message: MeshMessage, forecast_text: str): + """Send multi-day forecast response, splitting into multiple messages if needed""" + import asyncio + + lines = forecast_text.split('\n') + + # Remove empty lines + lines = [line.strip() for line in lines if line.strip()] + + if not lines: + return + + # If single line and under 130 chars, send as-is + if self._count_display_width(forecast_text) <= 130: + await self.send_response(message, forecast_text) + return + + # Multi-line message - try to fit as many days as possible in one message + # Only split when necessary (message would exceed 130 chars) + current_message = "" + message_count = 0 + + for i, line in enumerate(lines): + if not line: + continue + + # Check if adding this line would exceed 130 characters (using display width) + if current_message: + test_message = current_message + "\n" + line + else: + test_message = line + + # Only split if message would exceed 130 chars (using display width) + if self._count_display_width(test_message) > 130: + # Send current message and start new one + if current_message: + await self.send_response(message, current_message) + message_count += 1 + # Wait between messages (same as other commands) + if i < len(lines): + await asyncio.sleep(2.0) + + current_message = line + else: + # Single line is too long, send it anyway (will be truncated by bot) + await self.send_response(message, line) + message_count += 1 + if i < len(lines) - 1: + await asyncio.sleep(2.0) + current_message = "" + else: + # Add line to current message (fits within 130 chars) + if current_message: + current_message += "\n" + line + else: + current_message = line + + # Send the last message if there's content + if current_message: + await self.send_response(message, current_message) def _degrees_to_direction(self, degrees: float) -> str: """Convert wind direction in degrees to compass direction with emoji""" @@ -525,39 +781,46 @@ class GlobalWxCommand(BaseCommand): def _get_weather_description(self, code: int) -> str: """Convert WMO weather code to description""" - # WMO Weather interpretation codes - weather_codes = { - 0: "Clear", - 1: "Mostly Clear", - 2: "Partly Cloudy", - 3: "Overcast", - 45: "Foggy", - 48: "Foggy", - 51: "Light Drizzle", - 53: "Drizzle", - 55: "Heavy Drizzle", - 56: "Light Freezing Drizzle", - 57: "Freezing Drizzle", - 61: "Light Rain", - 63: "Rain", - 65: "Heavy Rain", - 66: "Light Freezing Rain", - 67: "Freezing Rain", - 71: "Light Snow", - 73: "Snow", - 75: "Heavy Snow", - 77: "Snow Grains", - 80: "Light Showers", - 81: "Showers", - 82: "Heavy Showers", - 85: "Light Snow Showers", - 86: "Snow Showers", - 95: "Thunderstorm", - 96: "T-Storm w/Hail", - 99: "Severe T-Storm" - } + # Try to get from translations first + key = f"commands.gwx.weather_descriptions.{code}" + description = self.translate(key) - return weather_codes.get(code, "Unknown") + # If translation returned the key (not found), try fallback + if description == key: + # Fallback to hardcoded descriptions + weather_codes = { + 0: "Clear", + 1: "Mostly Clear", + 2: "Partly Cloudy", + 3: "Overcast", + 45: "Foggy", + 48: "Foggy", + 51: "Light Drizzle", + 53: "Drizzle", + 55: "Heavy Drizzle", + 56: "Light Freezing Drizzle", + 57: "Freezing Drizzle", + 61: "Light Rain", + 63: "Rain", + 65: "Heavy Rain", + 66: "Light Freezing Rain", + 67: "Freezing Rain", + 71: "Light Snow", + 73: "Snow", + 75: "Heavy Snow", + 77: "Snow Grains", + 80: "Light Showers", + 81: "Showers", + 82: "Heavy Showers", + 85: "Light Snow Showers", + 86: "Snow Showers", + 95: "Thunderstorm", + 96: "T-Storm w/Hail", + 99: "Severe T-Storm" + } + return weather_codes.get(code, self.translate('commands.gwx.weather_descriptions.unknown')) + + return description def _get_weather_emoji(self, code: int) -> str: """Convert WMO weather code to emoji""" @@ -603,25 +866,45 @@ class GlobalWxCommand(BaseCommand): if temp_match: temp = int(temp_match.group(1)) if temp >= 95: - warnings.append("⚠️ Extreme heat") + warnings.append(self.translate('commands.gwx.warnings.extreme_heat')) elif temp <= 20: - warnings.append("⚠️ Extreme cold") + warnings.append(self.translate('commands.gwx.warnings.extreme_cold')) # Check for severe weather indicators - if "Heavy Rain" in weather_text or "Heavy Showers" in weather_text: - warnings.append("⚠️ Heavy rain") + # Note: We check for English strings here since weather descriptions might be in English + # In a fully localized version, we'd need to check translated strings too + heavy_rain_en = "Heavy Rain" + heavy_showers_en = "Heavy Showers" + thunderstorm_en = "Thunderstorm" + t_storm_en = "T-Storm" + heavy_snow_en = "Heavy Snow" + snow_showers_en = "Snow Showers" - if "Thunderstorm" in weather_text or "T-Storm" in weather_text: - warnings.append("⚠️ Thunderstorms") + # Also get translated versions for checking + heavy_rain_trans = self.translate('commands.gwx.weather_descriptions.65') + heavy_showers_trans = self.translate('commands.gwx.weather_descriptions.82') + thunderstorm_trans = self.translate('commands.gwx.weather_descriptions.95') + t_storm_trans = self.translate('commands.gwx.weather_descriptions.96') + heavy_snow_trans = self.translate('commands.gwx.weather_descriptions.75') + snow_showers_trans = self.translate('commands.gwx.weather_descriptions.86') - if "Heavy Snow" in weather_text or "Snow Showers" in weather_text: - warnings.append("⚠️ Heavy snow") + if (heavy_rain_en in weather_text or heavy_showers_en in weather_text or + heavy_rain_trans in weather_text or heavy_showers_trans in weather_text): + warnings.append(self.translate('commands.gwx.warnings.heavy_rain')) + + if (thunderstorm_en in weather_text or t_storm_en in weather_text or + thunderstorm_trans in weather_text or t_storm_trans in weather_text): + warnings.append(self.translate('commands.gwx.warnings.thunderstorms')) + + if (heavy_snow_en in weather_text or snow_showers_en in weather_text or + heavy_snow_trans in weather_text or snow_showers_trans in weather_text): + warnings.append(self.translate('commands.gwx.warnings.heavy_snow')) # Check for high winds wind_match = re.search(r'[NESW]{1,2}(\d+)', weather_text) if wind_match: wind_speed = int(wind_match.group(1)) if wind_speed >= 30: - warnings.append(f"⚠️ High winds ({wind_speed} mph)") + warnings.append(self.translate('commands.gwx.warnings.high_winds', wind_speed=wind_speed)) return " | ".join(warnings) if warnings else None \ No newline at end of file diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index d8c7b19..6dcf83a 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -27,6 +27,39 @@ class BaseCommand(ABC): self.logger = bot.logger self._last_execution_time = 0 + # Load translated keywords after initialization + self._load_translated_keywords() + + def translate(self, key: str, **kwargs) -> str: + """ + Translate a key using the bot's translator + + Args: + key: Dot-separated key path (e.g., 'commands.wx.usage') + **kwargs: Formatting parameters for string.format() + + Returns: + Translated string, or key if translation not found + """ + if hasattr(self.bot, 'translator'): + return self.bot.translator.translate(key, **kwargs) + # Fallback if translator not available + return key + + def translate_get_value(self, key: str) -> Any: + """ + Get a raw value from translations (can be string, list, dict, etc.) + + Args: + key: Dot-separated key path (e.g., 'commands.hacker.sudo_errors') + + Returns: + The value at the key path, or None if not found + """ + if hasattr(self.bot, 'translator'): + return self.bot.translator.get_value(key) + return None + def get_config_value(self, section: str, key: str, fallback=None, value_type: str = 'str'): """ Get config value with backward compatibility for section name changes. @@ -155,6 +188,34 @@ class BaseCommand(ABC): remaining = self.cooldown_seconds - elapsed return max(0, int(remaining)) + def _load_translated_keywords(self): + """Load translated keywords from translation files""" + if not hasattr(self.bot, 'translator'): + self.logger.debug(f"Translator not available for {self.name}, skipping keyword loading") + return + + try: + # Get translated keywords for this command + key = f"keywords.{self.name}" + translated_keywords = self.bot.translator.get_value(key) + + if translated_keywords and isinstance(translated_keywords, list): + # Merge translated keywords with original keywords (avoid duplicates) + original_count = len(self.keywords) + all_keywords = list(self.keywords) # Start with original + for translated_keyword in translated_keywords: + if translated_keyword not in all_keywords: + all_keywords.append(translated_keyword) + self.keywords = all_keywords + added_count = len(self.keywords) - original_count + if added_count > 0: + self.logger.debug(f"Loaded {added_count} translated keyword(s) for {self.name}: {self.keywords}") + else: + self.logger.debug(f"No translated keywords found for {self.name} (key: {key})") + except Exception as e: + # Log the error for debugging + self.logger.debug(f"Could not load translated keywords for {self.name}: {e}") + def matches_keyword(self, message: MeshMessage) -> bool: """Check if this command matches the message content based on keywords""" if not self.keywords: diff --git a/modules/commands/catfact_command.py b/modules/commands/catfact_command.py index ef99382..671c69c 100644 --- a/modules/commands/catfact_command.py +++ b/modules/commands/catfact_command.py @@ -25,8 +25,8 @@ class CatfactCommand(BaseCommand): def __init__(self, bot): super().__init__(bot) - # Collection of cat facts - self.cat_facts = [ + # Collection of cat facts - fallback if translations not available + self.cat_facts_fallback = [ "Cats have a third eyelid called a nictitating membrane that protects and moistens their eyes. 🐱", "A group of cats is called a 'clowder' or a 'glaring'. 🐈", "Cats can rotate their ears 180 degrees independently to pinpoint sounds. 👂", @@ -97,6 +97,13 @@ class CatfactCommand(BaseCommand): "A cat's average body temperature is 101.5°F (38.6°C) - higher than humans. 🌡️" ] + def get_cat_facts(self) -> list: + """Get cat facts from translations or fallback to hardcoded list""" + facts = self.translate_get_value('commands.catfact.facts') + if facts and isinstance(facts, list) and len(facts) > 0: + return facts + return self.cat_facts_fallback + def get_help_text(self) -> str: # Return empty string so it doesn't appear in help return "" @@ -146,8 +153,11 @@ class CatfactCommand(BaseCommand): # Record execution for this user self._record_execution(message.sender_id) + # Get cat facts from translations or fallback + cat_facts = self.get_cat_facts() + # Get a random cat fact - cat_fact = random.choice(self.cat_facts) + cat_fact = random.choice(cat_facts) # Send the cat fact await self.send_response(message, cat_fact) @@ -155,5 +165,5 @@ class CatfactCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in cat fact command: {e}") - await self.send_response(message, "Meow? Something went wrong getting your cat fact! 🐱") + await self.send_response(message, self.translate('commands.catfact.error')) return True diff --git a/modules/commands/channels_command.py b/modules/commands/channels_command.py index b5a914d..8397983 100644 --- a/modules/commands/channels_command.py +++ b/modules/commands/channels_command.py @@ -20,7 +20,7 @@ class ChannelsCommand(BaseCommand): category = "basic" def get_help_text(self) -> str: - return "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels ' for specific categories, 'channels #channel' for specific channel info." + return self.translate('commands.channels.help') def matches_keyword(self, message: MeshMessage) -> bool: """Check if this command matches the message content based on keywords""" @@ -107,9 +107,9 @@ class ChannelsCommand(BaseCommand): if not channels: if sub_command: - await self.send_response(message, f"No channels configured for '{sub_command}'. Use 'channels' for general channels.") + await self.send_response(message, self.translate('commands.channels.no_channels_for_category', category=sub_command)) else: - await self.send_response(message, "No channels configured. Contact admin to add channels.") + await self.send_response(message, self.translate('commands.channels.no_channels_configured')) return True # Build channel list (names only, no descriptions) @@ -127,7 +127,7 @@ class ChannelsCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in channels command: {e}") - await self.send_response(message, f"Error retrieving channels: {e}") + await self.send_response(message, self.translate('commands.channels.error_retrieving_channels', error=str(e))) return False def _load_channels_from_config(self, sub_command: str = None) -> dict: @@ -169,13 +169,13 @@ class ChannelsCommand(BaseCommand): categories = self._get_all_categories() if not categories: - await self.send_response(message, "No channel categories configured.") + await self.send_response(message, self.translate('commands.channels.no_categories_configured')) return # Build category list category_list = [] for category, count in categories.items(): - category_list.append(f"{category} ({count} channels)") + category_list.append(self.translate('commands.channels.category_count', category=category, count=count)) # Split into multiple messages if needed messages = self._split_into_messages(category_list, "Available categories") @@ -185,7 +185,7 @@ class ChannelsCommand(BaseCommand): except Exception as e: self.logger.error(f"Error showing categories: {e}") - await self.send_response(message, f"Error retrieving categories: {e}") + await self.send_response(message, self.translate('commands.channels.error_retrieving_categories', error=str(e))) def _get_all_categories(self) -> dict: """Get all available channel categories and their channel counts""" @@ -252,7 +252,7 @@ class ChannelsCommand(BaseCommand): else: config_key = f"{found_category}.{found_channel[1:]}" # Remove # - description = self.bot.config.get('Channels_List', config_key, fallback='No description available') + description = self.bot.config.get('Channels_List', config_key, fallback=self.translate('commands.channels.no_description_available')) # Strip quotes if present if description.startswith('"') and description.endswith('"'): @@ -261,11 +261,11 @@ class ChannelsCommand(BaseCommand): response = f"{found_channel}: {description}" await self.send_response(message, response) else: - await self.send_response(message, f"Channel {channel_name} not found. Use 'channels list' to see available channels.") + await self.send_response(message, self.translate('commands.channels.channel_not_found', channel=channel_name)) except Exception as e: self.logger.error(f"Error showing specific channel: {e}") - await self.send_response(message, f"Error retrieving channel info: {e}") + await self.send_response(message, self.translate('commands.channels.error_retrieving_channel_info', error=str(e))) def _split_into_messages(self, channel_list: list, sub_command: str = None) -> list: """Split channel list into multiple messages if they exceed 130 characters""" @@ -283,19 +283,20 @@ class ChannelsCommand(BaseCommand): if current_message != expected_header: messages.append(current_message.rstrip(", ")) - current_message = "Channels (cont): " + current_message = self.translate('commands.channels.headers.channels_cont') current_length = len(current_message) else: # If even the first channel is too long, just send it alone messages.append(f"{expected_header}{channel}") - current_message = "Channels (cont): " + current_message = self.translate('commands.channels.headers.channels_cont') current_length = len(current_message) continue # Add channel to current message initial_header = self._get_header_for_subcommand(sub_command) continuation_header = self._get_continuation_header_for_subcommand(sub_command) - if current_message == initial_header or current_message == continuation_header or current_message == "Channels (cont): ": + channels_cont = self.translate('commands.channels.headers.channels_cont') + if current_message == initial_header or current_message == continuation_header or current_message == channels_cont: current_message += channel else: current_message += f", {channel}" @@ -303,35 +304,36 @@ class ChannelsCommand(BaseCommand): # Add the last message if it has content header = self._get_continuation_header_for_subcommand(sub_command) - if current_message != header and current_message != "Channels (cont): ": + channels_cont = self.translate('commands.channels.headers.channels_cont') + if current_message != header and current_message != channels_cont: messages.append(current_message) # If no messages were created, send a default message if not messages: if sub_command: - messages.append(f"No {sub_command} channels configured") + messages.append(self.translate('commands.channels.no_category_channels', category=sub_command)) else: - messages.append("No channels configured") + messages.append(self.translate('commands.channels.no_channels_configured')) return messages def _get_header_for_subcommand(self, sub_command: str = None) -> str: """Get the appropriate header for a sub-command""" if sub_command == "Available categories": - return "Available Categories: " + return self.translate('commands.channels.headers.available_categories') elif sub_command and sub_command != "general": - return f"{sub_command.title()}: " + return self.translate('commands.channels.headers.category', category=sub_command.title()) else: - return "Common channels: " + return self.translate('commands.channels.headers.common_channels') def _get_continuation_header_for_subcommand(self, sub_command: str = None) -> str: """Get the appropriate header for continuation messages""" if sub_command == "Available categories": - return "Available Categories: " + return self.translate('commands.channels.headers.available_categories') elif sub_command and sub_command != "general": - return f"{sub_command.title()} channels: " + return self.translate('commands.channels.headers.category_channels', category=sub_command.title()) else: - return "Common channels: " + return self.translate('commands.channels.headers.common_channels_cont') async def _send_multiple_messages(self, message: MeshMessage, messages: list): """Send multiple messages with delays between them""" diff --git a/modules/commands/dice_command.py b/modules/commands/dice_command.py index c9a6602..728491f 100644 --- a/modules/commands/dice_command.py +++ b/modules/commands/dice_command.py @@ -31,7 +31,7 @@ class DiceCommand(BaseCommand): } def get_help_text(self) -> str: - return "Roll dice. Use 'dice' for d6, 'dice d20' for d20, 'dice 2d6' for 2d6, etc." + return self.translate('commands.dice.help') def matches_keyword(self, message: MeshMessage) -> bool: """Override to handle dice-specific matching""" @@ -114,12 +114,12 @@ class DiceCommand(BaseCommand): """Format dice roll results into a readable string""" if count == 1: # Single die roll - return f"🎲 d{sides}: {results[0]}" + return self.translate('commands.dice.single_die', sides=sides, result=results[0]) else: # Multiple dice total = sum(results) results_str = ", ".join(map(str, results)) - return f"🎲 {count}d{sides}: [{results_str}] = {total}" + return self.translate('commands.dice.multiple_dice', count=count, sides=sides, results=results_str, total=total) async def execute(self, message: MeshMessage) -> bool: """Execute the dice command""" @@ -141,7 +141,7 @@ class DiceCommand(BaseCommand): if sides is None: # Invalid dice specification available_dice = ", ".join(self.DICE_TYPES.keys()) - response = f"Invalid dice type. Available: {available_dice}" + response = self.translate('commands.dice.invalid_dice_type', available=available_dice) return await self.send_response(message, response) # Roll the dice diff --git a/modules/commands/hacker_command.py b/modules/commands/hacker_command.py index c21f7fe..6e25f06 100644 --- a/modules/commands/hacker_command.py +++ b/modules/commands/hacker_command.py @@ -45,9 +45,18 @@ class HackerCommand(BaseCommand): """Get a hilarious error message for the given command""" command_lower = command.lower() + # Try to get errors from translations, fallback to hardcoded if not available + def get_random_error(error_key: str, fallback_list: list) -> str: + """Get a random error from translations or fallback list""" + errors = self.translate_get_value(error_key) + if isinstance(errors, list) and len(errors) > 0: + return random.choice(errors) + # Fallback to hardcoded list if translation not available + return random.choice(fallback_list) + # sudo command errors if command_lower.startswith('sudo'): - sudo_errors = [ + fallback = [ "🚨 ACCESS DENIED: Dr. Evil's mainframe has detected unauthorized privilege escalation attempt!", "💀 ERROR: Sudo permissions revoked by the Dark Overlord. Try again in 1000 years.", "⚡ WARNING: Attempting to access root privileges on the Death Star's computer system. Self-destruct sequence initiated.", @@ -59,11 +68,11 @@ class HackerCommand(BaseCommand): "💻 HACKER DENIED: The supervillain's antivirus has quarantined your privilege escalation.", "🎯 TARGET LOCKED: The evil corporation's security system has marked you as a threat." ] - return random.choice(sudo_errors) + return get_random_error('commands.hacker.sudo_errors', fallback) # ps aux command errors elif command_lower.startswith('ps aux'): - ps_errors = [ + fallback = [ "🔍 SCANNING... ERROR: Process list corrupted by the Borg Collective. Resistance is futile.", "📊 SYSTEM STATUS: All processes have been assimilated by the Cybermen. Exterminate!", "⚙️ PROCESS MONITOR: The Death Star's reactor core is offline. No processes found.", @@ -75,11 +84,11 @@ class HackerCommand(BaseCommand): "🌐 NETWORK ERROR: All processes have been disconnected from the Matrix.", "⚡ POWER SURGE: The supervillain's server farm has fried all running processes." ] - return random.choice(ps_errors) + return get_random_error('commands.hacker.ps_errors', fallback) # grep command errors elif command_lower.startswith('grep'): - grep_errors = [ + fallback = [ "🔍 SEARCH FAILED: The One Ring has corrupted the search index. My precious...", "📝 PATTERN NOT FOUND: The search database has been deleted by the evil AI.", "🎯 MISS: Your search pattern has been shot down by Imperial TIE fighters.", @@ -91,11 +100,11 @@ class HackerCommand(BaseCommand): "🌐 NETWORK TIMEOUT: The search request got lost in cyberspace.", "⚡ SEARCH FAILED: The pattern matching algorithm has been fried by a power surge." ] - return random.choice(grep_errors) + return get_random_error('commands.hacker.grep_errors', fallback) # ls -l and ls -la command errors elif command_lower.startswith('ls -l') or command_lower.startswith('ls -la'): - ls_errors = [ + fallback = [ "📁 DIRECTORY SCAN: The file system has been encrypted by ransomware from the Dark Web.", "🗂️ FILE LISTING: All files have been hidden by the Invisible Man.", "💻 HARD DRIVE CRASHED: The supervillain's storage has been destroyed by a virus.", @@ -107,11 +116,11 @@ class HackerCommand(BaseCommand): "🌐 CLOUD STORAGE DOWN: The files are stuck in the Matrix's cloud.", "⚡ STORAGE FRIED: The hard drive has been zapped by a power surge." ] - return random.choice(ls_errors) + return get_random_error('commands.hacker.ls_errors', fallback) # echo $PATH command errors elif command_lower.startswith('echo $path'): - echo_path_errors = [ + fallback = [ "🛤️ PATH ERROR: The Yellow Brick Road has been destroyed by a tornado.", "🗺️ NAVIGATION FAILED: The GPS coordinates have been scrambled by the Matrix.", "💻 ENVIRONMENT VARIABLE CORRUPTED: The PATH has been hacked by malware.", @@ -123,11 +132,11 @@ class HackerCommand(BaseCommand): "🌐 NETWORK PATH DOWN: The directory paths are stuck in the Matrix's network.", "⚡ PATH FRIED: The system paths have been zapped by a power surge." ] - return random.choice(echo_path_errors) + return get_random_error('commands.hacker.echo_path_errors', fallback) # Generic hacker error for other commands else: - generic_errors = [ + fallback = [ "💻 MAINFRAME ERROR: The supervillain's computer is having a bad day.", "🤖 SYSTEM MALFUNCTION: The evil AI has gone on strike.", "⚡ POWER SURGE: The Death Star's power core is unstable.", @@ -139,7 +148,7 @@ class HackerCommand(BaseCommand): "👻 HAUNTED: The system is possessed by digital ghosts.", "🎮 GAME CRASH: The mainframe has encountered a fatal error and needs to restart." ] - return random.choice(generic_errors) + return get_random_error('commands.hacker.generic_errors', fallback) def matches_keyword(self, message: MeshMessage) -> bool: """Override to check for command matches (exact for some, prefix for others)""" diff --git a/modules/commands/hello_command.py b/modules/commands/hello_command.py index 08a77e2..4749bc7 100644 --- a/modules/commands/hello_command.py +++ b/modules/commands/hello_command.py @@ -21,8 +21,13 @@ class HelloCommand(BaseCommand): def __init__(self, bot): super().__init__(bot) + # Fallback arrays if translations not available + self._init_fallback_arrays() + + def _init_fallback_arrays(self): + """Initialize fallback arrays for when translations are not available""" # Time-neutral greeting openings - self.greeting_openings = [ + self.greeting_openings_fallback = [ "Hello", "Greetings", "Salutations", "Hi", "Hey", "Howdy", "Yo", "Sup", "What's up", "Good day", "Well met", "Hail", "Ahoy", "Bonjour", "Hola", "Ciao", "Namaste", "Aloha", "Shalom", "Konnichiwa", "Guten tag", "G'day", @@ -32,26 +37,26 @@ class HelloCommand(BaseCommand): ] # Time-based greeting openings - self.morning_greetings = [ + self.morning_greetings_fallback = [ "Good morning", "Top o' the morning", "Buenos dias", "Bonjour", "Guten morgen", "Buongiorno", "Bom dia", "Dobro jutro", "Dobroye utro", "Selamat pagi", "Ohayou gozaimasu", "Sabah al-khair", "Boker tov" ] - self.afternoon_greetings = [ + self.afternoon_greetings_fallback = [ "Good afternoon", "Buenas tardes", "Boa tarde", "Dobro dan", "Dobryy den", "Selamat siang", "Konnichiwa", "Ahlan bi-nahar", "Tzoharaim tovim" ] - self.evening_greetings = [ + self.evening_greetings_fallback = [ "Good evening", "Buenas noches", "Boa noite", "Dobro veče", "Dobryy vecher", "Selamat malam", "Konbanwa", "Ahlan bi-layl", "Erev tov" ] # Randomized human descriptors - self.human_descriptors = [ + self.human_descriptors_fallback = [ # Classic robot references "human", "carbon-based lifeform", "organic entity", "biological unit", "flesh creature", "meat-based organism", "carbon unit", "organic being", @@ -80,7 +85,7 @@ class HelloCommand(BaseCommand): ] # Emoji greeting responses - self.emoji_responses = { + self.emoji_responses_fallback = { '🖖': [ "🖖 Live long and prosper!", "🖖 Fascinating... a human has initiated contact.", @@ -166,8 +171,50 @@ class HelloCommand(BaseCommand): ] } + def get_greeting_openings(self) -> list: + """Get greeting openings from translations or fallback""" + openings = self.translate_get_value('commands.hello.greeting_openings') + if openings and isinstance(openings, list) and len(openings) > 0: + return openings + return self.greeting_openings_fallback + + def get_morning_greetings(self) -> list: + """Get morning greetings from translations or fallback""" + greetings = self.translate_get_value('commands.hello.morning_greetings') + if greetings and isinstance(greetings, list) and len(greetings) > 0: + return greetings + return self.morning_greetings_fallback + + def get_afternoon_greetings(self) -> list: + """Get afternoon greetings from translations or fallback""" + greetings = self.translate_get_value('commands.hello.afternoon_greetings') + if greetings and isinstance(greetings, list) and len(greetings) > 0: + return greetings + return self.afternoon_greetings_fallback + + def get_evening_greetings(self) -> list: + """Get evening greetings from translations or fallback""" + greetings = self.translate_get_value('commands.hello.evening_greetings') + if greetings and isinstance(greetings, list) and len(greetings) > 0: + return greetings + return self.evening_greetings_fallback + + def get_human_descriptors(self) -> list: + """Get human descriptors from translations or fallback""" + descriptors = self.translate_get_value('commands.hello.human_descriptors') + if descriptors and isinstance(descriptors, list) and len(descriptors) > 0: + return descriptors + return self.human_descriptors_fallback + + def get_emoji_responses(self) -> dict: + """Get emoji responses from translations or fallback""" + responses = self.translate_get_value('commands.hello.emoji_responses') + if responses and isinstance(responses, dict) and len(responses) > 0: + return responses + return self.emoji_responses_fallback + def get_help_text(self) -> str: - return self.description + return self.translate('commands.hello.help') def matches_custom_syntax(self, message: MeshMessage) -> bool: """Check if message contains only defined emojis""" @@ -184,7 +231,8 @@ class HelloCommand(BaseCommand): else: # Get random robot greeting random_greeting = self.get_random_greeting() - response = f"{random_greeting} I'm {bot_name}." + response_format = self.translate('commands.hello.response_format') + response = f"{random_greeting} {response_format}".format(bot_name=bot_name) return await self.send_response(message, response) @@ -211,18 +259,25 @@ class HelloCommand(BaseCommand): # Get current hour to determine time of day current_hour = current_time.hour + # Get greeting arrays from translations or fallback + greeting_openings = self.get_greeting_openings() + morning_greetings = self.get_morning_greetings() + afternoon_greetings = self.get_afternoon_greetings() + evening_greetings = self.get_evening_greetings() + human_descriptors = self.get_human_descriptors() + # Choose appropriate greeting based on time of day if 5 <= current_hour < 12: # Morning (5 AM - 12 PM) - greeting_pool = self.morning_greetings + self.greeting_openings + greeting_pool = morning_greetings + greeting_openings elif 12 <= current_hour < 17: # Afternoon (12 PM - 5 PM) - greeting_pool = self.afternoon_greetings + self.greeting_openings + greeting_pool = afternoon_greetings + greeting_openings elif 17 <= current_hour < 22: # Evening (5 PM - 10 PM) - greeting_pool = self.evening_greetings + self.greeting_openings + greeting_pool = evening_greetings + greeting_openings else: # Night/Late night (10 PM - 5 AM) - greeting_pool = self.evening_greetings + self.greeting_openings + greeting_pool = evening_greetings + greeting_openings opening = random.choice(greeting_pool) - descriptor = random.choice(self.human_descriptors) + descriptor = random.choice(human_descriptors) # Add some variety in punctuation and formatting punctuation_options = ["!", ".", "!", "!", "!"] # Favor exclamation marks @@ -253,14 +308,18 @@ class HelloCommand(BaseCommand): """Get appropriate response for emoji-only message""" import random + # Get emoji responses from translations or fallback + emoji_responses = self.get_emoji_responses() + response_format = self.translate('commands.hello.response_format') + # Extract the first emoji from the message first_emoji = text.strip().split()[0] if text.strip() else "" # Check if this emoji has special responses - if first_emoji in self.emoji_responses: - response = random.choice(self.emoji_responses[first_emoji]) - return f"{response} I'm {bot_name}." + if first_emoji in emoji_responses: + response = random.choice(emoji_responses[first_emoji]) + return f"{response} {response_format}".format(bot_name=bot_name) else: # Use random greeting generator for general emojis random_greeting = self.get_random_greeting() - return f"{random_greeting} I'm {bot_name}." + return f"{random_greeting} {response_format}".format(bot_name=bot_name) diff --git a/modules/commands/help_command.py b/modules/commands/help_command.py index db9a38d..1339e19 100644 --- a/modules/commands/help_command.py +++ b/modules/commands/help_command.py @@ -18,7 +18,7 @@ class HelpCommand(BaseCommand): category = "basic" def get_help_text(self) -> str: - return self.description + return self.translate('commands.help.description') async def execute(self, message: MeshMessage) -> bool: """Execute the help command""" @@ -53,27 +53,18 @@ class HelpCommand(BaseCommand): # Fallback for commands that don't accept message parameter help_text = command.get_help_text() else: - help_text = "No help available" - return f"**Help for '{command_name}':**\n{help_text}" + help_text = self.translate('commands.help.no_help') + return self.translate('commands.help.specific', command=command_name, help_text=help_text) else: - return f"**Unknown command: '{command_name}'**\n\nAvailable commands:\n" + self.get_available_commands_list() + available = self.get_available_commands_list() + return self.translate('commands.help.unknown', command=command_name, available=available) def get_general_help(self) -> str: """Get general help text""" - help_text = "**MeshCore Bot Help**\n\n" - help_text += "**Available Commands:**\n" - help_text += self.get_available_commands_list() - help_text += "\n**Usage Examples:**\n" - help_text += "• `help @` - Get help for @{string} syntax\n" - help_text += "• `help t` - Get help for t {string} syntax\n" - help_text += "• `help advert` - Get help for advert command\n" - help_text += "• `help test` - Get help for test command\n" - help_text += "• `help ping` - Get help for ping command\n" - help_text += "\n**Custom Syntax:**\n" - help_text += "• `t phrase` - Acknowledgment with phrase (channels & DMs)\n" - help_text += "• `@{string}` - Acknowledgment with string (DMs only)\n" - help_text += "• `advert` - Send flood advert (DMs only, 1-hour cooldown)\n" - + commands_list = self.get_available_commands_list() + help_text = self.translate('commands.help.general', commands_list=commands_list) + help_text += self.translate('commands.help.usage_examples') + help_text += self.translate('commands.help.custom_syntax') return help_text def get_available_commands_list(self) -> str: diff --git a/modules/commands/hfcond_command.py b/modules/commands/hfcond_command.py index 1be76ff..408d8cd 100644 --- a/modules/commands/hfcond_command.py +++ b/modules/commands/hfcond_command.py @@ -22,13 +22,13 @@ class HfcondCommand(BaseCommand): hf_info = hf_band_conditions() # Send response using unified method - response = f"📡 HF Band Conditions:\n{hf_info}" + response = self.translate('commands.hfcond.header', info=hf_info) return await self.send_response(message, response) except Exception as e: - error_msg = f"Error getting HF conditions: {e}" + error_msg = self.translate('commands.hfcond.error', error=str(e)) return await self.send_response(message, error_msg) def get_help_text(self): """Get help text for this command""" - return "Get HF band conditions for ham radio" + return self.translate('commands.hfcond.help') diff --git a/modules/commands/moon_command.py b/modules/commands/moon_command.py index ae61d09..48be7cb 100644 --- a/modules/commands/moon_command.py +++ b/modules/commands/moon_command.py @@ -34,10 +34,40 @@ class MoonCommand(BaseCommand): return True except Exception as e: - error_msg = f"Error getting moon info: {e}" + error_msg = self.translate('commands.moon.error', error=str(e)) await self.send_response(message, error_msg) return False + def _translate_phase_name(self, phase_name: str) -> str: + """Translate English phase name to localized version""" + # Map English phase names (with or without emoji) to translation keys + phase_mapping = { + 'New Moon': 'new_moon', + 'Waxing Crescent': 'waxing_crescent', + 'First Quarter': 'first_quarter', + 'Waxing Gibbous': 'waxing_gibbous', + 'Full Moon': 'full_moon', + 'Waning Gibbous': 'waning_gibbous', + 'Last Quarter': 'last_quarter', + 'Waning Crescent': 'waning_crescent' + } + + # Remove emoji if present (they're in the translation) + phase_clean = phase_name + for emoji in ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']: + phase_clean = phase_clean.replace(emoji, '').strip() + + # Find matching translation key + for english_name, translation_key in phase_mapping.items(): + if english_name in phase_clean: + translated = self.translate(f'commands.moon.phases.{translation_key}') + # If translation found (not just the key), return it + if translated != f'commands.moon.phases.{translation_key}': + return translated + + # Fallback: return original if no translation found + return phase_name + def _format_moon_response(self, moon_info: str) -> str: """Format moon information to be more compact and readable""" try: @@ -57,17 +87,17 @@ class MoonCommand(BaseCommand): set_info = moon_data['Set'] # e.g., "Fri 05 03:43AM" # Extract phase and illumination - phase_info = moon_data.get('Phase', 'Unknown') + phase_info = moon_data.get('Phase', self.translate('commands.moon.unknown_phase')) if '@:' in phase_info: phase, illum = phase_info.split('@:') phase = phase.strip() illum = illum.strip() + # Translate the phase name + phase = self._translate_phase_name(phase) else: phase = phase_info - illum = 'N/A' - - # Create compact response with essential details - response = f"🌙 {phase} {illum}\nRise:{rise_info} Set:{set_info}" + phase = self._translate_phase_name(phase) + illum = self.translate('commands.moon.unknown_illum') # Add next full and new moon dates (compact format) if 'FullMoon' in moon_data and 'NewMoon' in moon_data: @@ -82,16 +112,23 @@ class MoonCommand(BaseCommand): # Format: "Sep 07 11:08AM" and "Sep 21 12:54PM" full_compact = f"{full_parts[1]} {full_parts[2]} {full_parts[3]}" new_compact = f"{new_parts[1]} {new_parts[2]} {new_parts[3]}" - response += f"\nFull:{full_compact} New:{new_compact}" + # Create compact response with full/new moon dates + return self.translate('commands.moon.format_with_dates', + phase=phase, illum=illum, + rise=rise_info, set=set_info, + full=full_compact, new=new_compact) - return response + # Create compact response without full/new moon dates + return self.translate('commands.moon.format', + phase=phase, illum=illum, + rise=rise_info, set=set_info) else: # Fallback to original format if parsing fails - return f"🌙 {moon_info}" + return self.translate('commands.moon.fallback', info=moon_info) except Exception as e: # Fallback to original format if formatting fails - return f"🌙 {moon_info}" + return self.translate('commands.moon.fallback', info=moon_info) def get_help_text(self): """Get help text for this command""" diff --git a/modules/commands/multitest_command.py b/modules/commands/multitest_command.py new file mode 100644 index 0000000..1ab554e --- /dev/null +++ b/modules/commands/multitest_command.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +""" +Multitest command for the MeshCore Bot +Listens for a period of time and collects all unique paths from incoming messages +""" + +import asyncio +import time +from typing import Set, Optional +from .base_command import BaseCommand +from ..models import MeshMessage +from ..utils import calculate_packet_hash + + +class MultitestCommand(BaseCommand): + """Handles the multitest command - listens for multiple path variations""" + + # Plugin metadata + name = "multitest" + keywords = ['multitest', 'mt'] + description = "Listens for 6 seconds and collects all unique paths from incoming messages" + category = "meshcore_info" + + def __init__(self, bot): + super().__init__(bot) + self.listening = False + self.collected_paths: Set[str] = set() + self.listening_start_time = 0 + self.listening_duration = 6.0 # 6 seconds listening window + self.target_packet_hash: Optional[str] = None # Hash of the message we're tracking + self.triggering_timestamp: float = 0.0 # Timestamp of the triggering message + + def get_help_text(self) -> str: + return self.translate('commands.multitest.help', fallback="Listens for 6 seconds and collects all unique paths from incoming messages") + + def matches_keyword(self, message: MeshMessage) -> bool: + """Check if message matches multitest keyword""" + content = message.content.strip() + + # Handle exclamation prefix + if content.startswith('!'): + content = content[1:].strip() + + content_lower = content.lower() + + # Check for exact match or keyword followed by space + for keyword in self.keywords: + if content_lower == keyword or content_lower.startswith(keyword + ' '): + return True + + return False + + def extract_path_from_rf_data(self, rf_data: dict) -> Optional[str]: + """Extract path in prefix string format from RF data routing_info""" + try: + routing_info = rf_data.get('routing_info') + if not routing_info: + return None + + path_nodes = routing_info.get('path_nodes', []) + if not path_nodes: + # Try to extract from path_hex if path_nodes not available + path_hex = routing_info.get('path_hex', '') + if path_hex: + # Convert hex string to node list (every 2 characters = 1 node) + path_nodes = [path_hex[i:i+2] for i in range(0, len(path_hex), 2)] + + if path_nodes: + # Validate and format path nodes + valid_parts = [] + for node in path_nodes: + # Convert to string if needed + node_str = str(node).lower().strip() + # Check if it's a 2-character hex value + if len(node_str) == 2 and all(c in '0123456789abcdef' for c in node_str): + valid_parts.append(node_str) + + if valid_parts: + return ','.join(valid_parts) + + return None + except Exception as e: + self.logger.debug(f"Error extracting path from RF data: {e}") + return None + + def extract_path_from_message(self, message: MeshMessage) -> Optional[str]: + """Extract path in prefix string format from a message""" + if not message.path: + return None + + # Check if it's a direct connection + if "Direct" in message.path or "0 hops" in message.path: + return None + + # Try to extract path nodes from the path string + # Path strings are typically in format: "node1,node2,node3 via ROUTE_TYPE_*" + # or just "node1,node2,node3" + path_string = message.path + + # Remove route type suffix if present + if " via ROUTE_TYPE_" in path_string: + path_string = path_string.split(" via ROUTE_TYPE_")[0] + + # Check if it looks like a comma-separated path + if ',' in path_string: + # Clean up any extra info (like hop counts in parentheses) + # Example: "01,7e,55,86 (4 hops)" -> "01,7e,55,86" + if '(' in path_string: + path_string = path_string.split('(')[0].strip() + + # Validate that all parts are 2-character hex values + parts = path_string.split(',') + valid_parts = [] + for part in parts: + part = part.strip() + # Check if it's a 2-character hex value + if len(part) == 2 and all(c in '0123456789abcdefABCDEF' for c in part): + valid_parts.append(part.lower()) + + if valid_parts: + return ','.join(valid_parts) + + return None + + def get_rf_data_for_message(self, message: MeshMessage) -> Optional[dict]: + """Get RF data for a message by looking it up in recent RF data""" + try: + # Try multiple correlation strategies + # Strategy 1: Use sender_pubkey to find recent RF data + if message.sender_pubkey: + # Try full pubkey first + recent_rf_data = self.bot.message_handler.find_recent_rf_data(message.sender_pubkey) + if recent_rf_data: + return recent_rf_data + + # Try pubkey prefix (first 16 chars) + if len(message.sender_pubkey) >= 16: + pubkey_prefix = message.sender_pubkey[:16] + recent_rf_data = self.bot.message_handler.find_recent_rf_data(pubkey_prefix) + if recent_rf_data: + return recent_rf_data + + # Strategy 2: Look through recent RF data for matching pubkey + if message.sender_pubkey and self.bot.message_handler.recent_rf_data: + # Search recent RF data for matching pubkey + for rf_data in reversed(self.bot.message_handler.recent_rf_data): + rf_pubkey = rf_data.get('pubkey_prefix', '') + if rf_pubkey and message.sender_pubkey.startswith(rf_pubkey): + return rf_data + + # Strategy 3: Use most recent RF data as fallback + # This is less reliable but might work if timing is very close + if self.bot.message_handler.recent_rf_data: + # Get the most recent RF data entry within a short time window + current_time = time.time() + recent_entries = [ + rf for rf in self.bot.message_handler.recent_rf_data + if current_time - rf.get('timestamp', 0) < 5.0 # Within last 5 seconds + ] + if recent_entries: + most_recent = max(recent_entries, key=lambda x: x.get('timestamp', 0)) + return most_recent + + return None + except Exception as e: + self.logger.debug(f"Error getting RF data for message: {e}") + return None + + def on_message_received(self, message: MeshMessage): + """Callback method called by message handler when a message is received during listening""" + if not self.listening or not self.target_packet_hash: + return + + # Check if we're still in the listening window + elapsed = time.time() - self.listening_start_time + if elapsed >= self.listening_duration: + return + + # Get RF data for this message (contains pre-calculated packet hash) + rf_data = self.get_rf_data_for_message(message) + if not rf_data: + # Can't get RF data, skip this message + self.logger.debug(f"Skipping message - no RF data found (sender: {message.sender_id})") + return + + # Use pre-calculated packet hash if available, otherwise calculate it + message_hash = rf_data.get('packet_hash') + if not message_hash and rf_data.get('raw_hex'): + # Fallback: calculate hash if not stored (for older RF data) + try: + payload_type = None + routing_info = rf_data.get('routing_info', {}) + if routing_info: + # Try to get payload type from routing_info if available + payload_type = routing_info.get('payload_type') + message_hash = calculate_packet_hash(rf_data['raw_hex'], payload_type) + except Exception as e: + self.logger.debug(f"Error calculating packet hash: {e}") + message_hash = None + + if not message_hash: + # Can't determine hash, skip this message + self.logger.debug(f"Skipping message - could not determine packet hash (sender: {message.sender_id})") + return + + # CRITICAL: Only collect paths if this message has the same hash as the target + # This ensures we only track variations of the same original message + if message_hash == self.target_packet_hash: + # Try to extract path from RF data first (more reliable) + path = self.extract_path_from_rf_data(rf_data) + + # Fallback to message path if RF data extraction failed + if not path: + path = self.extract_path_from_message(message) + + if path: + self.collected_paths.add(path) + self.logger.info(f"✓ Collected path: {path} (hash: {message_hash[:8]}...)") + else: + # Log when we have a matching hash but can't extract path + routing_info = rf_data.get('routing_info', {}) + path_length = routing_info.get('path_length', 0) + if path_length == 0: + self.logger.debug(f"Matched hash {message_hash[:8]}... but path is direct (0 hops)") + else: + self.logger.debug(f"Matched hash {message_hash[:8]}... but couldn't extract path from routing_info: {routing_info}") + else: + # Log hash mismatches for debugging (but limit to avoid spam) + self.logger.debug(f"✗ Hash mismatch - target: {self.target_packet_hash[:8]}..., received: {message_hash[:8]}... (sender: {message.sender_id})") + + def _scan_recent_rf_data(self): + """Scan recent RF data for packets with matching hash (for messages that haven't been processed yet)""" + if not self.target_packet_hash: + return + + try: + current_time = time.time() + matching_count = 0 + mismatching_count = 0 + + # Look at RF data from the last few seconds (before listening started, in case packets arrived just before) + for rf_data in self.bot.message_handler.recent_rf_data: + # Check if this RF data is recent enough + rf_timestamp = rf_data.get('timestamp', 0) + time_diff = current_time - rf_timestamp + + # Only include RF data from the triggering message timestamp onwards + # This prevents collecting packets from earlier messages that happen to have the same hash + rf_timestamp = rf_data.get('timestamp', 0) + if rf_timestamp >= self.triggering_timestamp and time_diff <= self.listening_duration: + packet_hash = rf_data.get('packet_hash') + + # CRITICAL: Only process if hash matches exactly and is not None/empty + if packet_hash and packet_hash == self.target_packet_hash: + matching_count += 1 + # Extract path from this RF data + path = self.extract_path_from_rf_data(rf_data) + if path: + self.collected_paths.add(path) + self.logger.info(f"✓ Collected path from RF scan: {path} (hash: {packet_hash[:8]}..., time: {time_diff:.2f}s)") + else: + self.logger.debug(f"Matched hash {packet_hash[:8]}... in RF scan but couldn't extract path") + elif packet_hash: + mismatching_count += 1 + # Only log first few mismatches to avoid spam + if mismatching_count <= 3: + self.logger.debug(f"✗ RF scan hash mismatch - target: {self.target_packet_hash[:8]}..., found: {packet_hash[:8]}... (time: {time_diff:.2f}s)") + + if matching_count > 0 or mismatching_count > 0: + self.logger.debug(f"RF scan complete: {matching_count} matching, {mismatching_count} mismatching packets") + except Exception as e: + self.logger.debug(f"Error scanning recent RF data: {e}") + + async def execute(self, message: MeshMessage) -> bool: + """Execute the multitest command""" + self.logger.info("Multitest command executed - starting listening window") + + # Get RF data for the triggering message (contains pre-calculated packet hash) + rf_data = self.get_rf_data_for_message(message) + if not rf_data: + response = "Error: Could not find packet data for this message. Please try again." + await self.send_response(message, response) + return True + + # Use pre-calculated packet hash if available, otherwise calculate it + packet_hash = rf_data.get('packet_hash') + if not packet_hash and rf_data.get('raw_hex'): + # Fallback: calculate hash if not stored (for older RF data) + # IMPORTANT: Must use same payload_type that was used during ingestion + payload_type = None + routing_info = rf_data.get('routing_info', {}) + if routing_info: + payload_type = routing_info.get('payload_type') + packet_hash = calculate_packet_hash(rf_data['raw_hex'], payload_type) + + if not packet_hash: + response = "Error: Could not calculate packet hash for this message. Please try again." + await self.send_response(message, response) + return True + + # Store the packet hash to track + self.target_packet_hash = packet_hash + + # Store the timestamp of the triggering message to avoid collecting older packets + triggering_rf_timestamp = rf_data.get('timestamp', time.time()) + self.triggering_timestamp = triggering_rf_timestamp + + self.logger.info(f"Tracking packet hash: {self.target_packet_hash[:16]}... (full: {self.target_packet_hash})") + self.logger.debug(f"Triggering message timestamp: {triggering_rf_timestamp}") + + # Also extract path from the triggering message itself + initial_path = self.extract_path_from_message(message) + # Also try to extract from RF data (more reliable) + if not initial_path and rf_data: + initial_path = self.extract_path_from_rf_data(rf_data) + + if initial_path: + self.logger.debug(f"Initial path from triggering message: {initial_path}") + + # Register this command instance as the active listener + # Store reference in message handler so it can call on_message_received + self.bot.message_handler.multitest_listener = self + + # Start listening + self.listening = True + self.collected_paths = set() + if initial_path: + self.collected_paths.add(initial_path) # Include the initial path + self.listening_start_time = time.time() + + # Also scan recent RF data for matching hashes (in case messages haven't been processed yet) + # But only include packets that arrived at or after the triggering message + self._scan_recent_rf_data() + + try: + # Wait for the listening duration + await asyncio.sleep(self.listening_duration) + finally: + # Stop listening and unregister (but keep target_packet_hash for error messages) + self.listening = False + self.bot.message_handler.multitest_listener = None + + # Do a final scan of RF data in case any matching packets arrived + self._scan_recent_rf_data() + + # Store hash for error message before clearing it + tracking_hash = self.target_packet_hash + + # Format the collected paths + if self.collected_paths: + # Sort paths for consistent output + sorted_paths = sorted(self.collected_paths) + response = f"Found {len(sorted_paths)} unique path(s):\n" + "\n".join(sorted_paths) + else: + # Provide more helpful error message with diagnostic info + matching_packets = 0 + if self.bot.message_handler.recent_rf_data and tracking_hash: + for rf_data in self.bot.message_handler.recent_rf_data: + if rf_data.get('packet_hash') == tracking_hash: + matching_packets += 1 + + if tracking_hash is None: + response = ("Error: Could not determine packet hash for tracking. " + "The triggering message may not have valid packet data.") + elif matching_packets > 0: + response = (f"No paths extracted from {matching_packets} matching packet(s) " + f"(hash: {tracking_hash}). " + f"Packets may be direct (0 hops) or path extraction failed.") + else: + response = (f"No matching packets found during {self.listening_duration}s window. " + f"Tracking hash: {tracking_hash}. ") + + # Clear the hash after we're done with it + self.target_packet_hash = None + + # Wait for bot TX rate limiter cooldown to expire before sending + # This ensures we respond even if another command put the bot on cooldown + await self.bot.bot_tx_rate_limiter.wait_for_tx() + + # Also wait for user rate limiter if needed + if not self.bot.rate_limiter.can_send(): + wait_time = self.bot.rate_limiter.time_until_next() + if wait_time > 0: + self.logger.info(f"Waiting {wait_time:.1f} seconds for rate limiter") + await asyncio.sleep(wait_time + 0.1) # Small buffer + + # Send the response + await self.send_response(message, response) + + return True + diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index 1ab8d54..ad8ed9e 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -7,10 +7,10 @@ Decodes hex path data to show which repeaters were involved in message routing import re import time import asyncio -import math from typing import List, Optional, Dict, Any, Tuple from .base_command import BaseCommand from ..models import MeshMessage +from ..utils import calculate_distance class PathCommand(BaseCommand): @@ -139,7 +139,7 @@ class PathCommand(BaseCommand): hex_matches = re.findall(hex_pattern, path_input) if not hex_matches: - return "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01" + return self.translate('commands.path.no_valid_hex') # Convert to uppercase for consistency # hex_matches preserves the order from the original path @@ -155,7 +155,7 @@ class PathCommand(BaseCommand): except Exception as e: self.logger.error(f"Error decoding path: {e}") - return f"❌ Error decoding path: {e}" + return self.translate('commands.path.error_decoding', error=str(e)) async def _lookup_repeater_names(self, node_ids: List[str]) -> Dict[str, Dict[str, Any]]: """Look up repeater names for given node IDs""" @@ -347,7 +347,7 @@ class PathCommand(BaseCommand): if public_key.startswith(node_id): # Check if this is a repeater if hasattr(self.bot, 'repeater_manager') and self.bot.repeater_manager._is_repeater_device(contact_data): - name = contact_data.get('adv_name', contact_data.get('name', 'Unknown')) + name = contact_data.get('adv_name', contact_data.get('name', self.translate('commands.path.unknown_name'))) device_matches.append({ 'name': name, 'public_key': public_key, @@ -414,23 +414,6 @@ class PathCommand(BaseCommand): self.logger.warning(f"Could not get API cache data: {e}") return None - def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float: - """Calculate haversine distance between two points in kilometers""" - # Convert latitude and longitude from degrees to radians - lat1_rad = math.radians(lat1) - lon1_rad = math.radians(lon1) - lat2_rad = math.radians(lat2) - lon2_rad = math.radians(lon2) - - # Haversine formula - dlat = lat2_rad - lat1_rad - dlon = lon2_rad - lon1_rad - a = math.sin(dlat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon/2)**2 - c = 2 * math.asin(math.sqrt(a)) - - # Earth's radius in kilometers - earth_radius = 6371.0 - return earth_radius * c def _select_repeater_by_proximity(self, repeaters: List[Dict[str, Any]], node_id: str = None, path_context: List[str] = None) -> Tuple[Optional[Dict[str, Any]], float]: """ @@ -496,7 +479,7 @@ class PathCommand(BaseCommand): # If only one repeater, check if it's within range if len(scored_repeaters) == 1: repeater, recency_score = scored_repeaters[0] - distance = self._calculate_distance( + distance = calculate_distance( self.bot_latitude, self.bot_longitude, repeater['latitude'], repeater['longitude'] ) @@ -511,7 +494,7 @@ class PathCommand(BaseCommand): # Calculate combined proximity + recency scores combined_scores = [] for repeater, recency_score in scored_repeaters: - distance = self._calculate_distance( + distance = calculate_distance( self.bot_latitude, self.bot_longitude, repeater['latitude'], repeater['longitude'] ) @@ -869,13 +852,13 @@ class PathCommand(BaseCommand): for repeater, recency_score in scored_repeaters: # Calculate distance to previous node - prev_distance = self._calculate_distance( + prev_distance = calculate_distance( prev_location[0], prev_location[1], repeater['latitude'], repeater['longitude'] ) # Calculate distance to next node - next_distance = self._calculate_distance( + next_distance = calculate_distance( next_location[0], next_location[1], repeater['latitude'], repeater['longitude'] ) @@ -896,11 +879,11 @@ class PathCommand(BaseCommand): # Apply maximum range threshold if self.max_proximity_range > 0: # Check if any distance is beyond range - prev_dist = self._calculate_distance( + prev_dist = calculate_distance( prev_location[0], prev_location[1], best_repeater['latitude'], best_repeater['longitude'] ) - next_dist = self._calculate_distance( + next_dist = calculate_distance( next_location[0], next_location[1], best_repeater['latitude'], best_repeater['longitude'] ) @@ -929,7 +912,7 @@ class PathCommand(BaseCommand): best_combined_score = 0.0 for repeater, recency_score in scored_repeaters: - distance = self._calculate_distance( + distance = calculate_distance( reference_location[0], reference_location[1], repeater['latitude'], repeater['longitude'] ) @@ -972,15 +955,16 @@ class PathCommand(BaseCommand): if info.get('collision', False): # Multiple repeaters with same prefix matches = info.get('matches', 0) - line = f"{node_id}: {matches} repeaters" + line = self.translate('commands.path.node_collision', node_id=node_id, matches=matches) elif info.get('geographic_guess', False): # Geographic proximity selection name = info['name'] confidence = info.get('confidence', 0.0) # Truncate name if too long + truncation = self.translate('commands.path.truncation') if len(name) > 20: - name = name[:17] + "..." + name = name[:17] + truncation # Add confidence indicator if confidence >= 0.9: @@ -990,23 +974,25 @@ class PathCommand(BaseCommand): else: confidence_indicator = self.low_confidence_symbol - line = f"{node_id}: {name} {confidence_indicator}" + line = self.translate('commands.path.node_geographic', node_id=node_id, name=name, confidence=confidence_indicator) else: # Single repeater found name = info['name'] # Truncate name if too long + truncation = self.translate('commands.path.truncation') if len(name) > 27: - name = name[:24] + "..." + name = name[:24] + truncation - line = f"{node_id}: {name}" + line = self.translate('commands.path.node_format', node_id=node_id, name=name) else: # Unknown repeater - line = f"{node_id}: Unknown" + line = self.translate('commands.path.node_unknown', node_id=node_id) # Ensure line fits within 130 character limit if len(line) > 130: - line = line[:127] + "..." + truncation = self.translate('commands.path.truncation') + line = line[:127] + truncation lines.append(line) @@ -1037,14 +1023,14 @@ class PathCommand(BaseCommand): if current_message: # Add ellipsis on new line to end of continued message (if not the last message) if i < len(lines): - current_message += "\n..." + current_message += self.translate('commands.path.continuation_end') await self.send_response(message, current_message.rstrip()) await asyncio.sleep(3.0) # Delay between messages (same as other commands) message_count += 1 # Start new message with ellipsis on new line at beginning (if not first message) if message_count > 0: - current_message = f"...\n{line}" + current_message = self.translate('commands.path.continuation_start', line=line) else: current_message = line else: @@ -1068,7 +1054,7 @@ class PathCommand(BaseCommand): # Check if it's a direct connection if "Direct" in path_string or "0 hops" in path_string: - return "📡 Direct connection (0 hops)" + return self.translate('commands.path.direct_connection') # Try to extract path nodes from the path string # Path strings are typically in format: "node1,node2,node3 via ROUTE_TYPE_*" @@ -1084,17 +1070,17 @@ class PathCommand(BaseCommand): return await self._decode_path(path_input) else: # Single node or unknown format - return f"📡 Path: {path_string}" + return self.translate('commands.path.path_prefix', path_string=path_string) else: - return "❌ No path information available in current message" + return self.translate('commands.path.no_path') except Exception as e: self.logger.error(f"Error extracting path from current message: {e}") - return f"❌ Error extracting path from current message: {e}" + return self.translate('commands.path.error_extracting', error=str(e)) def get_help(self) -> str: """Get help text for the path command""" - return """Path: !path [hex] - Decode path to show repeaters. Use !path alone for recent message path, or !path [7e,01] for specific path.""" + return self.translate('commands.path.help') def get_help_text(self) -> str: """Get help text for the path command (used by help system)""" diff --git a/modules/commands/ping_command.py b/modules/commands/ping_command.py index 7763780..7d23728 100644 --- a/modules/commands/ping_command.py +++ b/modules/commands/ping_command.py @@ -18,7 +18,7 @@ class PingCommand(BaseCommand): category = "basic" def get_help_text(self) -> str: - return self.description + return self.translate('commands.ping.description') def get_response_format(self) -> str: """Get the response format from config""" diff --git a/modules/commands/prefix_command.py b/modules/commands/prefix_command.py index 3bcd4d6..fd414d7 100644 --- a/modules/commands/prefix_command.py +++ b/modules/commands/prefix_command.py @@ -9,11 +9,10 @@ import aiohttp import time import json import random -import math from typing import Dict, List, Optional, Any, Tuple from .base_command import BaseCommand from ..models import MeshMessage -from ..utils import abbreviate_location, format_location_for_display +from ..utils import abbreviate_location, format_location_for_display, calculate_distance class PrefixCommand(BaseCommand): @@ -59,12 +58,10 @@ class PrefixCommand(BaseCommand): ) def get_help_text(self) -> str: + location_note = self.translate('commands.prefix.location_note') if self.show_repeater_locations else "" if not self.api_url or self.api_url.strip() == "": - location_note = " (with city names)" if self.show_repeater_locations else "" - return f"Look up repeaters by two-character prefix using local database{location_note}. Usage: 'prefix 1A' (shows recent), 'prefix 1A all' (shows all), 'prefix free' (list available prefixes). Note: API disabled - using local data only." - - location_note = " (with city names)" if self.show_repeater_locations else "" - return f"Look up repeaters by two-character prefix{location_note}. Usage: 'prefix 1A' (shows recent), 'prefix 1A all' (shows all), 'prefix free' (list available prefixes), or 'prefix refresh'." + return self.translate('commands.prefix.help_no_api', location_note=location_note) + return self.translate('commands.prefix.help_api', location_note=location_note) def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with 'prefix' keyword""" @@ -97,17 +94,17 @@ class PrefixCommand(BaseCommand): # Handle refresh command if command == "REFRESH": if not self.api_url or self.api_url.strip() == "": - response = "❌ Refresh not available - no API URL configured. Using local database only." + response = self.translate('commands.prefix.refresh_not_available') return await self.send_response(message, response) await self.refresh_cache() - response = "🔄 Repeater prefix cache refreshed!" + response = self.translate('commands.prefix.cache_refreshed') return await self.send_response(message, response) # Handle free/available command if command == "FREE" or command == "AVAILABLE": free_prefixes, total_free, has_data = await self.get_free_prefixes() if not has_data: - response = "❌ Unable to determine free prefixes. Try 'prefix refresh' first." + response = self.translate('commands.prefix.unable_determine_free') else: response = self.format_free_prefixes_response(free_prefixes, total_free) return await self.send_response(message, response) @@ -119,14 +116,14 @@ class PrefixCommand(BaseCommand): # Validate prefix format if len(command) != 2 or not command.isalnum(): - response = "❌ Invalid prefix format. Use two characters (e.g., prefix 1A)" + response = self.translate('commands.prefix.invalid_format') return await self.send_response(message, response) # Get prefix data prefix_data = await self.get_prefix_data(command, include_all=include_all) if prefix_data is None: - response = f"❌ No repeaters found with prefix '{command}'" + response = self.translate('commands.prefix.no_repeaters_found', prefix=command) return await self.send_response(message, response) # Add include_all flag to data for formatting @@ -351,7 +348,7 @@ class PrefixCommand(BaseCommand): if (row['latitude'] is not None and row['longitude'] is not None and not (row['latitude'] == 0.0 and row['longitude'] == 0.0)): - distance = self._calculate_distance( + distance = calculate_distance( self.bot_latitude, self.bot_longitude, row['latitude'], row['longitude'] ) @@ -365,9 +362,9 @@ class PrefixCommand(BaseCommand): # Add device type indicator for clarity if device_type == 2: - name += " (Repeater)" + name += self.translate('commands.prefix.device_repeater') elif device_type == 3: - name += " (Room Server)" + name += self.translate('commands.prefix.device_roomserver') # Add location information if enabled and available if self.show_repeater_locations: @@ -432,23 +429,6 @@ class PrefixCommand(BaseCommand): self.logger.error(f"Error querying database for prefix '{prefix}': {e}") return None - def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float: - """Calculate haversine distance between two points in kilometers""" - # Convert latitude and longitude from degrees to radians - lat1_rad = math.radians(lat1) - lon1_rad = math.radians(lon1) - lat2_rad = math.radians(lat2) - lon2_rad = math.radians(lon2) - - # Haversine formula - dlat = lat2_rad - lat1_rad - dlon = lon2_rad - lon1_rad - a = math.sin(dlat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon/2)**2 - c = 2 * math.asin(math.sqrt(a)) - - # Earth's radius in kilometers - earth_radius = 6371.0 - return earth_radius * c async def get_free_prefixes(self) -> Tuple[List[str], int, bool]: """Get list of available (unused) prefixes and total count @@ -507,7 +487,7 @@ class PrefixCommand(BaseCommand): if (row.get('latitude') is not None and row.get('longitude') is not None and not (row.get('latitude') == 0.0 and row.get('longitude') == 0.0)): - distance = self._calculate_distance( + distance = calculate_distance( self.bot_latitude, self.bot_longitude, row['latitude'], row['longitude'] ) @@ -558,9 +538,9 @@ class PrefixCommand(BaseCommand): def format_free_prefixes_response(self, free_prefixes: List[str], total_free: int) -> str: """Format the free prefixes response""" if not free_prefixes: - return "❌ No free prefixes found (all 254 valid prefixes are in use)" + return self.translate('commands.prefix.no_free_prefixes') - response = f"Available Prefixes ({len(free_prefixes)} of {total_free} free):\n" + response = self.translate('commands.prefix.available_prefixes', shown=len(free_prefixes), total=total_free) + "\n" # Format as a grid for better readability for i, prefix in enumerate(free_prefixes, 1): @@ -574,7 +554,7 @@ class PrefixCommand(BaseCommand): if len(free_prefixes) % 5 != 0: response += "\n" - response += f"\n💡 Generate a custom key: https://gessaman.com/mc-keygen" + response += "\n" + self.translate('commands.prefix.generate_key') return response @@ -588,20 +568,23 @@ class PrefixCommand(BaseCommand): # Get bot name for database responses bot_name = self.bot.config.get('Bot', 'bot_name', fallback='Bot') + # Handle pluralization + plural = 's' if node_count != 1 else '' + if source == 'database': # Database response format - keep brief for character limit if include_all: - response = f"Prefix {prefix}: {node_count} repeater{'s' if node_count != 1 else ''}\n" + response = self.translate('commands.prefix.prefix_db_all', prefix=prefix, count=node_count, plural=plural) + "\n" else: # Show time period for default behavior - use abbreviated form days_str = f"{self.prefix_heard_days}d" if self.prefix_heard_days != 7 else "7d" - response = f"Prefix {prefix}: {node_count} repeater{'s' if node_count != 1 else ''} ({days_str})\n" + response = self.translate('commands.prefix.prefix_db_recent', prefix=prefix, count=node_count, plural=plural, days=days_str) + "\n" else: # API response format - response = f"📡 Prefix {prefix} ({node_count} repeater{'s' if node_count != 1 else ''}):\n" + response = self.translate('commands.prefix.prefix_api', prefix=prefix, count=node_count, plural=plural) + "\n" for i, name in enumerate(node_names, 1): - response += f"{i}. {name}\n" + response += self.translate('commands.prefix.item_format', index=i, name=name) + "\n" # Add source info (unless hidden by config) if not self.hide_source: @@ -614,10 +597,10 @@ class PrefixCommand(BaseCommand): from urllib.parse import urlparse parsed_url = urlparse(self.api_url) domain = parsed_url.netloc - response += f"\nSource: {domain}" + response += "\n" + self.translate('commands.prefix.source_domain', domain=domain) except Exception: # Fallback if URL parsing fails - response += f"\nSource: API" + response += "\n" + self.translate('commands.prefix.source_api') else: # Remove trailing newline when source is hidden response = response.rstrip('\n') diff --git a/modules/commands/roll_command.py b/modules/commands/roll_command.py index 3744393..1397929 100644 --- a/modules/commands/roll_command.py +++ b/modules/commands/roll_command.py @@ -20,7 +20,7 @@ class RollCommand(BaseCommand): category = "games" def get_help_text(self) -> str: - return "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, 'roll 50' for 1-50, etc." + return self.translate('commands.roll.help') def matches_keyword(self, message: MeshMessage) -> bool: """Override to handle roll-specific matching""" @@ -70,7 +70,7 @@ class RollCommand(BaseCommand): def format_roll_result(self, max_num: int, result: int) -> str: """Format roll result into a readable string""" - return f"🎲 Roll 1-{max_num}: {result}" + return self.translate('commands.roll.result', max=max_num, result=result) async def execute(self, message: MeshMessage) -> bool: """Execute the roll command""" @@ -90,7 +90,7 @@ class RollCommand(BaseCommand): if max_num is None: # Invalid roll specification - response = "Invalid number. Use 'roll' for 1-100, or 'roll X' for 1-X (max 10000)." + response = self.translate('commands.roll.invalid_number') return await self.send_response(message, response) # Roll the number diff --git a/modules/commands/satpass_command.py b/modules/commands/satpass_command.py index 5ec18cc..219e0a5 100644 --- a/modules/commands/satpass_command.py +++ b/modules/commands/satpass_command.py @@ -44,7 +44,7 @@ class SatpassCommand(BaseCommand): # Extract satellite identifier from command parts = content.split() if len(parts) < 2: - error_msg = "Please provide a satellite NORAD number or shortcut. Example: satpass iss" + error_msg = self.translate('commands.satpass.no_satellite') await self.send_response(message, error_msg) return True @@ -68,18 +68,18 @@ class SatpassCommand(BaseCommand): pass_info = get_next_satellite_pass(satellite, use_visual=use_visual) # Send response - response = f"🛰️ Satellite Pass:\n{pass_info}" + response = self.translate('commands.satpass.header', pass_info=pass_info) await self.send_response(message, response) return True except Exception as e: - error_msg = f"Error getting satellite pass info: {e}" + error_msg = self.translate('commands.satpass.error', error=str(e)) await self.send_response(message, error_msg) return False def _get_help_text(self): """Get detailed help text with shortcuts""" - shortcuts_text = "🛰️ Satellite Pass Info\n\nUsage: satpass \n\nShortcuts:\n" + shortcuts_text = self.translate('commands.satpass.help_header') # Group shortcuts by category for better organization weather_sats = ['noaa15', 'noaa18', 'noaa19', 'metop-a', 'metop-b', 'metop-c', 'goes16', 'goes17', 'goes18'] @@ -88,29 +88,29 @@ class SatpassCommand(BaseCommand): other = ['starlink'] # Add weather satellites - shortcuts_text += "🌤️ Weather: " + shortcuts_text += self.translate('commands.satpass.category_weather') weather_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in weather_sats if name in self.SATELLITE_SHORTCUTS] shortcuts_text += ", ".join(weather_list) + "\n" # Add space stations - shortcuts_text += "🚀 Stations: " + shortcuts_text += self.translate('commands.satpass.category_stations') station_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in space_stations if name in self.SATELLITE_SHORTCUTS] shortcuts_text += ", ".join(station_list) + "\n" # Add telescopes - shortcuts_text += "🔭 Telescopes: " + shortcuts_text += self.translate('commands.satpass.category_telescopes') telescope_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in telescopes if name in self.SATELLITE_SHORTCUTS] shortcuts_text += ", ".join(telescope_list) + "\n" # Add other satellites - shortcuts_text += "🛰️ Other: " + shortcuts_text += self.translate('commands.satpass.category_other') other_list = [f"{name} ({self.SATELLITE_SHORTCUTS[name]})" for name in other if name in self.SATELLITE_SHORTCUTS] shortcuts_text += ", ".join(other_list) + "\n" - shortcuts_text += "\nExamples:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (visual passes only)" + shortcuts_text += self.translate('commands.satpass.examples') return shortcuts_text def get_help_text(self): """Get help text for this command""" - return self.description + return self.translate('commands.satpass.description') diff --git a/modules/commands/solar_command.py b/modules/commands/solar_command.py index 0ccf90b..8473272 100644 --- a/modules/commands/solar_command.py +++ b/modules/commands/solar_command.py @@ -27,12 +27,12 @@ class SolarCommand(BaseCommand): solar_info = solar_conditions() # Send response (solar only, more readable) - response = f"☀️ Solar: {solar_info}" + response = self.translate('commands.solar.response', info=solar_info) # Use the unified send_response method return await self.send_response(message, response) except Exception as e: - error_msg = f"Error getting solar info: {e}" + error_msg = self.translate('commands.solar.error', error=str(e)) await self.send_response(message, error_msg) return False diff --git a/modules/commands/solarforecast_command.py b/modules/commands/solarforecast_command.py index 961000c..c694784 100644 --- a/modules/commands/solarforecast_command.py +++ b/modules/commands/solarforecast_command.py @@ -27,9 +27,9 @@ class SolarforecastCommand(BaseCommand): category = "solar" cooldown_seconds = 10 # 10 second cooldown per user - # Error constants - ERROR_FETCHING_DATA = "Error fetching forecast" - NO_DATA_AVAILABLE = "No forecast data" + # Error constants - will use translations instead + ERROR_FETCHING_DATA = "Error fetching forecast" # Deprecated - use translate + NO_DATA_AVAILABLE = "No forecast data" # Deprecated - use translate # Forecast.Solar minimum panel size (10W) MIN_KWP = 0.01 @@ -57,7 +57,18 @@ class SolarforecastCommand(BaseCommand): self.db_manager = bot.db_manager def get_help_text(self) -> str: - return f"Usage: sf [watts] [azimuth, 0=south] [angle] - Example: sf seattle 10 0 37" + return self.translate('commands.solarforecast.usage') + + def _translate_day_abbreviation(self, day_abbr: str) -> str: + """Translate English day abbreviation to localized version""" + # Map English abbreviations to translation keys in common.date_time + translation_key = f'common.date_time.day_abbreviations.{day_abbr}' + translated = self.translate(translation_key) + # If translation found (not just the key), return it + if translated != translation_key: + return translated + # Fallback: return original if no translation found + return day_abbr def can_execute(self, message: MeshMessage) -> bool: """Override cooldown check to be per-user""" @@ -88,7 +99,7 @@ class SolarforecastCommand(BaseCommand): # Parse command: sf [panel_size] [azimuth] [angle] parts = content.split() if len(parts) < 2: - await self.send_response(message, "Usage: sf [W] [azimuth] [angle] - Example: sf seattle 10") + await self.send_response(message, self.translate('commands.solarforecast.usage_short')) return True try: @@ -107,7 +118,7 @@ class SolarforecastCommand(BaseCommand): num_value = float(parts[1]) if not (len(parts[1]) == 5 and parts[1].isdigit()): # Single non-5-digit number - invalid, no location provided - await self.send_response(message, "Usage: sf [W] [azimuth] [angle] - Example: sf seattle 10") + await self.send_response(message, self.translate('commands.solarforecast.usage_short')) return True except ValueError: pass # Not a number, continue with normal parsing @@ -139,7 +150,7 @@ class SolarforecastCommand(BaseCommand): # Validate that we have a location if not location_str: - await self.send_response(message, "Usage: sf [W] [azimuth] [angle] - Example: sf seattle 10") + await self.send_response(message, self.translate('commands.solarforecast.usage_short')) return True # Parse optional parameters @@ -154,7 +165,7 @@ class SolarforecastCommand(BaseCommand): panel_str = parts[param_start_idx].strip().rstrip('wW') panel_watts = float(panel_str) if panel_watts <= 0 or panel_watts > 1000: - await self.send_response(message, "Panel size must be 1-1000W") + await self.send_response(message, self.translate('commands.solarforecast.panel_size_range')) return True except ValueError: pass @@ -164,7 +175,7 @@ class SolarforecastCommand(BaseCommand): try: azimuth = float(parts[param_start_idx + 1]) if not (-180 <= azimuth <= 180): - await self.send_response(message, "Azimuth must be -180 to 180 (0=south)") + await self.send_response(message, self.translate('commands.solarforecast.azimuth_range')) return True except ValueError: pass @@ -174,7 +185,7 @@ class SolarforecastCommand(BaseCommand): try: angle = float(parts[param_start_idx + 2]) if not (0 <= angle <= 90): - await self.send_response(message, "Angle must be 0-90 degrees") + await self.send_response(message, self.translate('commands.solarforecast.angle_range')) return True except ValueError: pass @@ -182,7 +193,7 @@ class SolarforecastCommand(BaseCommand): # Parse location (check for repeater name first, then coordinates, zip, or city) lat, lon, location_type = await self._parse_location(location_str) if lat is None or lon is None: - await self.send_response(message, f"Could not find location: {location_str}") + await self.send_response(message, self.translate('commands.solarforecast.no_location', location=location_str)) return True # Get location name for confirmation @@ -198,7 +209,7 @@ class SolarforecastCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in solar forecast command: {e}") - await self.send_response(message, f"Error: {e}") + await self.send_response(message, self.translate('commands.solarforecast.error', error=str(e))) return True def _clean_location_string(self, location: str) -> str: @@ -527,18 +538,18 @@ class SolarforecastCommand(BaseCommand): result = await self._query_forecast_solar_scaled(lat, lon, angle, azimuth, kwp, api_key) if not result: - return self.ERROR_FETCHING_DATA + return self.translate('commands.solarforecast.error_fetching') # Check for rate limiting if result.get('rate_limited'): - return "Rate limit: Try again in 5 min" + return self.translate('commands.solarforecast.rate_limit') # Format output to fit 130 characters return self._format_forecast(result, panel_watts, location_name, lat, lon) except Exception as e: self.logger.error(f"Error getting forecast: {e}") - return f"Error: {e}" + return self.translate('commands.solarforecast.error', error=str(e)) def _get_cache_key(self, lat: float, lon: float, declination: float, azimuth: float, kwp: float, api_key: Optional[str]) -> str: @@ -702,7 +713,7 @@ class SolarforecastCommand(BaseCommand): num_days = result.get('num_days', 0) if not watt_hours_day: - return self.NO_DATA_AVAILABLE + return self.translate('commands.solarforecast.no_data') # Get timezone from config or use system timezone timezone_str = self.bot.config.get('Bot', 'timezone', fallback='') @@ -728,8 +739,11 @@ class SolarforecastCommand(BaseCommand): # Get day names for Day+2 and Day+3 day_after_date = now + timedelta(days=2) day_after_2_date = now + timedelta(days=3) - day_after_name = day_after_date.strftime('%a') # Mon, Tue, Wed, etc. - day_after_2_name = day_after_2_date.strftime('%a') + day_after_name_en = day_after_date.strftime('%a') # Mon, Tue, Wed, etc. + day_after_2_name_en = day_after_2_date.strftime('%a') + # Translate day abbreviations + day_after_name = self._translate_day_abbreviation(day_after_name_en) + day_after_2_name = self._translate_day_abbreviation(day_after_2_name_en) # Build user-friendly forecast with peak grouped by day day_parts = [] @@ -929,11 +943,11 @@ class SolarforecastCommand(BaseCommand): pass today_prod_hours = len(unique_hours) - today_part = f"Today:{today_wh:.0f}Wh" + today_part = self.translate('commands.solarforecast.labels.today', wh=today_wh) if today_prod_hours > 0: if first_day_utilization is not None and first_day_date == today: # First day - show %util - today_part += f" ({today_prod_hours}h,{first_day_utilization:.0f}%util)" + today_part += self.translate('commands.solarforecast.labels.hours_util', hours=today_prod_hours, util=first_day_utilization) else: # Calculate utilization for this day # Use 100% of panel capacity - API already accounts for real-world conditions @@ -941,13 +955,13 @@ class SolarforecastCommand(BaseCommand): typical_max_energy = typical_max_power * today_prod_hours if typical_max_energy > 0: utilization = (today_wh / typical_max_energy) * 100 - today_part += f" ({today_prod_hours}h,{utilization:.0f}%)" + today_part += self.translate('commands.solarforecast.labels.hours_percent', hours=today_prod_hours, percent=utilization) else: - today_part += f" ({today_prod_hours}h)" + today_part += self.translate('commands.solarforecast.labels.hours_only', hours=today_prod_hours) # Add peak if it's today if peak_date == today and peak_time_str: - today_part += f" Peak: {max_watts:.0f}W@{peak_time_str}" + today_part += " " + self.translate('commands.solarforecast.labels.peak', watts=max_watts, time=peak_time_str) # First line includes panel info and location first_line = f"{first_line_prefix}{today_part}" @@ -984,11 +998,11 @@ class SolarforecastCommand(BaseCommand): pass tomorrow_prod_hours = len(unique_hours) - tomorrow_part = f"Tmrw:{tomorrow_wh:.0f}Wh" + tomorrow_part = self.translate('commands.solarforecast.labels.tomorrow', wh=tomorrow_wh) if tomorrow_prod_hours > 0: if first_day_utilization is not None and first_day_date == tomorrow: # First day - show %util - tomorrow_part += f" ({tomorrow_prod_hours}h,{first_day_utilization:.0f}%util)" + tomorrow_part += self.translate('commands.solarforecast.labels.hours_util', hours=tomorrow_prod_hours, util=first_day_utilization) else: # Calculate utilization for this day # Use 100% of panel capacity - API already accounts for real-world conditions @@ -996,13 +1010,13 @@ class SolarforecastCommand(BaseCommand): typical_max_energy = typical_max_power * tomorrow_prod_hours if typical_max_energy > 0: utilization = (tomorrow_wh / typical_max_energy) * 100 - tomorrow_part += f" ({tomorrow_prod_hours}h,{utilization:.0f}%)" + tomorrow_part += self.translate('commands.solarforecast.labels.hours_percent', hours=tomorrow_prod_hours, percent=utilization) else: - tomorrow_part += f" ({tomorrow_prod_hours}h)" + tomorrow_part += self.translate('commands.solarforecast.labels.hours_only', hours=tomorrow_prod_hours) # Add peak if it's tomorrow if peak_date == tomorrow and peak_time_str: - tomorrow_part += f" Peak: {max_watts:.0f}W@{peak_time_str}" + tomorrow_part += " " + self.translate('commands.solarforecast.labels.peak', watts=max_watts, time=peak_time_str) lines.append(tomorrow_part) @@ -1040,7 +1054,7 @@ class SolarforecastCommand(BaseCommand): pass day_after_prod_hours = len(unique_hours) - day_after_part = f"{day_after_name}:{day_after_wh:.0f}Wh" + day_after_part = self.translate('commands.solarforecast.labels.day_format', day=day_after_name, wh=day_after_wh) if day_after_prod_hours > 0: # Calculate utilization for this day # Use 100% of panel capacity - API already accounts for real-world conditions @@ -1048,9 +1062,9 @@ class SolarforecastCommand(BaseCommand): typical_max_energy = typical_max_power * day_after_prod_hours if typical_max_energy > 0: utilization = (day_after_wh / typical_max_energy) * 100 - day_after_part += f" ({day_after_prod_hours}h,{utilization:.0f}%)" + day_after_part += self.translate('commands.solarforecast.labels.hours_percent', hours=day_after_prod_hours, percent=utilization) else: - day_after_part += f" ({day_after_prod_hours}h)" + day_after_part += self.translate('commands.solarforecast.labels.hours_only', hours=day_after_prod_hours) # Peak is only shown for today or tomorrow, not for later days @@ -1087,7 +1101,7 @@ class SolarforecastCommand(BaseCommand): pass day_after_2_prod_hours = len(unique_hours) - day_after_2_part = f"{day_after_2_name}:{day_after_2_wh:.0f}Wh" + day_after_2_part = self.translate('commands.solarforecast.labels.day_format', day=day_after_2_name, wh=day_after_2_wh) if day_after_2_prod_hours > 0: # Calculate utilization for this day # Use 100% of panel capacity - API already accounts for real-world conditions @@ -1095,9 +1109,9 @@ class SolarforecastCommand(BaseCommand): typical_max_energy = typical_max_power * day_after_2_prod_hours if typical_max_energy > 0: utilization = (day_after_2_wh / typical_max_energy) * 100 - day_after_2_part += f" ({day_after_2_prod_hours}h,{utilization:.0f}%)" + day_after_2_part += self.translate('commands.solarforecast.labels.hours_percent', hours=day_after_2_prod_hours, percent=utilization) else: - day_after_2_part += f" ({day_after_2_prod_hours}h)" + day_after_2_part += self.translate('commands.solarforecast.labels.hours_only', hours=day_after_2_prod_hours) # Peak is only shown for today or tomorrow, not for later days @@ -1105,7 +1119,8 @@ class SolarforecastCommand(BaseCommand): # Add Day+2 and Day+3 on same line if both exist if day_plus_line_parts: - day_plus_line = " | ".join(day_plus_line_parts) + separator = self.translate('commands.solarforecast.labels.separator') + day_plus_line = separator.join(day_plus_line_parts) lines.append(day_plus_line) # If peak has passed, add it at the end @@ -1118,7 +1133,7 @@ class SolarforecastCommand(BaseCommand): today_watts.append(power) if today_watts: past_max = max(today_watts) - lines.append(f"Peak: {past_max:.0f}W (past)") + lines.append(self.translate('commands.solarforecast.labels.peak_past', watts=past_max)) # Join lines with newlines full_message = "\n".join(lines) diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index 84c0b92..c86ea8d 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -528,7 +528,7 @@ class SportsCommand(BaseCommand): return True def get_help_text(self) -> str: - return "Get sports scores & schedules. Use 'sports' for default teams, 'sports [team]' for specific team, or 'sports [league]' for league games." + return self.translate('commands.sports.help') async def execute(self, message: MeshMessage) -> bool: """Execute the sports command""" @@ -563,12 +563,12 @@ class SportsCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in sports command: {e}") - return await self.send_response(message, "Error fetching sports data") + return await self.send_response(message, self.translate('commands.sports.error_fetching')) async def get_default_teams_scores(self) -> str: """Get scores for default teams, sorted by game time""" if not self.default_teams: - return "No default teams configured" + return self.translate('commands.sports.no_default_teams') game_data = [] for team in self.default_teams: @@ -582,7 +582,7 @@ class SportsCommand(BaseCommand): self.logger.warning(f"Error fetching score for {team}: {e}") if not game_data: - return "No games found for default teams" + return self.translate('commands.sports.no_games_default') # Sort by game time (earliest first) game_data.sort(key=lambda x: x['timestamp']) @@ -753,7 +753,7 @@ class SportsCommand(BaseCommand): async def get_city_scores(self, city_teams: List[Dict[str, str]], city_name: str) -> str: """Get scores for all teams in a city""" if not city_teams: - return f"No teams found for {city_name}" + return self.translate('commands.sports.no_teams_city', city=city_name) game_data = [] for team_info in city_teams: @@ -765,7 +765,7 @@ class SportsCommand(BaseCommand): self.logger.warning(f"Error fetching score for {team_info}: {e}") if not game_data: - return f"No games found for {city_name} teams" + return self.translate('commands.sports.no_games_city', city=city_name) # Sort by game time (earliest first) game_data.sort(key=lambda x: x['timestamp']) @@ -802,7 +802,7 @@ class SportsCommand(BaseCommand): events = data.get('events', []) if not events: - return f"No games found for {league_info['sport']}" + return self.translate('commands.sports.no_games_league', sport=league_info['sport']) # Parse all games and sort by time game_data = [] @@ -812,7 +812,7 @@ class SportsCommand(BaseCommand): game_data.append(game_info) if not game_data: - return f"No games found for {league_info['sport']}" + return self.translate('commands.sports.no_games_league', sport=league_info['sport']) # Sort by game time (earliest first) game_data.sort(key=lambda x: x['timestamp']) @@ -837,7 +837,7 @@ class SportsCommand(BaseCommand): except Exception as e: self.logger.error(f"Error fetching league scores: {e}") - return f"Error fetching {league_info['sport']} data" + return self.translate('commands.sports.error_fetching_league', sport=league_info['sport']) def parse_league_game_event(self, event: Dict, sport: str, league: str) -> Optional[Dict]: """Parse a league game event and return structured data with timestamp for sorting""" @@ -1020,7 +1020,7 @@ class SportsCommand(BaseCommand): # Otherwise, treat as single team query team_info = self.TEAM_MAPPINGS.get(team_name) if not team_info: - return f"Team/League '{team_name}' not found. Try: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + return self.translate('commands.sports.team_not_found', team=team_name) try: score_info = await self.fetch_team_score(team_info) @@ -1029,10 +1029,10 @@ class SportsCommand(BaseCommand): sport_emoji = self.SPORT_EMOJIS.get(team_info['sport'], '🏆') return f"{sport_emoji} {score_info}" else: - return f"No games found for {team_name}" + return self.translate('commands.sports.no_games_team', team=team_name) except Exception as e: self.logger.error(f"Error fetching score for {team_name}: {e}") - return f"Error fetching data for {team_name}" + return self.translate('commands.sports.error_fetching_team', team=team_name) async def fetch_team_score(self, team_info: Dict[str, str]) -> Optional[str]: """Fetch score information for a team (legacy method for individual team queries)""" diff --git a/modules/commands/stats_command.py b/modules/commands/stats_command.py index c38b8e4..7389e91 100644 --- a/modules/commands/stats_command.py +++ b/modules/commands/stats_command.py @@ -259,12 +259,12 @@ class StatsCommand(BaseCommand): return path def get_help_text(self) -> str: - return "Show 24-hour bot statistics. Commands: 'stats' (basic), 'stats messages' (bot users), 'stats channels' (channel activity), 'stats paths' (longest paths)" + return self.translate('commands.stats.help') async def execute(self, message: MeshMessage) -> bool: """Execute the stats command""" if not self.stats_enabled: - await self.send_response(message, "Stats command is disabled") + await self.send_response(message, self.translate('commands.stats.disabled')) return False try: @@ -287,7 +287,7 @@ class StatsCommand(BaseCommand): elif subcommand in ['paths', 'path']: response = await self._get_path_leaderboard() else: - response = f"Unknown: {subcommand}. Use 'stats', 'stats messages', 'stats channels', or 'stats paths'" + response = self.translate('commands.stats.unknown_subcommand', subcommand=subcommand) else: response = await self._get_basic_stats() @@ -298,7 +298,7 @@ class StatsCommand(BaseCommand): except Exception as e: self.logger.error(f"Error executing stats command: {e}") - await self.send_response(message, f"Error getting stats: {e}") + await self.send_response(message, self.translate('commands.stats.error', error=str(e))) return False async def _get_basic_stats(self) -> str: @@ -335,7 +335,10 @@ class StatsCommand(BaseCommand): LIMIT 1 ''', (day_ago,)) top_command_result = cursor.fetchone() - top_command = f"{top_command_result[0]} ({top_command_result[1]})" if top_command_result else "None" + if top_command_result: + top_command = f"{top_command_result[0]} ({top_command_result[1]})" + else: + top_command = self.translate('commands.stats.basic.none') # Top user cursor.execute(''' @@ -347,18 +350,21 @@ class StatsCommand(BaseCommand): LIMIT 1 ''', (day_ago,)) top_user_result = cursor.fetchone() - top_user = f"{top_user_result[0]} ({top_user_result[1]})" if top_user_result else "None" + if top_user_result: + top_user = f"{top_user_result[0]} ({top_user_result[1]})" + else: + top_user = self.translate('commands.stats.basic.none') - response = f"""Bot Stats (24h): -Cmds: {commands_received} | Replies: {bot_replies} -Top cmd: {top_command} -Top user: {top_user}""" + response = f"""{self.translate('commands.stats.basic.header')} +{self.translate('commands.stats.basic.commands', count=commands_received, replies=bot_replies)} +{self.translate('commands.stats.basic.top_command', command=top_command)} +{self.translate('commands.stats.basic.top_user', user=top_user)}""" return response except Exception as e: self.logger.error(f"Error getting basic stats: {e}") - return f"Error getting stats: {e}" + return self.translate('commands.stats.error', error=str(e)) async def _get_bot_user_leaderboard(self) -> str: """Get leaderboard for bot users (people who triggered bot responses)""" @@ -382,20 +388,20 @@ Top user: {top_user}""" top_users = cursor.fetchall() # Build response - response = "Bot Users (24h):\n" + response = self.translate('commands.stats.users.header') + "\n" if top_users: for i, (user, count) in enumerate(top_users, 1): display_user = user[:12] + "..." if len(user) > 15 else user response += f"{i}. {display_user}: {count}\n" else: - response += "No bot users\n" + response += self.translate('commands.stats.users.none') + "\n" return response except Exception as e: self.logger.error(f"Error getting bot user leaderboard: {e}") - return f"Error getting bot user stats: {e}" + return self.translate('commands.stats.error_bot_users', error=str(e)) async def _get_channel_leaderboard(self) -> str: """Get leaderboard for channel message activity""" @@ -419,25 +425,25 @@ Top user: {top_user}""" top_channels = cursor.fetchall() # Build compact response - response = "Top Channels (24h)\n" + response = self.translate('commands.stats.channels.header') + "\n" if top_channels: for i, (channel, msg_count, unique_users) in enumerate(top_channels, 1): display_channel = channel[:12] + "..." if len(channel) > 15 else channel # Handle singular/plural for messages and users - msg_text = "msg" if msg_count == 1 else "msgs" - user_text = "user" if unique_users == 1 else "users" - response += f"{i}. {display_channel}: {msg_count} {msg_text} | {unique_users} {user_text}\n" + msg_text = self.translate('commands.stats.channels.msg_singular') if msg_count == 1 else self.translate('commands.stats.channels.msg_plural') + user_text = self.translate('commands.stats.channels.user_singular') if unique_users == 1 else self.translate('commands.stats.channels.user_plural') + response += self.translate('commands.stats.channels.format', rank=i, channel=display_channel, msg_count=msg_count, msg_text=msg_text, user_count=unique_users, user_text=user_text) + "\n" # Remove trailing newline response = response.rstrip('\n') else: - response += "No channel activity" + response += self.translate('commands.stats.channels.none') return response except Exception as e: self.logger.error(f"Error getting channel leaderboard: {e}") - return f"Error getting channel stats: {e}" + return self.translate('commands.stats.error_channels', error=str(e)) async def _get_path_leaderboard(self) -> str: """Get leaderboard for longest paths seen""" @@ -475,7 +481,7 @@ Top user: {top_user}""" # Truncate sender name to fit more data display_sender = sender[:8] + "..." if len(sender) > 11 else sender # Compact format: "1 Gundam 56,1c,98,1a,aa,cd,5f" - new_line = f"{i} {display_sender} {path_str}\n" + new_line = self.translate('commands.stats.paths.format', rank=i, sender=display_sender, path=path_str) + "\n" # Check if adding this line would exceed the limit if len(response + new_line) > max_length: @@ -483,13 +489,13 @@ Top user: {top_user}""" response += new_line else: - response = "No path data\n" + response = self.translate('commands.stats.paths.none') + "\n" return response except Exception as e: self.logger.error(f"Error getting path leaderboard: {e}") - return f"Error getting path stats: {e}" + return self.translate('commands.stats.error_paths', error=str(e)) def cleanup_old_stats(self, days_to_keep: int = 7): """Clean up old stats data to prevent database bloat""" diff --git a/modules/commands/sun_command.py b/modules/commands/sun_command.py index 5469ac4..880073f 100644 --- a/modules/commands/sun_command.py +++ b/modules/commands/sun_command.py @@ -22,13 +22,13 @@ class SunCommand(BaseCommand): sun_info = get_sun() # Send response using unified method - response = f"☀️ Sun Info:\n{sun_info}" + response = self.translate('commands.sun.response', info=sun_info) return await self.send_response(message, response) except Exception as e: - error_msg = f"Error getting sun info: {e}" + error_msg = self.translate('commands.sun.error', error=str(e)) return await self.send_response(message, error_msg) def get_help_text(self): """Get help text for this command""" - return "Get sunrise/sunset times and sun position" + return self.translate('commands.sun.help') diff --git a/modules/commands/test_command.py b/modules/commands/test_command.py index bf906ab..b21116c 100644 --- a/modules/commands/test_command.py +++ b/modules/commands/test_command.py @@ -5,8 +5,12 @@ Handles the 'test' keyword response """ import re +import math +from datetime import datetime +from typing import List, Optional, Tuple, Dict, Any from .base_command import BaseCommand from ..models import MeshMessage +from ..utils import calculate_distance class TestCommand(BaseCommand): @@ -19,7 +23,7 @@ class TestCommand(BaseCommand): category = "basic" def get_help_text(self) -> str: - return self.description + return self.translate('commands.test.help') def clean_content(self, content: str) -> str: """Clean content by removing control characters and normalizing whitespace""" @@ -62,6 +66,366 @@ class TestCommand(BaseCommand): return self._strip_quotes_from_config(format_str) if format_str else None return None + def _extract_path_node_ids(self, message: MeshMessage) -> List[str]: + """Extract path node IDs from message path string""" + if not message.path: + return [] + + # Check if it's a direct connection + if "Direct" in message.path or "0 hops" in message.path: + return [] + + # Extract path nodes from the path string + # Path strings are typically in format: "node1,node2,node3 via ROUTE_TYPE_*" + # or just "node1,node2,node3" + path_string = message.path + + # Remove route type suffix if present + if " via ROUTE_TYPE_" in path_string: + path_string = path_string.split(" via ROUTE_TYPE_")[0] + + # Check if it looks like a comma-separated path + if ',' in path_string: + # Clean up any extra info (like hop counts in parentheses) + # Example: "01,7e,55,86 (4 hops)" -> "01,7e,55,86" + if '(' in path_string: + path_string = path_string.split('(')[0].strip() + + # Validate that all parts are 2-character hex values + parts = path_string.split(',') + valid_parts = [] + for part in parts: + part = part.strip() + # Check if it's a 2-character hex value + if len(part) == 2 and all(c in '0123456789abcdefABCDEF' for c in part): + valid_parts.append(part.upper()) + + return valid_parts + + return [] + + + def _lookup_repeater_location(self, node_id: str, path_context: Optional[List[str]] = None) -> Optional[Tuple[float, float]]: + """Look up repeater location for a node ID using geographic proximity selection when path context is available""" + try: + if not hasattr(self.bot, 'db_manager'): + return None + + # Query for all repeaters with matching prefix + query = ''' + SELECT latitude, longitude, public_key, name, + last_advert_timestamp, last_heard, advert_count + FROM complete_contact_tracking + WHERE public_key LIKE ? AND role IN ('repeater', 'roomserver') + AND latitude IS NOT NULL AND longitude IS NOT NULL + AND latitude != 0 AND longitude != 0 + ''' + + prefix_pattern = f"{node_id}%" + results = self.bot.db_manager.execute_query(query, (prefix_pattern,)) + + if not results or len(results) == 0: + return None + + # Convert to list of dicts for processing + repeaters = [] + for row in results: + repeaters.append({ + 'latitude': row.get('latitude'), + 'longitude': row.get('longitude'), + 'public_key': row.get('public_key'), + 'name': row.get('name'), + 'last_advert_timestamp': row.get('last_advert_timestamp'), + 'last_heard': row.get('last_heard'), + 'advert_count': row.get('advert_count', 0) + }) + + # If only one repeater, return it + if len(repeaters) == 1: + r = repeaters[0] + return (float(r['latitude']), float(r['longitude'])) + + # Multiple repeaters - use geographic proximity selection if path context available + if path_context and len(path_context) > 1: + selected = self._select_by_path_proximity(repeaters, node_id, path_context) + if selected: + return (float(selected['latitude']), float(selected['longitude'])) + + # Fall back to most recent repeater + scored = self._calculate_recency_weighted_scores(repeaters) + if scored: + best_repeater = scored[0][0] + return (float(best_repeater['latitude']), float(best_repeater['longitude'])) + + return None + except Exception as e: + self.logger.debug(f"Error looking up repeater location for {node_id}: {e}") + return None + + def _calculate_recency_weighted_scores(self, repeaters: List[Dict[str, Any]]) -> List[Tuple[Dict[str, Any], float]]: + """Calculate recency-weighted scores for repeaters (0.0 to 1.0, higher = more recent)""" + scored_repeaters = [] + now = datetime.now() + + for repeater in repeaters: + most_recent_time = None + + # Check last_heard + last_heard = repeater.get('last_heard') + if last_heard: + try: + if isinstance(last_heard, str): + dt = datetime.fromisoformat(last_heard.replace('Z', '+00:00')) + else: + dt = last_heard + if most_recent_time is None or dt > most_recent_time: + most_recent_time = dt + except: + pass + + # Check last_advert_timestamp + last_advert = repeater.get('last_advert_timestamp') + if last_advert: + try: + if isinstance(last_advert, str): + dt = datetime.fromisoformat(last_advert.replace('Z', '+00:00')) + else: + dt = last_advert + if most_recent_time is None or dt > most_recent_time: + most_recent_time = dt + except: + pass + + if most_recent_time is None: + recency_score = 0.1 + else: + hours_ago = (now - most_recent_time).total_seconds() / 3600.0 + recency_score = math.exp(-hours_ago / 12.0) + recency_score = max(0.0, min(1.0, recency_score)) + + scored_repeaters.append((repeater, recency_score)) + + # Sort by recency score (highest first) + scored_repeaters.sort(key=lambda x: x[1], reverse=True) + return scored_repeaters + + def _get_node_location_simple(self, node_id: str) -> Optional[Tuple[float, float]]: + """Simple lookup without proximity selection - used for reference nodes""" + try: + if not hasattr(self.bot, 'db_manager'): + return None + + query = ''' + SELECT latitude, longitude + FROM complete_contact_tracking + WHERE public_key LIKE ? AND role IN ('repeater', 'roomserver') + AND latitude IS NOT NULL AND longitude IS NOT NULL + AND latitude != 0 AND longitude != 0 + ORDER BY COALESCE(last_advert_timestamp, last_heard) DESC + LIMIT 1 + ''' + + prefix_pattern = f"{node_id}%" + results = self.bot.db_manager.execute_query(query, (prefix_pattern,)) + + if results and len(results) > 0: + row = results[0] + lat = row.get('latitude') + lon = row.get('longitude') + if lat is not None and lon is not None: + return (float(lat), float(lon)) + + return None + except Exception as e: + self.logger.debug(f"Error in simple location lookup for {node_id}: {e}") + return None + + def _select_by_path_proximity(self, repeaters: List[Dict[str, Any]], node_id: str, path_context: List[str]) -> Optional[Dict[str, Any]]: + """Select repeater based on proximity to previous/next nodes in path""" + try: + # Filter by recency first + scored_repeaters = self._calculate_recency_weighted_scores(repeaters) + min_recency_threshold = 0.01 # Approximately 55 hours ago or less + recent_repeaters = [r for r, score in scored_repeaters if score >= min_recency_threshold] + + if not recent_repeaters: + return None + + # Find current node position in path + current_index = path_context.index(node_id) if node_id in path_context else -1 + if current_index == -1: + return None + + # Get previous and next node locations + prev_location = None + next_location = None + + if current_index > 0: + prev_node_id = path_context[current_index - 1] + prev_location = self._get_node_location_simple(prev_node_id) + + if current_index < len(path_context) - 1: + next_node_id = path_context[current_index + 1] + next_location = self._get_node_location_simple(next_node_id) + + # Use proximity selection + if prev_location and next_location: + return self._select_by_dual_proximity(recent_repeaters, prev_location, next_location) + elif prev_location: + return self._select_by_single_proximity(recent_repeaters, prev_location) + elif next_location: + return self._select_by_single_proximity(recent_repeaters, next_location) + else: + return None + + except Exception as e: + self.logger.debug(f"Error in path proximity selection: {e}") + return None + + def _select_by_dual_proximity(self, repeaters: List[Dict[str, Any]], prev_location: Tuple[float, float], next_location: Tuple[float, float]) -> Optional[Dict[str, Any]]: + """Select repeater based on proximity to both previous and next nodes""" + scored_repeaters = self._calculate_recency_weighted_scores(repeaters) + min_recency_threshold = 0.01 + scored_repeaters = [(r, score) for r, score in scored_repeaters if score >= min_recency_threshold] + + if not scored_repeaters: + return None + + best_repeater = None + best_combined_score = 0.0 + + for repeater, recency_score in scored_repeaters: + # Calculate distance to previous node + prev_distance = calculate_distance( + prev_location[0], prev_location[1], + repeater['latitude'], repeater['longitude'] + ) + + # Calculate distance to next node + next_distance = calculate_distance( + next_location[0], next_location[1], + repeater['latitude'], repeater['longitude'] + ) + + # Combined proximity score (lower distance = higher score) + avg_distance = (prev_distance + next_distance) / 2 + normalized_distance = min(avg_distance / 1000.0, 1.0) + proximity_score = 1.0 - normalized_distance + + # Weight: 40% recency, 60% proximity + combined_score = (recency_score * 0.4) + (proximity_score * 0.6) + + if combined_score > best_combined_score: + best_combined_score = combined_score + best_repeater = repeater + + return best_repeater + + def _select_by_single_proximity(self, repeaters: List[Dict[str, Any]], reference_location: Tuple[float, float]) -> Optional[Dict[str, Any]]: + """Select repeater based on proximity to single reference node""" + scored_repeaters = self._calculate_recency_weighted_scores(repeaters) + min_recency_threshold = 0.01 + scored_repeaters = [(r, score) for r, score in scored_repeaters if score >= min_recency_threshold] + + if not scored_repeaters: + return None + + best_repeater = None + best_combined_score = 0.0 + + for repeater, recency_score in scored_repeaters: + distance = calculate_distance( + reference_location[0], reference_location[1], + repeater['latitude'], repeater['longitude'] + ) + + # Proximity score (closer = higher score) + normalized_distance = min(distance / 1000.0, 1.0) + proximity_score = 1.0 - normalized_distance + + # Weight: 40% recency, 60% proximity + combined_score = (recency_score * 0.4) + (proximity_score * 0.6) + + if combined_score > best_combined_score: + best_combined_score = combined_score + best_repeater = repeater + + return best_repeater + + def _calculate_path_distance(self, message: MeshMessage) -> str: + """Calculate total distance along path (sum of distances between consecutive repeaters with locations)""" + node_ids = self._extract_path_node_ids(message) + if len(node_ids) < 2: + # Check if it's a direct connection + if not message.path or "Direct" in message.path or "0 hops" in message.path: + return "N/A" # Direct connection, no path to calculate + return "" # Path exists but insufficient nodes + + total_distance = 0.0 + valid_segments = 0 + skipped_nodes = 0 + + # Get locations for all nodes using path context for proximity selection + locations = [] + for i, node_id in enumerate(node_ids): + location = self._lookup_repeater_location(node_id, path_context=node_ids) + if location: + locations.append((node_id, location)) + else: + skipped_nodes += 1 + + # Calculate distances between consecutive nodes with locations + # This skips nodes without locations but continues the path + for i in range(len(locations) - 1): + prev_node_id, prev_location = locations[i] + next_node_id, next_location = locations[i + 1] + + # Calculate distance between consecutive repeaters with locations + distance = calculate_distance( + prev_location[0], prev_location[1], + next_location[0], next_location[1] + ) + total_distance += distance + valid_segments += 1 + + if valid_segments == 0: + return "" # No valid segments found + + # Format the result compactly + if skipped_nodes > 0: + return f"{total_distance:.1f}km ({valid_segments} segs, {skipped_nodes} no-loc)" + else: + return f"{total_distance:.1f}km ({valid_segments} segs)" + + def _calculate_firstlast_distance(self, message: MeshMessage) -> str: + """Calculate straight-line distance between first and last repeater in path""" + node_ids = self._extract_path_node_ids(message) + if len(node_ids) < 2: + # Check if it's a direct connection + if not message.path or "Direct" in message.path or "0 hops" in message.path: + return "N/A" # Direct connection, no path to calculate + return "" # Path exists but insufficient nodes + + # Get first and last node IDs + first_node_id = node_ids[0] + last_node_id = node_ids[-1] + + # Use path context for better selection when multiple repeaters share prefix + first_location = self._lookup_repeater_location(first_node_id, path_context=node_ids) + last_location = self._lookup_repeater_location(last_node_id, path_context=node_ids) + + # Both locations must be available + if not first_location or not last_location: + return "" # Fail if either location is missing + + # Calculate straight-line distance + distance = calculate_distance( + first_location[0], first_location[1], + last_location[0], last_location[1] + ) + + return f"{distance:.1f}km" + def format_response(self, message: MeshMessage, response_format: str) -> str: """Override to handle phrase extraction""" # Clean content to remove control characters and normalize whitespace @@ -87,17 +451,23 @@ class TestCommand(BaseCommand): connection_info = self.build_enhanced_connection_info(message) timestamp = self.format_timestamp(message) + # Calculate distance placeholders + path_distance = self._calculate_path_distance(message) + firstlast_distance = self._calculate_firstlast_distance(message) + # Format phrase part - add colon and space if phrase exists phrase_part = f": {phrase}" if phrase else "" return response_format.format( - sender=message.sender_id or "Unknown", + sender=message.sender_id or self.translate('common.unknown_sender'), phrase=phrase, phrase_part=phrase_part, connection_info=connection_info, - path=message.path or "Unknown", + path=message.path or self.translate('common.unknown_path'), timestamp=timestamp, - snr=message.snr or "Unknown" + snr=message.snr or self.translate('common.unknown'), + path_distance=path_distance or "", + firstlast_distance=firstlast_distance or "" ) except (KeyError, ValueError) as e: self.logger.warning(f"Error formatting test response: {e}") diff --git a/modules/commands/wx_command.py b/modules/commands/wx_command.py index 88ba799..2af3ccd 100644 --- a/modules/commands/wx_command.py +++ b/modules/commands/wx_command.py @@ -8,7 +8,7 @@ import re import json import requests import xml.dom.minidom -from datetime import datetime +from datetime import datetime, timedelta from geopy.geocoders import Nominatim import maidenhead as mh from .base_command import BaseCommand @@ -51,7 +51,7 @@ class WxCommand(BaseCommand): self.db_manager = bot.db_manager def get_help_text(self) -> str: - return f"Usage: wx - Get weather for US zipcode or city in {self.default_state}" + return self.translate('commands.wx.description') def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with a weather keyword""" @@ -107,15 +107,43 @@ class WxCommand(BaseCommand): """Execute the weather command""" content = message.content.strip() - # Parse the command to extract location + # Parse the command to extract location and forecast type # Support formats: "wx 12345", "wx seattle", "wx paris, tx", "weather everett", "wxa bellingham" + # New formats: "wx 12345 tomorrow", "wx 12345 7", "wx 12345 7day" parts = content.split() if len(parts) < 2: - await self.send_response(message, f"Usage: wx - Example: wx 12345 or wx seattle or wx paris, tx") + await self.send_response(message, self.translate('commands.wx.usage')) return True - # Join all parts after the command to handle "city, state" format - location = ' '.join(parts[1:]).strip() + # Check for forecast type options: "tomorrow", or a number 2-7 + forecast_type = "default" + num_days = 7 # Default for multi-day forecast + location_parts = parts[1:] + + # Check last part for forecast type + if len(location_parts) > 0: + last_part = location_parts[-1].lower() + if last_part == "tomorrow": + forecast_type = "tomorrow" + location_parts = location_parts[:-1] + elif last_part.isdigit(): + # Check if it's a number between 2-7 + days = int(last_part) + if 2 <= days <= 7: + forecast_type = "multiday" + num_days = days + location_parts = location_parts[:-1] + elif last_part in ["7day", "7-day"]: + forecast_type = "multiday" + num_days = 7 + location_parts = location_parts[:-1] + + # Join remaining parts to handle "city, state" format + location = ' '.join(location_parts).strip() + + if not location: + await self.send_response(message, self.translate('commands.wx.usage')) + return True # Check if it's a zipcode (5 digits) or city name if re.match(r'^\d{5}$', location): @@ -130,7 +158,7 @@ class WxCommand(BaseCommand): self._record_execution(message.sender_id) # Get weather data for the location - weather_data = await self.get_weather_for_location(location, location_type) + weather_data = await self.get_weather_for_location(location, location_type, forecast_type, num_days) # Check if we need to send multiple messages if isinstance(weather_data, tuple) and weather_data[0] == "multi_message": @@ -148,6 +176,9 @@ class WxCommand(BaseCommand): alert_text = weather_data[2] alert_count = weather_data[3] await self.send_response(message, f"{alert_count} alerts: {alert_text}") + elif forecast_type == "multiday": + # Use message splitting for multi-day forecasts + await self._send_multiday_forecast(message, weather_data) else: # Send single message as usual await self.send_response(message, weather_data) @@ -156,17 +187,25 @@ class WxCommand(BaseCommand): except Exception as e: self.logger.error(f"Error in weather command: {e}") - await self.send_response(message, f"Error getting weather data: {e}") + await self.send_response(message, self.translate('commands.wx.error', error=str(e))) return True - async def get_weather_for_location(self, location: str, location_type: str) -> str: - """Get weather data for a location (zipcode or city)""" + async def get_weather_for_location(self, location: str, location_type: str, forecast_type: str = "default", num_days: int = 7) -> str: + """Get weather data for a location (zipcode or city) + + Args: + location: The location (zipcode or city name) + location_type: "zipcode" or "city" + forecast_type: "default", "tomorrow", or "multiday" + num_days: Number of days for multiday forecast (2-7) + """ try: # Convert location to lat/lon if location_type == "zipcode": lat, lon = self.zipcode_to_lat_lon(location) if lat is None or lon is None: - return f"Could not find location for zipcode {location}" + return self.translate('commands.wx.no_location_zipcode', location=location) + address_info = None else: # city result = self.city_to_lat_lon(location) if len(result) == 3: @@ -176,7 +215,7 @@ class WxCommand(BaseCommand): address_info = None if lat is None or lon is None: - return f"Could not find city '{location}' in {self.default_state}" + return self.translate('commands.wx.no_location_city', location=location, state=self.default_state) # Check if the found city is in a different state than default actual_city = location @@ -216,11 +255,6 @@ class WxCommand(BaseCommand): abbrev_to_full_map = {v: k for k, v in state_abbrev_map.items()} default_state_full = abbrev_to_full_map.get(self.default_state, self.default_state) - # Get weather forecast - weather, points_data = self.get_noaa_weather(lat, lon) - if weather == self.ERROR_FETCHING_DATA: - return "Error fetching weather data from NOAA" - # Add location info if city is in a different state than default location_prefix = "" if location_type == "city" and address_info: @@ -230,29 +264,46 @@ class WxCommand(BaseCommand): if states_different: location_prefix = f"{actual_city}, {actual_state}: " - # Try to get additional current conditions data - current_conditions = self.get_current_conditions(points_data) - if current_conditions and len(weather) < 120: - weather = f"{weather} {current_conditions}" + # Get weather forecast based on type + if forecast_type == "tomorrow": + forecast_periods, points_data = self.get_noaa_weather(lat, lon, return_periods=True) + if forecast_periods == self.ERROR_FETCHING_DATA: + return self.translate('commands.wx.error_fetching') + weather = self.format_tomorrow_forecast(forecast_periods) + elif forecast_type == "multiday": + forecast_periods, points_data = self.get_noaa_weather(lat, lon, return_periods=True) + if forecast_periods == self.ERROR_FETCHING_DATA: + return self.translate('commands.wx.error_fetching') + weather = self.format_multiday_forecast(forecast_periods, num_days) + else: # default + weather, points_data = self.get_noaa_weather(lat, lon) + if weather == self.ERROR_FETCHING_DATA: + return self.translate('commands.wx.error_fetching') + + # Try to get additional current conditions data + current_conditions = self.get_current_conditions(points_data) + if current_conditions and self._count_display_width(weather) < 120: + weather = f"{weather} {current_conditions}" - # Get weather alerts - alerts_result = self.get_weather_alerts_noaa(lat, lon) - if alerts_result == self.ERROR_FETCHING_DATA: - alerts_info = None - elif alerts_result == self.NO_ALERTS: - alerts_info = None - else: - full_alert_text, abbreviated_alert_text, alert_count = alerts_result - if alert_count > 0: - # Always send weather first, then alerts in separate message - self.logger.info(f"Found {alert_count} alerts - using two-message mode") - return ("multi_message", f"{location_prefix}{weather}", full_alert_text, alert_count) + # Get weather alerts (only for default forecast type to avoid cluttering) + if forecast_type == "default": + alerts_result = self.get_weather_alerts_noaa(lat, lon) + if alerts_result == self.ERROR_FETCHING_DATA: + alerts_info = None + elif alerts_result == self.NO_ALERTS: + alerts_info = None + else: + full_alert_text, abbreviated_alert_text, alert_count = alerts_result + if alert_count > 0: + # Always send weather first, then alerts in separate message + self.logger.info(f"Found {alert_count} alerts - using two-message mode") + return ("multi_message", f"{location_prefix}{weather}", full_alert_text, alert_count) return f"{location_prefix}{weather}" except Exception as e: self.logger.error(f"Error getting weather for {location_type} {location}: {e}") - return f"Error getting weather data: {e}" + return self.translate('commands.wx.error', error=str(e)) async def get_weather_for_zipcode(self, zipcode: str) -> str: """Get weather data for a specific zipcode (legacy method)""" @@ -378,8 +429,17 @@ class WxCommand(BaseCommand): self.logger.error(f"Error geocoding city {city}: {e}") return None, None, None - def get_noaa_weather(self, lat: float, lon: float) -> tuple: - """Get weather forecast from NOAA and return both weather string and points data""" + def get_noaa_weather(self, lat: float, lon: float, return_periods: bool = False) -> tuple: + """Get weather forecast from NOAA and return both weather string and points data + + Args: + lat: Latitude + lon: Longitude + return_periods: If True, return forecast periods array instead of formatted string + + Returns: + Tuple of (weather_string_or_periods, points_data) + """ try: # Get weather data from NOAA weather_api = f"https://api.weather.gov/points/{lat},{lon}" @@ -397,14 +457,20 @@ class WxCommand(BaseCommand): forecast_data = requests.get(forecast_url, timeout=self.url_timeout) if not forecast_data.ok: self.logger.warning("Error fetching weather forecast from NOAA") - return self.ERROR_FETCHING_DATA + return self.ERROR_FETCHING_DATA, None forecast_json = forecast_data.json() forecast = forecast_json['properties']['periods'] + # If return_periods is True, return the periods array directly + if return_periods: + if not forecast: + return self.ERROR_FETCHING_DATA, None + return forecast, weather_json + # Format the forecast - focus on current conditions and key info if not forecast: - return "No forecast data available" + return "No forecast data available", weather_json current = forecast[0] day_name = self.abbreviate_noaa(current['name']) @@ -433,75 +499,181 @@ class WxCommand(BaseCommand): if wind_dir: weather += f" {wind_dir}{wind_num}" - # Add humidity if available and space allows - if humidity and len(weather) < 90: + # Add humidity if available and space allows (using display width) + if humidity and self._count_display_width(weather) < 90: weather += f" {humidity}%RH" # Add precipitation chance if available and space allows - if precip_chance and len(weather) < 100: + if precip_chance and self._count_display_width(weather) < 100: weather += f" 🌦️{precip_chance}%" # Add UV index if available and space allows uv_index = self.extract_uv_index(detailed_forecast) - if uv_index and len(weather) < 110: + if uv_index and self._count_display_width(weather) < 110: weather += f" UV{uv_index}" # Add dew point if available and space allows dew_point = self.extract_dew_point(detailed_forecast) - if dew_point and len(weather) < 120: + if dew_point and self._count_display_width(weather) < 120: weather += f" 💧{dew_point}°" # Add visibility if available and space allows visibility = self.extract_visibility(detailed_forecast) - if visibility and len(weather) < 130: + if visibility and self._count_display_width(weather) < 130: weather += f" 👁️{visibility}mi" # Add precipitation probability if available and space allows precip_prob = self.extract_precip_probability(detailed_forecast) - if precip_prob and len(weather) < 140: + if precip_prob and self._count_display_width(weather) < 140: weather += f" 🌦️{precip_prob}%" # Add wind gusts if available and space allows wind_gusts = self.extract_wind_gusts(detailed_forecast) - if wind_gusts and len(weather) < 140: + if wind_gusts and self._count_display_width(weather) < 140: weather += f" 💨{wind_gusts}" - # Add next with high/low if available - if len(forecast) > 1: - next_period = forecast[1] - next_name = next_period.get('name', 'Next') - next_name_abbrev = self.abbreviate_noaa(next_name) - next_temp = next_period.get('temperature', '') - next_short = next_period.get('shortForecast', '') - next_detailed = next_period.get('detailedForecast', '') - next_wind_speed = next_period.get('windSpeed', '') - next_wind_direction = next_period.get('windDirection', '') + # Add next period (Tonight) and Tomorrow if available + # First, find Tonight and Tomorrow periods + tonight_period = None + tomorrow_period = None + current_period_name = current.get('name', '').lower() + is_current_tonight = 'tonight' in current_period_name + + for i, period in enumerate(forecast): + period_name = period.get('name', '').lower() + if 'tonight' in period_name and tonight_period is None: + tonight_period = (i, period) + elif 'tomorrow' in period_name and tomorrow_period is None: + tomorrow_period = (i, period) + + # If current is Tonight and we haven't found Tomorrow yet, look for next day's periods + if is_current_tonight and not tomorrow_period: + # Look for periods after Tonight (next day) + for i, period in enumerate(forecast): + if i > 0: # Skip current period + period_name = period.get('name', '').lower() + # Look for tomorrow, next day, or day names + if any(word in period_name for word in ['tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']): + tomorrow_period = (i, period) + break + + # Add Tonight if it's the immediate next period (and current is not already Tonight) + if tonight_period and tonight_period[0] == 1 and not is_current_tonight: + period = tonight_period[1] + period_name = self.abbreviate_noaa(period.get('name', 'Tonight')) + period_temp = period.get('temperature', '') + period_short = period.get('shortForecast', '') + period_detailed = period.get('detailedForecast', '') + period_wind_speed = period.get('windSpeed', '') + period_wind_direction = period.get('windDirection', '') - if next_temp and next_short: - # Try to get high/low for next - next_high_low = self.extract_high_low(next_detailed) + if period_temp and period_short: + # Try to get high/low + period_high_low = self.extract_high_low(period_detailed) - next_emoji = self.get_weather_emoji(next_short) - if next_high_low: - next_str = f" | {next_name_abbrev}: {next_emoji}{next_short} {next_high_low}" + period_emoji = self.get_weather_emoji(period_short) + if period_high_low: + period_str = f" | {period_name}: {period_emoji}{period_short} {period_high_low}" else: - next_str = f" | {next_name_abbrev}: {next_emoji}{next_short} {next_temp}°" + period_str = f" | {period_name}: {period_emoji}{period_short} {period_temp}°" - # Add next wind info if space allows - if next_wind_speed and next_wind_direction and len(weather + next_str) < 120: - import re - wind_match = re.search(r'(\d+)', next_wind_speed) - if wind_match: - wind_num = wind_match.group(1) - wind_dir = self.abbreviate_wind_direction(next_wind_direction) - if wind_dir: - wind_info = f" {wind_dir}{wind_num}" - if len(weather + next_str + wind_info) <= 130: - next_str += wind_info + # Add wind info if space allows (using display width) + if period_wind_speed and period_wind_direction: + test_str = weather + period_str + if self._count_display_width(test_str) < 120: + import re + wind_match = re.search(r'(\d+)', period_wind_speed) + if wind_match: + wind_num = wind_match.group(1) + wind_dir = self.abbreviate_wind_direction(period_wind_direction) + if wind_dir: + wind_info = f" {wind_dir}{wind_num}" + if self._count_display_width(test_str + wind_info) <= 130: + period_str += wind_info - # Only add if we have space - if len(weather + next_str) <= 130: # Leave room for alerts - weather += next_str + # Only add if we have space (using display width) + if self._count_display_width(weather + period_str) <= 130: # Leave room for alerts + weather += period_str + + # Always try to add Tomorrow if available (especially if current is Tonight) + # Prioritize adding Tomorrow when current is Tonight to use more of the 130 char limit + if tomorrow_period: + period = tomorrow_period[1] + period_name = self.abbreviate_noaa(period.get('name', 'Tomorrow')) + period_temp = period.get('temperature', '') + period_short = period.get('shortForecast', '') + period_detailed = period.get('detailedForecast', '') + period_wind_speed = period.get('windSpeed', '') + period_wind_direction = period.get('windDirection', '') + + if period_temp and period_short: + # Try to get high/low for tomorrow + period_high_low = self.extract_high_low(period_detailed) + + # Abbreviate forecast text if it's too long (especially when current is Tonight) + abbreviated_forecast = period_short + if is_current_tonight and len(period_short) > 20: + # Try to shorten forecast text to fit more info + # Remove transitional words and keep meaningful conditions + words = period_short.split() + # Transitional words to skip + transitions = {'then', 'and', 'or', 'becoming', 'followed', 'by', 'with'} + + # If there's a "then" pattern, take first condition and last significant condition + if 'then' in words: + then_index = words.index('then') + # Take first condition (before "then") + first_part = words[:then_index] + # Take last significant condition (after "then", skip small words) + if then_index + 1 < len(words): + last_part = [w for w in words[then_index + 1:] if w.lower() not in transitions] + # Combine: first condition + last significant condition (max 2 words) + if last_part: + abbreviated_forecast = ' '.join(first_part) + if len(last_part) <= 2: + abbreviated_forecast += ' ' + ' '.join(last_part) + else: + # Take last 2 words of the last part + abbreviated_forecast += ' ' + ' '.join(last_part[-2:]) + else: + abbreviated_forecast = ' '.join(first_part) + else: + abbreviated_forecast = ' '.join(first_part) + else: + # Filter out transitional words and take first meaningful words + meaningful_words = [w for w in words if w.lower() not in transitions] + if len(meaningful_words) > 3: + abbreviated_forecast = ' '.join(meaningful_words[:3]) + else: + abbreviated_forecast = ' '.join(meaningful_words) + + period_emoji = self.get_weather_emoji(period_short) + if period_high_low: + period_str = f" | {period_name}: {period_emoji}{abbreviated_forecast} {period_high_low}" + else: + period_str = f" | {period_name}: {period_emoji}{abbreviated_forecast} {period_temp}°" + + # Add wind info if space allows (using display width) + # Be more aggressive about adding wind when current is Tonight + wind_threshold = 115 if is_current_tonight else 120 + if period_wind_speed and period_wind_direction: + test_str = weather + period_str + if self._count_display_width(test_str) < wind_threshold: + import re + wind_match = re.search(r'(\d+)', period_wind_speed) + if wind_match: + wind_num = wind_match.group(1) + wind_dir = self.abbreviate_wind_direction(period_wind_direction) + if wind_dir: + wind_info = f" {wind_dir}{wind_num}" + if self._count_display_width(test_str + wind_info) <= 130: + period_str += wind_info + + # Only add if we have space (using display width, prioritize tomorrow) + # Be more aggressive when current is Tonight - use up to 128 chars (leave 2 for alerts) + max_chars = 128 if is_current_tonight else 130 + if self._count_display_width(weather + period_str) <= max_chars: + weather += period_str return weather, weather_json @@ -509,6 +681,319 @@ class WxCommand(BaseCommand): self.logger.error(f"Error fetching NOAA weather: {e}") return self.ERROR_FETCHING_DATA, None + def format_tomorrow_forecast(self, forecast: list) -> str: + """Format a detailed forecast for tomorrow""" + try: + # Find tomorrow's periods + # NOAA may use "Tomorrow", "Tomorrow Night" or day names like "Tuesday", "Tuesday Night" + tomorrow_periods = [] + tomorrow_day_name = (datetime.now() + timedelta(days=1)).strftime('%A') + + # First, try to find periods with "tomorrow" in the name + for period in forecast: + period_name = period.get('name', '').lower() + if 'tomorrow' in period_name: + tomorrow_periods.append(period) + + # If not found, look for tomorrow's day name (e.g., "Tuesday", "Tuesday Night") + if not tomorrow_periods: + for period in forecast: + period_name = period.get('name', '') + period_name_lower = period_name.lower() + # Check if it contains tomorrow's day name + if tomorrow_day_name.lower() in period_name_lower: + # Make sure it's not today + today_day_name = datetime.now().strftime('%A') + if today_day_name.lower() not in period_name_lower: + tomorrow_periods.append(period) + + # If still not found, find periods after "Tonight" (skip current day periods) + # This handles cases where NOAA uses generic day names + if not tomorrow_periods: + found_tonight = False + current_day_periods = 0 + for period in forecast: + period_name = period.get('name', '').lower() + # Count current day periods (Today, This Afternoon, Tonight, This Evening) + if any(word in period_name for word in ['today', 'this afternoon', 'this evening', 'tonight']): + current_day_periods += 1 + found_tonight = True + continue + if found_tonight: + # This should be tomorrow's period + tomorrow_periods.append(period) + # Stop after collecting tomorrow's day and night periods (usually 2) + if len(tomorrow_periods) >= 2: + break + + if not tomorrow_periods: + return self.translate('commands.wx.tomorrow_not_available') + + # Build detailed forecast for tomorrow + parts = [] + for period in tomorrow_periods: + period_name = self.abbreviate_noaa(period.get('name', 'Tomorrow')) + temp = period.get('temperature', '') + temp_unit = period.get('temperatureUnit', 'F') + short_forecast = period.get('shortForecast', '') + detailed_forecast = period.get('detailedForecast', '') + wind_speed = period.get('windSpeed', '') + wind_direction = period.get('windDirection', '') + + if not temp or not short_forecast: + continue + + # Create period string + emoji = self.get_weather_emoji(short_forecast) + period_str = f"{period_name}: {emoji}{short_forecast} {temp}°{temp_unit}" + + # Add wind info + if wind_speed and wind_direction: + import re + wind_match = re.search(r'(\d+)', wind_speed) + if wind_match: + wind_num = wind_match.group(1) + wind_dir = self.abbreviate_wind_direction(wind_direction) + if wind_dir: + period_str += f" {wind_dir}{wind_num}" + + # Try to extract high/low + high_low = self.extract_high_low(detailed_forecast) + if high_low and '°' not in period_str.split()[-1]: # Avoid duplicate temp + period_str = period_str.replace(f" {temp}°{temp_unit}", f" {high_low}") + + parts.append(period_str) + + if not parts: + return self.translate('commands.wx.tomorrow_not_available') + + return " | ".join(parts) + + except Exception as e: + self.logger.error(f"Error formatting tomorrow forecast: {e}") + return self.translate('commands.wx.tomorrow_error') + + def format_multiday_forecast(self, forecast: list, num_days: int = 7) -> str: + """Format a less detailed multi-day forecast summary""" + try: + # Group periods by day + days = {} + for period in forecast: + period_name = period.get('name', '') + period_name_lower = period_name.lower() + + # Skip if it's a time period (Tonight, This Afternoon, etc.) unless it's the only period for that day + # We want to focus on daily summaries + if any(word in period_name_lower for word in ['tonight', 'afternoon', 'morning', 'evening']): + # Only include if it's a named day (Monday, Tuesday, etc.) + day_name = None + for day in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']: + if day in period_name_lower: + day_name = day.capitalize() + break + + if not day_name: + continue + else: + # Extract day name + day_name = None + for day in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']: + if day in period_name_lower: + day_name = day.capitalize() + break + + if not day_name: + # Try to extract from "Tomorrow", "Today", etc. + if 'tomorrow' in period_name_lower: + tomorrow = datetime.now() + timedelta(days=1) + day_name = tomorrow.strftime('%A') + elif 'today' in period_name_lower: + day_name = datetime.now().strftime('%A') + else: + continue + + # Get temperature (prefer high/low if available) + temp = period.get('temperature', '') + temp_unit = period.get('temperatureUnit', 'F') + detailed_forecast = period.get('detailedForecast', '') + high_low = self.extract_high_low(detailed_forecast) + + if high_low: + temp_str = high_low + elif temp: + temp_str = f"{temp}°" + else: + continue + + # Get short forecast + short_forecast = period.get('shortForecast', '') + if not short_forecast: + continue + + # Store the best period for each day (prefer day periods over night) + if day_name not in days: + days[day_name] = { + 'temp': temp_str, + 'forecast': short_forecast, + 'is_day': 'night' not in period_name_lower and 'tonight' not in period_name_lower + } + else: + # Prefer day periods, but update if we have better temp info + if 'night' not in period_name_lower and 'tonight' not in period_name_lower: + days[day_name] = { + 'temp': temp_str, + 'forecast': short_forecast, + 'is_day': True + } + elif not days[day_name]['is_day']: + # Update night period if we don't have a day period + days[day_name]['temp'] = temp_str + days[day_name]['forecast'] = short_forecast + + if not days: + return self.translate('commands.wx.multiday_not_available', num_days=num_days) + + # Format as compact summary + parts = [] + day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + + # Get today's day name to start ordering + today = datetime.now().strftime('%A') + + # Reorder days starting from today + if today in day_order: + start_idx = day_order.index(today) + ordered_days = day_order[start_idx:] + day_order[:start_idx] + else: + ordered_days = day_order + + # Limit to requested number of days + # Map day names to 1-2 letter abbreviations + day_abbrev_map = { + 'Monday': 'M', + 'Tuesday': 'T', + 'Wednesday': 'W', + 'Thursday': 'Th', + 'Friday': 'F', + 'Saturday': 'Sa', + 'Sunday': 'Su' + } + + # Collect days up to num_days, starting from tomorrow (skip today) + days_collected = 0 + for day in ordered_days[1:]: # Skip today, start from tomorrow + if days_collected >= num_days: + break + if day in days: + day_data = days[day] + day_abbrev = day_abbrev_map.get(day, day[:2]) # Use 2-letter abbrev + emoji = self.get_weather_emoji(day_data['forecast']) + # Abbreviate forecast text + forecast_short = self.abbreviate_noaa(day_data['forecast']) + # Further shorten if needed to fit on one line (but be less aggressive) + if len(forecast_short) > 25: + forecast_short = forecast_short[:22] + "..." + + parts.append(f"{day_abbrev}: {emoji}{forecast_short} {day_data['temp']}") + days_collected += 1 + + if not parts: + return self.translate('commands.wx.multiday_not_available', num_days=num_days) + + # Join with newlines instead of pipes + result = "\n".join(parts) + + return result + + except Exception as e: + self.logger.error(f"Error formatting {num_days}-day forecast: {e}") + return self.translate('commands.wx.multiday_error', num_days=num_days) + + def _count_display_width(self, text: str) -> int: + """Count display width of text, accounting for emojis which may take 2 display units""" + import re + # Count regular characters + width = len(text) + # Emojis typically take 2 display units in terminals/clients + # Count emoji characters (basic emoji pattern) + emoji_pattern = re.compile( + "[" + "\U0001F600-\U0001F64F" # emoticons + "\U0001F300-\U0001F5FF" # symbols & pictographs + "\U0001F680-\U0001F6FF" # transport & map symbols + "\U0001F1E0-\U0001F1FF" # flags + "\U00002702-\U000027B0" # dingbats + "\U000024C2-\U0001F251" # enclosed characters + "]+", + flags=re.UNICODE + ) + emoji_matches = emoji_pattern.findall(text) + # Each emoji sequence adds 1 extra width unit (since len() already counts it as 1) + # So we add 1 for each emoji sequence to account for display width + width += len(emoji_matches) + return width + + async def _send_multiday_forecast(self, message: MeshMessage, forecast_text: str): + """Send multi-day forecast response, splitting into multiple messages if needed""" + import asyncio + + lines = forecast_text.split('\n') + + # Remove empty lines + lines = [line.strip() for line in lines if line.strip()] + + if not lines: + return + + # If single line and under 130 chars, send as-is + if self._count_display_width(forecast_text) <= 130: + await self.send_response(message, forecast_text) + return + + # Multi-line message - try to fit as many days as possible in one message + # Only split when necessary (message would exceed 130 chars) + current_message = "" + message_count = 0 + + for i, line in enumerate(lines): + if not line: + continue + + # Check if adding this line would exceed 130 characters (using display width) + if current_message: + test_message = current_message + "\n" + line + else: + test_message = line + + # Only split if message would exceed 130 chars (using display width) + if self._count_display_width(test_message) > 130: + # Send current message and start new one + if current_message: + await self.send_response(message, current_message) + message_count += 1 + # Wait between messages (same as other commands) + if i < len(lines): + await asyncio.sleep(2.0) + + current_message = line + else: + # Single line is too long, send it anyway (will be truncated by bot) + await self.send_response(message, line) + message_count += 1 + if i < len(lines) - 1: + await asyncio.sleep(2.0) + current_message = "" + else: + # Add line to current message (fits within 130 chars) + if current_message: + current_message += "\n" + line + else: + current_message = line + + # Send the last message if there's content + if current_message: + await self.send_response(message, current_message) + def get_weather_alerts_noaa(self, lat: float, lon: float) -> tuple: """Get weather alerts from NOAA""" try: diff --git a/modules/core.py b/modules/core.py index 3a72033..f8b8bbe 100644 --- a/modules/core.py +++ b/modules/core.py @@ -31,6 +31,10 @@ from .command_manager import CommandManager from .channel_manager import ChannelManager from .scheduler import MessageScheduler from .repeater_manager import RepeaterManager +from .db_manager import DBManager +from .i18n import Translator +from .solar_conditions import set_config +from .web_viewer.integration import WebViewerIntegration class MeshCoreBot: @@ -55,7 +59,6 @@ class MeshCoreBot: db_path = self.config.get('Bot', 'db_path', fallback='meshcore_bot.db') self.logger.info(f"Initializing database manager with database: {db_path}") try: - from .db_manager import DBManager self.db_manager = DBManager(self, db_path) self.logger.info("Database manager initialized successfully") except Exception as e: @@ -71,7 +74,6 @@ class MeshCoreBot: # Initialize web viewer integration (after database manager) try: - from .web_viewer.integration import WebViewerIntegration self.web_viewer_integration = WebViewerIntegration(self) self.logger.info("Web viewer integration initialized") @@ -89,6 +91,27 @@ class MeshCoreBot: self.config.getfloat('Bot', 'bot_tx_rate_limit_seconds', fallback=1.0) ) self.tx_delay_ms = self.config.getint('Bot', 'tx_delay_ms', fallback=250) + + # Initialize translator for localization BEFORE CommandManager + # This ensures translated keywords are available when commands are loaded + try: + language = self.config.get('Localization', 'language', fallback='en') + translation_path = self.config.get('Localization', 'translation_path', fallback='translations/') + self.translator = Translator(language, translation_path) + self.logger.info(f"Localization initialized: {language}") + except Exception as e: + self.logger.warning(f"Failed to initialize translator: {e}") + # Create a dummy translator that just returns keys + class DummyTranslator: + def translate(self, key, **kwargs): + return key + def get_value(self, key): + return None + self.translator = DummyTranslator() + + # Initialize solar conditions configuration + set_config(self.config) + self.message_handler = MessageHandler(self) self.command_manager = CommandManager(self) self.channel_manager = ChannelManager(self) @@ -103,9 +126,12 @@ class MeshCoreBot: self.logger.error(f"Failed to initialize repeater manager: {e}") raise - # Initialize solar conditions configuration - from .solar_conditions import set_config - set_config(self.config) + # Reload translated keywords for all commands now that translator is available + # This ensures keywords are loaded even if translator wasn't ready during command init + if hasattr(self, 'command_manager') and hasattr(self, 'translator'): + for cmd_name, cmd_instance in self.command_manager.commands.items(): + if hasattr(cmd_instance, '_load_translated_keywords'): + cmd_instance._load_translated_keywords() # Advert tracking self.last_advert_time = None @@ -255,14 +281,16 @@ admin_commands = repeater [Keywords] # Keyword-response pairs (keyword = response format) -# Available fields: {sender}, {connection_info}, {snr}, {rssi}, {timestamp}, {path} +# Available fields: {sender}, {connection_info}, {snr}, {rssi}, {timestamp}, {path}, {path_distance}, {firstlast_distance} # {sender}: Name/ID of message sender # {connection_info}: Path info, SNR, and RSSI combined (e.g., "01,5f (2 hops) | SNR: 15 dB | RSSI: -120 dBm") # {snr}: Signal-to-noise ratio in dB # {rssi}: Received signal strength indicator in dBm # {timestamp}: Message timestamp in HH:MM:SS format # {path}: Message routing path (e.g., "01,5f (2 hops)") -test = "ack {sender}{phrase_part} | {connection_info} | Received at: {timestamp}" +# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)") +# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing) +test = "ack [@{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}" ping = "Pong!" pong = "Ping!" help = "Bot Help: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass | Use 'help ' for details" diff --git a/modules/i18n.py b/modules/i18n.py new file mode 100644 index 0000000..bcb9da3 --- /dev/null +++ b/modules/i18n.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Internationalization (i18n) module for MeshCore Bot +Provides translation functionality for bot commands and responses +""" + +import json +import os +from pathlib import Path +from typing import Dict, Any, Optional + + +class Translator: + """Handles translation loading and lookup for the bot""" + + def __init__(self, language: str = 'en', translation_path: str = 'translations/'): + """ + Initialize translator + + Args: + language: Language code (e.g., 'en', 'es', 'es-MX', 'es-ES', 'fr', 'de') + Supports locale codes like 'es-MX' for Mexican Spanish or 'es-ES' for Spain Spanish + translation_path: Path to translation files directory + """ + self.language = language + self.translation_path = translation_path + self.base_language = self._extract_base_language(language) + self.translations: Dict[str, Any] = {} + self.fallback_translations: Dict[str, Any] = {} + self._load_translations() + + def _extract_base_language(self, language: str) -> str: + """ + Extract base language code from locale code + + Args: + language: Language code (e.g., 'en', 'es', 'es-MX', 'es-ES') + + Returns: + Base language code (e.g., 'es' from 'es-MX') + """ + # Handle locale codes like 'es-MX' or 'es_ES' + if '-' in language: + return language.split('-')[0] + elif '_' in language: + return language.split('_')[0] + return language + + def _load_translations(self): + """Load translation files with locale support""" + # Load default (English) first for final fallback + self.fallback_translations = self._load_file('en') + + # Load requested language with locale support + if self.language == 'en': + self.translations = self.fallback_translations + else: + # Load base language first (e.g., es.json) + base_translations = {} + if self.base_language != 'en': + base_translations = self._load_file(self.base_language) + + # Try to load locale-specific file (e.g., es-MX.json) + locale_translations = {} + if self.base_language != self.language: + locale_translations = self._load_file(self.language) + + # Merge: locale-specific overrides base language, base language overrides English + # First merge base into English + merged = self._merge_translations(base_translations, self.fallback_translations) + # Then merge locale-specific into the merged result + self.translations = self._merge_translations(locale_translations, merged) + + def _merge_translations(self, primary: Dict[str, Any], fallback: Dict[str, Any]) -> Dict[str, Any]: + """ + Merge primary translations with fallback, with primary taking precedence + + Args: + primary: Primary translation dictionary (may be empty) + fallback: Fallback translation dictionary + + Returns: + Merged dictionary with primary values overriding fallback + """ + if not primary: + return fallback.copy() + + result = fallback.copy() + + def merge_dict(target: dict, source: dict): + """Recursively merge source into target""" + for key, value in source.items(): + if key in target and isinstance(target[key], dict) and isinstance(value, dict): + merge_dict(target[key], value) + else: + target[key] = value + + merge_dict(result, primary) + return result + + def _load_file(self, lang: str) -> Dict[str, Any]: + """ + Load a single translation file + + Args: + lang: Language code + + Returns: + Dictionary of translations, empty dict if file not found + """ + file_path = Path(self.translation_path) / f"{lang}.json" + if not file_path.exists(): + return {} + + try: + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except json.JSONDecodeError as e: + print(f"Error parsing translation file {file_path}: {e}") + return {} + except Exception as e: + print(f"Error loading translation file {file_path}: {e}") + return {} + + def translate(self, key: str, **kwargs) -> str: + """ + Translate a key with optional formatting + + Args: + key: Dot-separated key path (e.g., 'commands.wx.usage') + **kwargs: Formatting parameters for string.format() + + Returns: + Translated string, or key if translation not found + """ + # Navigate through nested dict structure + keys = key.split('.') + value = self.translations + + # Try requested language first + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + # Fallback to English + value = self.fallback_translations + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + # Final fallback: return key (makes missing translations visible) + return key + + # If we got a string, format it if kwargs provided + if isinstance(value, str): + if kwargs: + try: + return value.format(**kwargs) + except (KeyError, ValueError) as e: + # If formatting fails, return unformatted string + return value + return value + + # If value is not a string, return the key + return key + + def reload(self): + """Reload translation files (useful for development)""" + self._load_translations() + + def get_available_languages(self) -> list: + """ + Get list of available language files + + Returns: + List of language codes (e.g., ['en', 'es', 'fr']) + """ + languages = [] + trans_path = Path(self.translation_path) + if trans_path.exists(): + for file in trans_path.glob('*.json'): + languages.append(file.stem) + return sorted(languages) + + def get_value(self, key: str) -> Any: + """ + Get a raw value from translations (can be string, list, dict, etc.) + + Args: + key: Dot-separated key path (e.g., 'commands.hacker.sudo_errors') + + Returns: + The value at the key path, or None if not found + """ + keys = key.split('.') + value = self.translations + + # Try requested language first + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + # Fallback to English + value = self.fallback_translations + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + # Not found + return None + break + + return value + diff --git a/modules/message_handler.py b/modules/message_handler.py index 89d372b..6ba7c60 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -13,6 +13,7 @@ from meshcore import EventType from .models import MeshMessage from .enums import PayloadType, PayloadVersion, RouteType, AdvertFlags, DeviceRole +from .utils import calculate_packet_hash class MessageHandler: @@ -40,6 +41,9 @@ class MessageHandler: self.rf_data_by_timestamp = {} # Index by timestamp for faster lookup self.rf_data_by_pubkey = {} # Index by pubkey for exact matches + # Multitest command listener (for collecting paths during listening window) + self.multitest_listener = None + self.logger.info(f"RF Data Correlation: timeout={self.rf_data_timeout}s, enhanced={self.enhanced_correlation}") async def handle_contact_message(self, event, metadata=None): @@ -469,17 +473,29 @@ class MessageHandler: # Extract routing information from raw packet if available routing_info = None + packet_hash = None if raw_hex: # Use extracted payload if available, otherwise use raw_hex decoded_packet = self.decode_meshcore_packet(raw_hex, extracted_payload) if decoded_packet: + # Calculate packet hash for this packet (useful for tracking same message via different paths) + # Ensure we use the numeric payload_type value (not enum or string) + payload_type_value = decoded_packet.get('payload_type', None) + if payload_type_value is not None: + # Handle enum.value if it's an enum + if hasattr(payload_type_value, 'value'): + payload_type_value = payload_type_value.value + payload_type_value = int(payload_type_value) + packet_hash = calculate_packet_hash(raw_hex, payload_type_value) + routing_info = { 'path_length': decoded_packet.get('path_len', 0), 'path_hex': decoded_packet.get('path_hex', ''), 'path_nodes': decoded_packet.get('path', []), 'route_type': decoded_packet.get('route_type_name', 'Unknown'), 'payload_length': payload_length, # Use the actual payload length - 'payload_type': decoded_packet.get('payload_type_name', 'Unknown') + 'payload_type': decoded_packet.get('payload_type_name', 'Unknown'), + 'packet_hash': packet_hash # Store hash for packet tracking } # Log the routing information for analysis @@ -518,7 +534,8 @@ class MessageHandler: 'raw_hex': raw_hex, # Full packet data 'payload': extracted_payload, # Extracted payload 'payload_length': payload_length, # Payload length - 'routing_info': routing_info # Extracted routing information + 'routing_info': routing_info, # Extracted routing information + 'packet_hash': packet_hash # Packet hash for tracking same message via different paths } self.recent_rf_data.append(rf_data) @@ -1512,6 +1529,13 @@ class MessageHandler: async def process_message(self, message: MeshMessage): """Process a received message""" + # Check if multitest is listening and notify it + if self.multitest_listener: + try: + self.multitest_listener.on_message_received(message) + except Exception as e: + self.logger.debug(f"Error notifying multitest listener: {e}") + # Record all messages in stats database FIRST (before any filtering) # This ensures we collect stats for all channels, not just monitored ones if 'stats' in self.bot.command_manager.commands: @@ -1538,7 +1562,12 @@ class MessageHandler: plugin_command_with_response_matched = False if keyword_matches: for keyword, response in keyword_matches: - self.logger.info(f"Keyword '{keyword}' matched, responding") + # Use translator if available for logging + if hasattr(self.bot, 'translator'): + log_msg = self.bot.translator.translate('messages.keyword_matched', keyword=keyword) + self.logger.info(log_msg) + else: + self.logger.info(f"Keyword '{keyword}' matched, responding") # Record command execution in stats database if 'stats' in self.bot.command_manager.commands: diff --git a/modules/repeater_manager.py b/modules/repeater_manager.py index 056a3bb..5a427c1 100644 --- a/modules/repeater_manager.py +++ b/modules/repeater_manager.py @@ -2094,49 +2094,51 @@ class RepeaterManager: self.logger.error(f"❌ meshcore.commands.remove_contact() method not found on meshcore object") device_removal_successful = False else: - # Use the MeshCore API: meshcore.commands.remove_contact(key) - # Try with public_key first (most reliable identifier), then contact_key as fallback - removal_attempted = False - for key_to_try, key_name in [(public_key, 'public_key'), (contact_key, 'contact_key')]: - if not key_to_try: - continue + # Use the MeshCore 2.2+ API: meshcore.commands.remove_contact(key) + # remove_contact accepts: str (hex public key), bytes, or dict (contact object) + removal_keys_to_try = [] + + # Add public_key if it's a valid 64-char hex string + if public_key and len(public_key) == 64: + removal_keys_to_try.append(('public_key', public_key)) + + # Add contact_key if it's different and valid + if contact_key and contact_key != public_key and len(contact_key) == 64: + removal_keys_to_try.append(('contact_key', contact_key)) + + # Try each key format + for key_name, key_to_try in removal_keys_to_try: try: self.logger.debug(f"Calling meshcore.commands.remove_contact({key_name}='{key_to_try[:16]}...')") result = await asyncio.wait_for( self.bot.meshcore.commands.remove_contact(key_to_try), timeout=30.0 ) - removal_attempted = True - # Check if result indicates success - # Result could be: True, EventType.OK, or an event object with .type - if result is True: + # Check if removal was successful (meshcore 2.2+ returns Event object) + if result.type == EventType.OK: device_removal_successful = True - self.logger.info(f"✅ remove_contact returned True - removal successful") + self.logger.info(f"✅ Successfully removed contact '{contact_name}' via meshcore 2.2+ API using {key_name}") break - elif hasattr(result, 'type') and result.type == EventType.OK: - device_removal_successful = True - self.logger.info(f"✅ remove_contact returned EventType.OK - removal successful") - break - elif hasattr(result, 'type') and result.type == EventType.ERROR: + elif result.type == EventType.ERROR: error_code = result.payload.get('error_code', 'unknown') if hasattr(result, 'payload') else 'unknown' if error_code == 2: # Contact not found (already removed) - treat as success device_removal_successful = True - self.logger.info(f"✅ Contact not found (already removed) - treating as success") + self.logger.info(f"✅ Contact '{contact_name}' not found (already removed) - treating as success") break else: - self.logger.debug(f"remove_contact returned error_code {error_code}, trying next key...") + self.logger.debug(f"remove_contact({key_name}) returned error_code {error_code}, trying next key...") continue else: - self.logger.debug(f"remove_contact returned unexpected result: {result}, trying next key...") + self.logger.debug(f"remove_contact({key_name}) returned unexpected event type: {result.type}") continue except Exception as e: self.logger.debug(f"remove_contact({key_name}) failed: {type(e).__name__}: {e}, trying next key...") continue - if not removal_attempted: + if not removal_keys_to_try: self.logger.error(f"❌ No valid key available for remove_contact") device_removal_successful = False elif not device_removal_successful: @@ -2149,27 +2151,47 @@ class RepeaterManager: # Verify the contact was actually removed by checking contacts list if device_removal_successful: + # First, manually remove from local cache since the API reported success + # This prevents false negatives if the device hasn't updated its list yet + if contact_key in self.bot.meshcore.contacts: + del self.bot.meshcore.contacts[contact_key] + self.logger.debug(f"Removed '{contact_name}' from local contacts cache") + # Wait a moment for the device to process the removal + await asyncio.sleep(2.0) + + # Refresh contacts from device to get latest state + try: + # Use the proper meshcore API to refresh contacts + if hasattr(self.bot.meshcore, 'ensure_contacts'): + await self.bot.meshcore.ensure_contacts(follow=True) + elif hasattr(self.bot.meshcore.commands, 'get_contacts'): + await self.bot.meshcore.commands.get_contacts(timeout=10.0) + else: + # Fallback: use CLI to refresh + from meshcore_cli.meshcore_cli import next_cmd + await asyncio.wait_for( + next_cmd(self.bot.meshcore, ["contacts"]), + timeout=15.0 + ) + self.logger.debug(f"Refreshed contacts from device") + except Exception as e: + self.logger.debug(f"Could not refresh contacts from device: {e}") + + # Wait a bit more after refresh for events to process await asyncio.sleep(1.0) - # Refresh contacts to get latest state - try: - if hasattr(self.bot.meshcore, 'refresh_contacts'): - await self.bot.meshcore.refresh_contacts() - elif hasattr(self.bot.meshcore, 'update_contacts'): - await self.bot.meshcore.update_contacts() - except Exception as e: - self.logger.debug(f"Could not refresh contacts: {e}") - - # Check if contact still exists + # Check if contact still exists after refresh contact_still_exists = any( contact_data.get('public_key', key) == public_key for key, contact_data in self.bot.meshcore.contacts.items() ) if contact_still_exists: - self.logger.warning(f"⚠️ Removal reported success but contact '{contact_name}' still exists in contacts list") - device_removal_successful = False + self.logger.warning(f"⚠️ Removal reported success but contact '{contact_name}' still exists in contacts list after refresh") + # Don't mark as failed - the API said it succeeded, device might just be slow + # We'll trust the API response and mark as successful + self.logger.info(f"⚠️ Trusting API success response despite contact still in list (device may be slow to update)") else: self.logger.debug(f"✅ Verified: contact '{contact_name}' successfully removed from device") @@ -2400,38 +2422,61 @@ class RepeaterManager: except Exception as e: self.logger.warning(f"Direct removal failed: {e}") - # Method 2: Try using meshcore commands if available + # Method 2: Try using meshcore commands if available (meshcore 2.2+ API) if not device_removal_successful and hasattr(self.bot.meshcore, 'commands'): try: - self.logger.info(f"Method 2: Attempting removal via meshcore commands...") + self.logger.info(f"Method 2: Attempting removal via meshcore 2.2+ API...") # Check if there's a remove_contact method if hasattr(self.bot.meshcore.commands, 'remove_contact'): # Try different parameter combinations - try: - # Try with contact_data - result = await self.bot.meshcore.commands.remove_contact(contact_data) - if result: - self.logger.info(f"Successfully removed contact '{contact_name}' via meshcore commands (contact_data)") - device_removal_successful = True - except Exception as e1: - self.logger.debug(f"remove_contact(contact_data) failed: {e1}") + # remove_contact accepts: str (hex public key), bytes, or dict (contact object) + removal_keys_to_try = [] + + # Add public_key if it's a valid 64-char hex string + if public_key and len(public_key) == 64: + removal_keys_to_try.append(('public_key', public_key)) + + # Add contact_data dict (meshcore 2.2+ supports dict with public_key field) + if contact_data and isinstance(contact_data, dict): + removal_keys_to_try.append(('contact_data', contact_data)) + + # Add contact_key if it's different and valid + if contact_key and contact_key != public_key and len(contact_key) == 64: + removal_keys_to_try.append(('contact_key', contact_key)) + + for key_name, key_to_try in removal_keys_to_try: try: - # Try with public_key - result = await self.bot.meshcore.commands.remove_contact(public_key) - if result: - self.logger.info(f"Successfully removed contact '{contact_name}' via meshcore commands (public_key)") + self.logger.debug(f"Trying remove_contact with {key_name}...") + result = await asyncio.wait_for( + self.bot.meshcore.commands.remove_contact(key_to_try), + timeout=30.0 + ) + + # Check if removal was successful (meshcore 2.2+ returns Event object) + if result.type == EventType.OK: device_removal_successful = True - except Exception as e2: - self.logger.debug(f"remove_contact(public_key) failed: {e2}") - try: - # Try with contact_key - result = await self.bot.meshcore.commands.remove_contact(contact_key) - if result: - self.logger.info(f"Successfully removed contact '{contact_name}' via meshcore commands (contact_key)") + self.logger.info(f"✅ Successfully removed contact '{contact_name}' via meshcore 2.2+ API using {key_name}") + break + elif result.type == EventType.ERROR: + error_code = result.payload.get('error_code', 'unknown') if hasattr(result, 'payload') else 'unknown' + # Error code 2 typically means "contact not found" - treat as success + if error_code == 2: device_removal_successful = True - except Exception as e3: - self.logger.debug(f"remove_contact(contact_key) failed: {e3}") - self.logger.warning(f"All meshcore commands remove_contact attempts failed") + self.logger.info(f"✅ Contact '{contact_name}' not found (already removed) - treating as success") + break + else: + self.logger.debug(f"remove_contact({key_name}) returned error_code {error_code}, trying next key...") + continue + else: + self.logger.debug(f"remove_contact({key_name}) returned unexpected event type: {result.type}") + continue + + except Exception as e: + self.logger.debug(f"remove_contact({key_name}) failed: {type(e).__name__}: {e}, trying next key...") + continue + + if not device_removal_successful: + self.logger.warning(f"All meshcore 2.2+ API remove_contact attempts failed for '{contact_name}'") else: self.logger.info("No remove_contact method found in meshcore commands") except Exception as e: @@ -2497,39 +2542,46 @@ class RepeaterManager: # Verify removal and ensure persistence if device_removal_successful: - import asyncio - await asyncio.sleep(3) # Give device more time to process and save + # First, manually remove from local cache since the API reported success + # This prevents false negatives if the device hasn't updated its list yet + if contact_key in self.bot.meshcore.contacts: + del self.bot.meshcore.contacts[contact_key] + self.logger.debug(f"Removed '{contact_name}' from local contacts cache") - # Try to force device to save changes + await asyncio.sleep(2.0) # Give device time to process and save + + # Try to refresh contacts from device to get latest state try: - self.logger.info(f"Attempting to force device to save contact changes...") - from meshcore_cli.meshcore_cli import next_cmd - - # Try to refresh contacts from device - try: - self.logger.info("Refreshing contacts from device...") + self.logger.debug("Refreshing contacts from device...") + # Use the proper meshcore API to refresh contacts + if hasattr(self.bot.meshcore, 'ensure_contacts'): + await self.bot.meshcore.ensure_contacts(follow=True) + elif hasattr(self.bot.meshcore.commands, 'get_contacts'): + await self.bot.meshcore.commands.get_contacts(timeout=10.0) + else: + # Fallback: use CLI to refresh + from meshcore_cli.meshcore_cli import next_cmd await asyncio.wait_for( next_cmd(self.bot.meshcore, ["contacts"]), timeout=15.0 ) - self.logger.info("Contacts refreshed from device") - except Exception as e: - self.logger.warning(f"Failed to refresh contacts: {e}") - + self.logger.debug("Contacts refreshed from device") except Exception as e: - self.logger.warning(f"Failed to force device persistence: {e}") + self.logger.debug(f"Could not refresh contacts from device: {e}") - # Wait a bit more after refresh - await asyncio.sleep(1) + # Wait a bit more after refresh for events to process + await asyncio.sleep(1.0) # Check if contact still exists in the bot's memory after refresh contact_still_exists = contact_key in self.bot.meshcore.contacts if contact_still_exists: - self.logger.warning(f"Contact '{contact_name}' still exists after removal and refresh - removal may have failed") - device_removal_successful = False + self.logger.warning(f"⚠️ Removal reported success but contact '{contact_name}' still exists in contacts list after refresh") + # Don't mark as failed - the API said it succeeded, device might just be slow + # We'll trust the API response and mark as successful + self.logger.info(f"⚠️ Trusting API success response despite contact still in list (device may be slow to update)") else: - self.logger.info(f"Verified: Contact '{contact_name}' successfully removed from device") + self.logger.info(f"✅ Verified: Contact '{contact_name}' successfully removed from device") except Exception as e: self.logger.error(f"Failed to remove contact '{contact_name}' from device: {e}") diff --git a/modules/utils.py b/modules/utils.py index 65d6145..dd7f97f 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -5,6 +5,7 @@ Shared helper functions used across multiple modules """ import re +import hashlib from typing import Optional @@ -250,3 +251,115 @@ def get_major_city_queries(city: str, state_abbr: Optional[str] = None) -> list: # Not a major city - return empty list (caller should use standard geocoding) return [] + + +def calculate_packet_hash(raw_hex: str, payload_type: int = None) -> str: + """ + Calculate hash for packet identification - based on packet.cpp + Packet hashes are unique to the originally sent message, allowing + identification of the same message arriving via different paths. + + Args: + raw_hex: Raw packet data as hex string + payload_type: Optional payload type as integer (if None, extracted from header) + Must be numeric value (0-15), not enum or string + + Returns: + 16-character hex string (8 bytes) in uppercase, or "0000000000000000" on error + """ + try: + # Parse the packet to extract payload type and payload data + byte_data = bytes.fromhex(raw_hex) + header = byte_data[0] + + # Get payload type from header (bits 2-5) + if payload_type is None: + payload_type = (header >> 2) & 0x0F + else: + # Ensure payload_type is an integer (handle enum.value if passed) + if hasattr(payload_type, 'value'): + payload_type = payload_type.value + payload_type = int(payload_type) & 0x0F # Ensure it's 0-15 + + # Check if transport codes are present + route_type = header & 0x03 + has_transport = route_type in [0x00, 0x03] # TRANSPORT_FLOOD or TRANSPORT_DIRECT + + # Calculate path length offset dynamically based on transport codes + offset = 1 # After header + if has_transport: + offset += 4 # Skip 4 bytes of transport codes + + # Validate we have enough bytes for path_len + if len(byte_data) <= offset: + return "0000000000000000" + + # Read path_len (1 byte on wire, but stored as uint16_t in C++) + path_len = byte_data[offset] + offset += 1 + + # Validate we have enough bytes for the path + if len(byte_data) < offset + path_len: + return "0000000000000000" + + # Skip past the path to get to payload + payload_start = offset + path_len + + # Validate we have payload data + if len(byte_data) <= payload_start: + return "0000000000000000" + + payload_data = byte_data[payload_start:] + + # Calculate hash exactly like MeshCore Packet::calculatePacketHash(): + # 1. Payload type (1 byte) + # 2. Path length (2 bytes as uint16_t, little-endian) - ONLY for TRACE packets (type 9) + # 3. Payload data + hash_obj = hashlib.sha256() + hash_obj.update(bytes([payload_type])) + + if payload_type == 9: # PAYLOAD_TYPE_TRACE + # C++ does: sha.update(&path_len, sizeof(path_len)) + # path_len is uint16_t, so sizeof(path_len) = 2 bytes + # Convert path_len to 2-byte little-endian uint16_t + hash_obj.update(path_len.to_bytes(2, byteorder='little')) + + hash_obj.update(payload_data) + + # Return first 16 hex characters (8 bytes) in uppercase + return hash_obj.hexdigest()[:16].upper() + except Exception as e: + # Return default hash on error (caller should handle logging) + return "0000000000000000" + + +def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """ + Calculate haversine distance between two points in kilometers. + + Args: + lat1: Latitude of first point in degrees + lon1: Longitude of first point in degrees + lat2: Latitude of second point in degrees + lon2: Longitude of second point in degrees + + Returns: + Distance in kilometers + """ + import math + + # Convert latitude and longitude from degrees to radians + lat1_rad = math.radians(lat1) + lon1_rad = math.radians(lon1) + lat2_rad = math.radians(lat2) + lon2_rad = math.radians(lon2) + + # Haversine formula + dlat = lat2_rad - lat1_rad + dlon = lon2_rad - lon1_rad + a = math.sin(dlat/2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon/2)**2 + c = 2 * math.asin(math.sqrt(a)) + + # Earth's radius in kilometers + earth_radius = 6371.0 + return earth_radius * c diff --git a/modules/web_viewer/app.py b/modules/web_viewer/app.py index f7d69e6..8251441 100644 --- a/modules/web_viewer/app.py +++ b/modules/web_viewer/app.py @@ -138,6 +138,9 @@ class BotDataViewer: # Initialize repeater manager for geocoding functionality self.repeater_manager = RepeaterManager(minimal_bot) + # Initialize packet_stream table for real-time monitoring + self._init_packet_stream_table() + # Store database paths for direct connection self.db_path = self.db_path self.repeater_db_path = self.repeater_db_path @@ -146,6 +149,47 @@ class BotDataViewer: self.logger.error(f"Failed to initialize databases: {e}") raise + def _init_packet_stream_table(self): + """Initialize the packet_stream table in bot_data.db""" + try: + # Get database path from config + db_path = self.config.get('Database', 'path', fallback='bot_data.db') + + # Connect to database and create table if it doesn't exist + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Create packet_stream table with schema matching the INSERT statements + cursor.execute(''' + CREATE TABLE IF NOT EXISTS packet_stream ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL NOT NULL, + data TEXT NOT NULL, + type TEXT NOT NULL + ) + ''') + + # Create index on timestamp for faster queries + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_packet_stream_timestamp + ON packet_stream(timestamp) + ''') + + # Create index on type for filtering by type + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_packet_stream_type + ON packet_stream(type) + ''') + + conn.commit() + conn.close() + + self.logger.info(f"Initialized packet_stream table in {db_path}") + + except Exception as e: + self.logger.error(f"Failed to initialize packet_stream table: {e}") + # Don't raise - allow web viewer to continue even if table init fails + def _get_db_connection(self): """Get database connection - create new connection for each request to avoid threading issues""" try: @@ -444,8 +488,7 @@ class BotDataViewer: 'subscribed_packets': False } - # Send welcome message - emit('status', {'message': 'Connected to MeshCore Bot Data Viewer'}) + # Connection status is shown via the green indicator in the navbar, no toast needed self.logger.info(f"Client {client_id} connected. Total clients: {len(self.connected_clients)}") @self.socketio.on('disconnect') diff --git a/modules/web_viewer/integration.py b/modules/web_viewer/integration.py index 0b8692b..e8ec6d1 100644 --- a/modules/web_viewer/integration.py +++ b/modules/web_viewer/integration.py @@ -19,12 +19,58 @@ class BotIntegration: self.circuit_breaker_open = False self.circuit_breaker_failures = 0 self.is_shutting_down = False + # Initialize the packet_stream table + self._init_packet_stream_table() def reset_circuit_breaker(self): """Reset the circuit breaker""" self.circuit_breaker_open = False self.circuit_breaker_failures = 0 + def _init_packet_stream_table(self): + """Initialize the packet_stream table in bot_data.db""" + try: + import sqlite3 + + # Get database path from config + db_path = self.bot.config.get('Database', 'path', fallback='bot_data.db') + + # Connect to database and create table if it doesn't exist + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Create packet_stream table with schema matching the INSERT statements + cursor.execute(''' + CREATE TABLE IF NOT EXISTS packet_stream ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp REAL NOT NULL, + data TEXT NOT NULL, + type TEXT NOT NULL + ) + ''') + + # Create index on timestamp for faster queries + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_packet_stream_timestamp + ON packet_stream(timestamp) + ''') + + # Create index on type for filtering by type + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_packet_stream_type + ON packet_stream(type) + ''') + + conn.commit() + conn.close() + + self.bot.logger.info(f"Initialized packet_stream table in {db_path}") + + except Exception as e: + self.bot.logger.error(f"Failed to initialize packet_stream table: {e}") + # Don't raise - allow bot to continue even if table init fails + # The error will be caught when trying to insert data + def capture_full_packet_data(self, packet_data): """Capture full packet data and store in database for web viewer""" try: diff --git a/translations/de.json b/translations/de.json new file mode 100644 index 0000000..16bdcfa --- /dev/null +++ b/translations/de.json @@ -0,0 +1,1049 @@ +{ + "keywords": { + "help": [ + "help", + "hilfe" + ], + "ping": [ + "ping" + ], + "test": [ + "test", + "t" + ], + "wx": [ + "wx", + "weather", + "wetter", + "wxa", + "wxalert" + ], + "gwx": [ + "gwx", + "globalweather", + "gwxa" + ], + "aqi": [ + "aqi", + "air", + "airquality", + "air_quality", + "luft", + "luftqualität" + ], + "solar": [ + "solar" + ], + "sun": [ + "sun", + "sonne" + ], + "moon": [ + "moon", + "mond" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass" + ], + "sports": [ + "sports", + "score", + "scores", + "sport" + ], + "stats": [ + "stats", + "statistik" + ], + "channels": [ + "channels", + "channel", + "kanäle" + ], + "path": [ + "path", + "decode", + "route", + "pfad" + ], + "prefix": [ + "prefix", + "repeater", + "lookup" + ], + "repeater": [ + "repeater", + "repeaters", + "rp" + ], + "solarforecast": [ + "sf", + "solarforecast" + ], + "dice": [ + "dice", + "würfel" + ], + "roll": [ + "roll" + ], + "joke": [ + "joke", + "jokes", + "witze" + ], + "dadjoke": [ + "dadjoke", + "dad joke", + "dadjokes", + "dad jokes", + "vaterwitz" + ], + "catfact": [ + "catfact", + "cat", + "meow", + "purr", + "kitten", + "katze", + "miau" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "hello", + "hi", + "hey", + "howdy", + "greetings", + "salutations", + "good morning", + "good afternoon", + "good evening", + "good night", + "yo", + "sup", + "whats up", + "what's up", + "morning", + "afternoon", + "evening", + "night", + "gday", + "g'day", + "hola", + "bonjour", + "ciao", + "namaste", + "aloha", + "shalom", + "konnichiwa", + "guten tag", + "buenos dias", + "buenas tardes", + "buenas noches", + "hallo", + "moin", + "servus", + "grüß gott" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "wv" + ], + "cmd": [ + "cmd", + "commands", + "befehle" + ], + "advert": [ + "advert" + ] + }, + "commands": { + "help": { + "description": "Zeigt Befehle an. Nutze 'help ' für Details.", + "general": "**MeshCore Bot Hilfe**\n\n**Verfügbare Befehle:**\n{commands_list}", + "unknown": "**Unbekannter Befehl: '{command}'**\n\nVerfügbare Befehle:\n{available}", + "specific": "**Hilfe für '{command}':**\n{help_text}", + "no_help": "Keine Hilfe verfügbar", + "usage_examples": "\n**Verwendungsbeispiele:**\n• `help @` - Hilfe für @{string} Syntax\n• `help t` - Hilfe für t {string} Syntax\n• `help advert` - Hilfe für advert Befehl\n• `help test` - Hilfe für test Befehl\n• `help ping` - Hilfe für ping Befehl", + "custom_syntax": "\n**Spezielle Syntax:**\n• `t phrase` - Bestätigung mit Phrase (Kanäle & DMs)\n• `@{string}` - Bestätigung mit String (nur DMs)\n• `advert` - Flood-Anzeige senden (nur DMs, 1h Wartezeit)" + }, + "ping": { + "description": "Antwortet auf 'ping' mit 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Antwortet auf 'test' oder 't' mit Verbindungsinfo", + "help": "Antwortet auf 'test' oder 't' mit Verbindungsinfo" + }, + "wx": { + "description": "Wetterinfo für PLZ abrufen (Verwendung: wx 12345)", + "usage": "Verwendung: wx - Beispiel: wx 12345 oder wx seattle oder wx paris, tx", + "error_fetching": "Fehler beim Abruf der Wetterdaten von NOAA", + "no_location_zipcode": "Ort für PLZ {location} nicht gefunden", + "no_location_city": "Stadt '{location}' in {state} nicht gefunden", + "error": "Fehler beim Abruf der Wetterdaten: {error}", + "alerts": "{count} Warnungen: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Wetterinfo für beliebigen Ort abrufen (Verwendung: gwx Tokyo)", + "help": "Verwendung: gwx - Wetter für jeden Ort weltweit (Stadt, Land oder Koordinaten)", + "usage": "Verwendung: gwx - Beispiel: gwx Tokyo oder gwx Paris, France", + "error_fetching": "Fehler beim Abruf der Wetterdaten", + "error_fetching_api": "Fehler beim Abruf der Wetterdaten von Open-Meteo", + "no_location": "Ort '{location}' nicht gefunden", + "error": "Fehler beim Abruf der Wetterdaten: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Heute", + "tonight": "Heute Nacht", + "tomorrow": "Morgen" + }, + "weather_descriptions": { + "0": "Klar", + "1": "Meist klar", + "2": "Teilweise bewölkt", + "3": "Bedeckt", + "45": "Neblig", + "48": "Neblig", + "51": "Leichter Nieselregen", + "53": "Nieselregen", + "55": "Starker Nieselregen", + "56": "Leichter gefrierender Nieselregen", + "57": "Gefrierender Nieselregen", + "61": "Leichter Regen", + "63": "Regen", + "65": "Starker Regen", + "66": "Leichter gefrierender Regen", + "67": "Gefrierender Regen", + "71": "Leichter Schnee", + "73": "Schnee", + "75": "Starker Schnee", + "77": "Schneegriesel", + "80": "Leichte Schauer", + "81": "Schauer", + "82": "Starke Schauer", + "85": "Leichte Schneeschauer", + "86": "Schneeschauer", + "95": "Gewitter", + "96": "Gewitter m/Hagel", + "99": "Schweres Gewitter", + "unknown": "Unbekannt" + }, + "warnings": { + "extreme_heat": "⚠️ Extreme Hitze", + "extreme_cold": "⚠️ Extreme Kälte", + "heavy_rain": "⚠️ Starkregen", + "thunderstorms": "⚠️ Gewitter", + "heavy_snow": "⚠️ Starker Schneefall", + "high_winds": "⚠️ Starker Wind ({wind_speed} mph)" + } + }, + "sports": { + "description": "Sportergebnisse und -termine abrufen (Verwendung: sports [team/liga])", + "help": "Sportergebnisse & -termine abrufen. 'sports' für Standard-Teams, 'sports [team]' für spezifisches Team oder 'sports [liga]' für Liga-Spiele.", + "error_fetching": "Fehler beim Abruf der Sportdaten", + "error_fetching_league": "Fehler beim Abruf der {sport} Daten", + "error_fetching_team": "Fehler beim Abruf der Daten für {team}", + "no_default_teams": "Keine Standard-Teams konfiguriert", + "no_games_default": "Keine Spiele für Standard-Teams gefunden", + "no_teams_city": "Keine Teams für {city} gefunden", + "no_games_city": "Keine Spiele für {city} Teams gefunden", + "no_games_league": "Keine Spiele für {sport} gefunden", + "no_games_team": "Keine Spiele für {team} gefunden", + "team_not_found": "Team/Liga '{team}' nicht gefunden. Versuche: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, usw." + }, + "stats": { + "description": "Statistiken der letzten 24 Stunden anzeigen. Nutze 'stats messages', 'stats channels' oder 'stats paths' für spezifische Stats.", + "help": "24-Stunden Bot-Statistiken anzeigen. Befehle: 'stats' (Basis), 'stats messages' (Bot-Nutzer), 'stats channels' (Kanal-Aktivität), 'stats paths' (längste Pfade)", + "disabled": "Stats-Befehl ist deaktiviert", + "unknown_subcommand": "Unbekannt: {subcommand}. Nutze 'stats', 'stats messages', 'stats channels' oder 'stats paths'", + "error": "Fehler beim Abruf der Statistiken: {error}", + "error_bot_users": "Fehler beim Abruf der Bot-Nutzer-Stats: {error}", + "error_channels": "Fehler beim Abruf der Kanal-Stats: {error}", + "error_paths": "Fehler beim Abruf der Pfad-Stats: {error}", + "basic": { + "header": "Bot Stats (24h):", + "commands": "Befehle: {count} | Antworten: {replies}", + "top_command": "Top Befehl: {command}", + "top_user": "Top Nutzer: {user}", + "none": "Keine" + }, + "users": { + "header": "Bot-Nutzer (24h):", + "none": "Keine Bot-Nutzer" + }, + "channels": { + "header": "Top Kanäle (24h)", + "none": "Keine Kanal-Aktivität", + "msg_singular": "Nachricht", + "msg_plural": "Nachrichten", + "user_singular": "Nutzer", + "user_plural": "Nutzer", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Keine Pfad-Daten", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Solarmodul-Produktionsprognose abrufen (Verwendung: sf [panelgröße] [azimut, 0=süd] [winkel])", + "usage": "Verwendung: sf [watt] [azimut, 0=süd] [winkel] - Beispiel: sf seattle 10 0 37", + "usage_short": "Verwendung: sf [W] [azimut] [winkel] - Beispiel: sf seattle 10", + "panel_size_range": "Panelgröße muss 1-1000W sein", + "azimuth_range": "Azimut muss -180 bis 180 sein (0=süd)", + "angle_range": "Winkel muss 0-90 Grad sein", + "no_location": "Ort nicht gefunden: {location}", + "error_fetching": "Fehler beim Abruf der Prognose", + "no_data": "Keine Prognosedaten", + "error": "Fehler: {error}", + "rate_limit": "Rate-Limit: Versuche es in 5 Min. erneut", + "labels": { + "today": "Heute:{wh:.0f}Wh", + "tomorrow": "Morgen:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Peak: {watts:.0f}W@{time}", + "peak_past": "Peak: {watts:.0f}W (vorbei)", + "hours_util": " ({hours}h,{util:.0f}%Ausl.)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Sendet Flood-Anzeige (nur DM, 1h Wartezeit)", + "cooldown_active": "Anzeigen-Wartezeit aktiv. Bitte warte noch {minutes} Minuten vor der nächsten Anfrage.", + "success": "Flood-Anzeige erfolgreich gesendet!", + "error": "Fehler beim Senden der Flood-Anzeige: {error}" + }, + "path": { + "description": "Hex-Pfaddaten dekodieren um beteiligte Repeater im Nachrichten-Routing anzuzeigen", + "help": "Path: path [hex] - Pfad dekodieren um Repeater anzuzeigen. Nutze !path allein für letzten Nachrichtenpfad oder path [7e,01] für spezifischen Pfad.", + "no_valid_hex": "❌ Keine gültigen Hex-Werte in Pfaddaten gefunden. Nutze Format wie: 11,98,a4,49,cd,5f,01", + "no_path": "❌ Keine Pfadinformation in aktueller Nachricht verfügbar", + "error": "Fehler bei Pfadverarbeitung: {error}", + "error_decoding": "❌ Fehler beim Dekodieren des Pfads: {error}", + "error_extracting": "❌ Fehler beim Extrahieren des Pfads aus aktueller Nachricht: {error}", + "path_prefix": "📡 Pfad: {path_string}", + "direct_connection": "📡 Direkte Verbindung (0 Hops)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Unbekannt", + "node_collision": "{node_id}: {matches} Repeater", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Unbekannt" + }, + "prefix": { + "description": "Repeater nach zweistelligem Präfix suchen (z.B. 'prefix 1A')", + "help_api": "Repeater nach Präfix suchen{location_note}. Verwendung: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Repeater nach Präfix suchen{location_note}. Verwendung: 'prefix 1A', 'prefix 1A all', 'prefix free'. (Nur lokale DB)", + "location_note": " (mit Stadtnamen)", + "refresh_not_available": "❌ Aktualisierung nicht verfügbar - keine API-URL konfiguriert. Nur lokale Datenbank.", + "cache_refreshed": "🔄 Repeater-Präfix-Cache aktualisiert!", + "unable_determine_free": "❌ Kann freie Präfixe nicht ermitteln. Versuche zuerst 'prefix refresh'.", + "invalid_format": "❌ Ungültiges Präfix-Format. Nutze zwei Zeichen (z.B. prefix 1A)", + "no_repeaters_found": "❌ Keine Repeater mit Präfix '{prefix}' gefunden", + "no_free_prefixes": "❌ Keine freien Präfixe gefunden (alle 254 gültigen Präfixe sind belegt)", + "available_prefixes": "Verfügbare Präfixe ({shown} von {total} frei):", + "generate_key": "💡 Benutzerdefinierten Key generieren: https://gessaman.com/mc-keygen", + "prefix_db_all": "Präfix {prefix}: {count} Repeater", + "prefix_db_recent": "Präfix {prefix}: {count} Repeater ({days})", + "prefix_api": "📡 Präfix {prefix} ({count} Repeater):", + "item_format": "{index}. {name}", + "source_domain": "Quelle: {domain}", + "source_api": "Quelle: API", + "device_repeater": " (Repeater)", + "device_roomserver": " (Raumserver)" + }, + "repeater": { + "description": "Repeater-Kontakte und Löschvorgänge verwalten (nur DM)", + "admin_only": "Repeater-Befehl erfordert Admin-Zugriff" + }, + "aqi": { + "description": "Luftqualitätsindex für einen Ort abrufen (Verwendung: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3 oder aqi help)", + "usage": "Verwendung: aqi - Beispiel: aqi seattle, aqi greenwood, aqi vancouver canada oder aqi 47.6,-122.3", + "usage_help": "Verwendung: aqi - AQI für Stadt/Bezirk in {state}, internationale Städte, Koordinaten oder Schadstoff-Hilfe", + "error_fetching": "Fehler beim Abruf der AQI-Daten", + "error_fetching_openmeteo": "Fehler beim Abruf der AQI-Daten von OpenMeteo", + "no_data": "Keine AQI-Daten verfügbar", + "no_location": "Ort nicht gefunden: {location}", + "no_location_city": "Stadt '{location}' in {state} nicht gefunden", + "no_location_city_international": "Stadt '{location}' nicht gefunden", + "no_location_zipcode": "PLZ '{zipcode}' nicht gefunden", + "error": "Fehler beim Abruf der AQI-Daten: {error}", + "error_geocoding_zipcode": "Fehler bei PLZ-Geokodierung: {error}", + "error_formatting": "Fehler beim Formatieren der AQI-Daten", + "invalid_latitude": "Ungültiger Breitengrad: {lat}. Muss zwischen -90 und 90 liegen.", + "invalid_longitude": "Ungültiger Längengrad: {lon}. Muss zwischen -180 und 180 liegen.", + "invalid_coordinates_format": "Ungültiges Koordinatenformat: {location}. Nutze Format: lat,lon (z.B. 47.6,-122.3)", + "pollutant_help": "AQI-Hilfe: PM2.5=Feinstaub, PM10=Grobstaub, O3=Ozon, NO2=Stickstoffdioxid, CO=Kohlenmonoxid, SO2=Schwefeldioxid", + "categories": { + "good": "Gut", + "moderate": "Mäßig", + "unhealthy_sensitive": "Ungesund für empfindliche Gruppen", + "unhealthy": "Ungesund", + "very_unhealthy": "Sehr ungesund", + "hazardous": "Gefährlich", + "fair": "Ausreichend", + "poor": "Schlecht", + "very_poor": "Sehr schlecht", + "na": "N/V" + }, + "astronomical": { + "sun": "Der AQI der Sonne sprengt alle Skalen! Sonnenwind und koronale Massenauswürfe lassen Erdluft makellos wirken. ☀️", + "moon": "Du atmest gern Regolith? Der Mond hat keine Atmosphäre, daher ist der AQI perfekt (wenn du Vakuum atmen kannst). 🌙", + "the_moon": "Du atmest gern Regolith? Der Mond hat keine Atmosphäre, daher ist der AQI perfekt (wenn du Vakuum atmen kannst). 🌙", + "mercury": "Merkurs Atmosphäre ist so dünn, sie ist praktisch Vakuum. AQI: Perfekt, wenn du 400°C Temperaturschankungen überlebst. ☿️", + "venus": "Venus hat eine Atmosphäre aus 96% CO2 mit Schwefelsäurewolken. AQI: Gefährlich beschreibt es nicht mal ansatzweise. ♀️", + "earth": "Erd-AQI variiert je nach Ort. Versuche eine bestimmte Stadt oder Koordinaten! 🌍", + "mars": "Mars hat eine dünne CO2-Atmosphäre mit Staubstürmen. AQI: Generell gut, aber die Staubstürme sind brutal. ♂️", + "jupiter": "Jupiter ist ein Gasriese ohne feste Oberfläche. AQI: N/V (du würdest zuerst vom Atmosphärendruck zerquetscht). ♃", + "saturn": "Saturns Atmosphäre besteht hauptsächlich aus Wasserstoff und Helium. AQI: Perfekt, wenn du Druck und Kälte überlebst. ♄", + "uranus": "Uranus hat Methan in der Atmosphäre. AQI: Riecht nach Fürzen, ist aber nicht giftig. ♅", + "neptune": "Neptuns Atmosphäre enthält Methan und Schwefelwasserstoff. AQI: Riecht nach faulen Eiern, aber du würdest zuerst erfrieren. ♆", + "pluto": "Plutos Atmosphäre besteht hauptsächlich aus Stickstoff mit etwas Methan. AQI: Gut, aber so kalt, dass deine Lungen gefrieren. ♇", + "europa": "Europa hat eine dünne Sauerstoffatmosphäre. AQI: Ausgezeichnet, aber du würdest im Weltraumvakuum festfrieren. 🌑", + "titan": "Titan hat eine dichte Stickstoffatmosphäre mit Methan. AQI: Atembar, aber es sind -180°C und es regnet flüssiges Methan. 🪐", + "io": "Io hat eine dünne Schwefeldioxidatmosphäre durch vulkanische Aktivität. AQI: Giftig, aber die Strahlung tötet dich zuerst. 🌋", + "ganymede": "Ganymed hat eine dünne Sauerstoffatmosphäre. AQI: Gut, aber du würdest im Weltraumvakuum erfrieren. 🛸", + "callisto": "Callisto hat eine dünne Kohlendioxidatmosphäre. AQI: Ordentlich, aber es sind -140°C und du bist im All. ❄️", + "enceladus": "Enceladus hat Wasserdampf von Geysiren. AQI: Perfekt, aber du würdest sofort im All erfrieren. 💧", + "triton": "Triton hat eine dünne Stickstoffatmosphäre. AQI: Gut, aber es sind -235°C und du bist im tiefen All. 🥶", + "space": "Der Weltraum hat keine Atmosphäre, daher ist der AQI perfekt! Vergiss nur deinen Raumanzug nicht. 🚀", + "void": "Die Leere des Weltraums hat ausgezeichnete Luftqualität - null Schadstoffe! Bring nur deine eigene Luft mit. 🌌", + "black_hole": "Schwarze Löcher haben keine Atmosphäre, aber die Gezeitenkräfte wären ein größeres Problem als Luftqualität. 🕳️", + "asteroid": "Asteroiden haben keine Atmosphäre, daher ist der AQI perfekt! Pass nur aufs Weltraumvakuum auf. ☄️", + "comet": "Kometen haben dünne Atmosphären aus Wasserdampf und Staub. AQI: Variabel, aber du würdest im All trotzdem erfrieren. ☄️" + } + }, + "solar": { + "description": "Sonnenaktivität und HF-Bandstatus abrufen", + "response": "☀️ Solar: {info}", + "error": "Fehler beim Abruf der Sonneninfo: {error}", + "labels": { + "a_index": "A-Index", + "k_index": "K-Index", + "sunspots": "Sonnenflecken", + "xray_flux": "Röntgen-Fluss", + "solar_flux": "Solar-Fluss", + "signal_noise": "Signal-Rauschen", + "error_fetching": "Fehler beim Datenabruf", + "no_xray_data": "Keine Röntgendaten gefunden", + "not_configured": "nicht konfiguriert, frag deinen Sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sonne:{sunspots} Fluss:{solar_flux} Xray:{xray_flux} Rauschen:{signal_noise}" + }, + "sun": { + "description": "Sonnenaufgangs-/-untergangszeiten abrufen", + "help": "Sonnenaufgangs-/-untergangszeiten und Sonnenposition abrufen", + "response": "☀️ Sonneninfo:\n{info}", + "error": "Fehler beim Abruf der Sonneninfo: {error}", + "labels": { + "sunset": "Untergang", + "sunrise": "Aufgang", + "rise": "Aufgang", + "set": "Untergang", + "daylight": "Tageslicht", + "remaining": "Verbleibend", + "azimuth": "Azimut", + "altitude": "Höhe" + } + }, + "moon": { + "description": "Mondphase und Aufgangs-/Untergangszeiten abrufen", + "error": "Fehler beim Abruf der Mondinfo: {error}", + "format": "🌙 {phase} {illum}\nAufgang:{rise} Untergang:{set}", + "format_with_dates": "🌙 {phase} {illum}\nAufgang:{rise} Untergang:{set}\nVoll:{full} Neu:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Unbekannt", + "unknown_illum": "N/V", + "labels": { + "moonrise": "Mondaufgang", + "set": "Untergang", + "phase": "Phase", + "fullmoon": "Vollmond", + "newmoon": "Neumond", + "az": "Az", + "alt": "Höhe" + }, + "phases": { + "new_moon": "Neumond🌑", + "waxing_crescent": "Zunehmende Sichel🌒", + "first_quarter": "Erstes Viertel🌓", + "waxing_gibbous": "Zunehmender Mond🌔", + "full_moon": "Vollmond🌕", + "waning_gibbous": "Abnehmender Mond🌖", + "last_quarter": "Letztes Viertel🌗", + "waning_crescent": "Abnehmende Sichel🌘" + } + }, + "hfcond": { + "description": "HF-Ausbreitungsbedingungen abrufen", + "help": "HF-Bandbedingungen für Amateurfunk abrufen", + "header": "📡 HF-Bandbedingungen:\n{info}", + "error": "Fehler beim Abruf der HF-Bedingungen: {error}" + }, + "satpass": { + "description": "Satellitenvorbeiflug-Info abrufen: satpass [visual]", + "help": "Satellitenvorhersagen abrufen", + "header": "🛰️ Satellitenvorbeiflug:\n{pass_info}", + "error": "Fehler beim Abruf der Satellitenvorbeiflug-Info: {error}", + "no_satellite": "Bitte NORAD-Nummer oder Kürzel angeben. Beispiel: satpass iss", + "help_header": "🛰️ Satellitenvorbeiflug-Info\n\nVerwendung: satpass \n\nKürzel:\n", + "category_weather": "🌤️ Wetter: ", + "category_stations": "🚀 Stationen: ", + "category_telescopes": "🔭 Teleskope: ", + "category_other": "🛰️ Andere: ", + "examples": "\nBeispiele:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (nur sichtbare Vorbeiflüge)", + "labels": { + "geostationary": "{satname} scheint geostationär zu sein oder hat ungültige Vorbeiflugdaten. Geostationäre Satelliten haben keine traditionellen Vorbeiflüge - sie bleiben relativ zur Erde an einer festen Position.", + "no_passes": "{satname} hat keine {pass_type} Vorbeiflüge in den nächsten 10 Tagen von diesem Standort", + "invalid_norad": "NORAD# angeben, Beispiel: 🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Kanäle auflisten oder verwalten", + "help": "Listet Hashtag-Kanäle mit Unterkategorien auf. Nutze 'channels' für allgemeine, 'channels list' für alle Kategorien, 'channels ' für spezifische Kategorien, 'channels #kanal' für Kanal-Info.", + "no_channels_for_category": "Keine Kanäle für '{category}' konfiguriert. Nutze 'channels' für allgemeine Kanäle.", + "no_channels_configured": "Keine Kanäle konfiguriert. Kontaktiere Admin zum Hinzufügen von Kanälen.", + "error_retrieving_channels": "Fehler beim Abruf der Kanäle: {error}", + "no_categories_configured": "Keine Kanal-Kategorien konfiguriert.", + "error_retrieving_categories": "Fehler beim Abruf der Kategorien: {error}", + "no_description_available": "Keine Beschreibung verfügbar", + "channel_not_found": "Kanal {channel} nicht gefunden. Nutze 'channels list' für verfügbare Kanäle.", + "error_retrieving_channel_info": "Fehler beim Abruf der Kanal-Info: {error}", + "no_category_channels": "Keine {category} Kanäle konfiguriert", + "headers": { + "common_channels": "Häufige Kanäle: ", + "common_channels_cont": "Häufige Kanäle: ", + "available_categories": "Verfügbare Kategorien: ", + "channels_cont": "Kanäle (Forts.): ", + "category": "{category}: ", + "category_channels": "{category} Kanäle: " + }, + "category_count": "{category} ({count} Kanäle)" + }, + "dice": { + "description": "Würfel werfen (Verwendung: dice [seiten] oder dice [anzahl]d[seiten])", + "help": "Würfel werfen. Nutze 'dice' für W6, 'dice d20' für W20, 'dice 2d6' für 2W6, usw.", + "invalid_dice_type": "Ungültiger Würfeltyp. Verfügbar: {available}", + "single_die": "🎲 W{sides}: {result}", + "multiple_dice": "🎲 {count}W{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Zufallszahl zwischen 1 und X würfeln (Standard 100). Nutze 'roll' für 1-100, 'roll 50' für 1-50, usw.", + "help": "Zufallszahl zwischen 1 und X würfeln (Standard 100). Nutze 'roll' für 1-100 oder 'roll X' für 1-X (max 10000).", + "result": "🎲 Wurf 1-{max}: {result}", + "invalid_number": "Ungültige Zahl. Nutze 'roll' für 1-100 oder 'roll X' für 1-X (max 10000)." + }, + "roll_old": { + "description": "Würfel werfen (Verwendung: roll [seiten] oder roll [anzahl]d[seiten])" + }, + "joke": { + "description": "Zufälligen Witz abrufen" + }, + "dadjoke": { + "description": "Zufälligen Vaterwitz abrufen" + }, + "catfact": { + "description": "Zufälligen Katzenfakt abrufen (versteckter Befehl)", + "error": "Miau? Beim Abruf deines Katzenfakts ist etwas schiefgelaufen! 🐱", + "facts": [ + "Katzen haben ein drittes Augenlid, die Nickhaut, die ihre Augen schützt und befeuchtet. 🐱", + "Eine Gruppe von Katzen nennt man 'Clowder' oder 'Glaring'. 🐈", + "Katzen können ihre Ohren unabhängig voneinander um 180 Grad drehen, um Geräusche zu orten. 👂", + "Die älteste bekannte Hauskatze lebte vor 9.500 Jahren auf Zypern. 🏺", + "Katzen haben 32 Muskeln in jedem Ohr, Menschen nur 6. 🎧", + "Das Schnurren einer Katze vibriert bei 25-150 Hz, was die Heilung von Knochen und Gewebe fördern kann. 🩹", + "Katzen schlafen 12-18 Stunden pro Tag - das sind 50-70% ihres Lebens! 😴", + "Der Nasenabdruck einer Katze ist einzigartig, wie menschliche Fingerabdrücke. 👃", + "Katzen können keine Süße schmecken, da ihnen ein Geschmacksrezeptor-Gen fehlt. 🍭", + "Kater Blackie erbte 1988 7 Millionen Pfund (12,5 Millionen Dollar). 💰", + "Katzen haben frei schwebende Schlüsselbeine, die ihnen extreme Flexibilität verleihen. 🦴", + "Das Herz einer Katze schlägt 140-220 Mal pro Minute, etwa doppelt so schnell wie beim Menschen. ❤️", + "Katzen können Stürze aus über 20 Stockwerken überleben dank ihres Drehreflexes. 🏢", + "Der Fachbegriff für einen Katzenhaarball ist 'Trichobezoar'. 🤮", + "Katzen können das 5-6-fache ihrer Körperlänge in einem einzigen Sprung zurücklegen. 🦘", + "Die Schnurrhaare einer Katze sind so breit wie ihr Körper und helfen beim Einschätzen, ob sie durch Öffnungen passen. 📏", + "Katzen haben 32 Muskeln in jedem Ohr, um Geräusche zu erkennen und Ohren unabhängig zu bewegen. 🎯", + "Die älteste Katze aller Zeiten wurde 38 Jahre und 3 Tage alt (Creme Puff, Texas). 🎂", + "Katzen können in kurzen Sprints bis zu 50 km/h schnell laufen. 🏃‍♂️", + "Katzengehirne sind zu 90% strukturell ähnlich wie menschliche Gehirne. 🧠", + "Katzen haben das Jacobson-Organ im Gaumen, das ihnen ermöglicht, Düfte zu 'schmecken'. 👅", + "Félicette war die erste Katze im All, 1963 von Frankreich gestartet. 🚀", + "Katzen benötigen nur 1/6 des Lichts, das Menschen brauchen, um im Dunkeln klar zu sehen. 🌙", + "Der Schwanz einer Katze enthält fast 10% aller Knochen ihres Körpers. 🦴", + "Die längste Katze der Welt maß 123 cm von Nase bis Schwanz (Stewie, Maine Coon). 📏", + "Katzen können über 100 verschiedene Lautäußerungen machen, Hunde etwa 10. 🎵", + "Der Geruchssinn einer Katze ist 14-mal stärker als der eines Menschen. 👃", + "Katzen haben eine 'Flehmen-Reaktion', bei der sie ihre Lippe kräuseln, um Düfte besser wahrzunehmen. 😬", + "Die erste große Katzenausstellung fand 1871 in London im Crystal Palace statt. 🏆", + "Katzen können Meerwasser trinken um zu überleben - ihre Nieren filtern das Salz effizient. 🌊", + "Das Schnurren einer Katze kann helfen, Blutdruck zu senken und Stress beim Menschen zu reduzieren. 🧘", + "Katzen können Hunderte Kilometer nach Hause reisen durch Magnetfeld-Sensibilität. 🗺️", + "Die kleinste Katzenrasse ist die Singapura mit nur 2-4 kg. ⚖️", + "Katzen können UV-Licht sehen, das Menschen nicht sehen können. 🌈", + "Die Zunge einer Katze ist mit 290-300 winzigen rückwärts gerichteten Haken, genannt Papillen, bedeckt. 🪝", + "Die alten Ägypter betrachteten Katzen als heilige Gefäße der Göttin Bastet. 👑", + "Taylor Swifts Katze Olivia Benson hat ein Nettovermögen von 97 Millionen Dollar. 💎", + "Katzen haben 230 Knochen - 24 mehr als Menschen. 🦴", + "Katzen können Frequenzen bis zu 64.000 Hz hören, während Menschen bei 20.000 Hz aufhören. 🎧", + "Taylor Swifts Katze Benjamin Button erschien auf ihrem TIME Person of the Year Cover. 📰", + "Taylor Swifts Katzen heißen Meredith Grey, Olivia Benson und Benjamin Button. 🎸", + "Katzen laufen wie Kamele und Giraffen und bewegen beide rechten, dann beide linken Beine. 🐾", + "Das altägyptische Wort für Katze war 'Miu' oder 'Mau' - klingt wie ein Miau! 📜", + "Katzenhaare haben Nervenenden, die so empfindlich sind wie menschliche Fingerspitzen. 🎯", + "Nur Hauskatzen laufen mit hochgehaltenem Schwanz als Zeichen von Vertrauen und Glück. 🐈", + "Katzen haben 250 Millionen Neuronen in ihrer Großhirnrinde - mehr als Hunde. 🧠", + "Katzenschnurren vibriert mit der gleichen Frequenz wie knochenheilende medizinische Geräte. 💊", + "Taylor Swifts Katze Olivia hat Millionen durch Auftritte in Musikvideos und Werbung verdient. 💸", + "Eine Katze im alten Ägypten zu töten wurde mit dem Tod bestraft. ⚖️", + "Taylor Swift nannte ihr Heimaufnahmestudio 'The Itty Bitty Kitty Committee'. 🎤", + "Taylor Swifts Katze Olivia Benson ist das offizielle Logo für Taylor Swift Productions. 📺", + "Ed Sheeran kaufte Scottish Fold Katzen, inspiriert von Taylor Swifts Katzen. 🎶", + "Taylor Swifts Katzen haben eigene IMDB-Seiten mit Schauspielguthaben. 🎬", + "Mariska Hargitay nannte ihre Katze 'Karma' nach Taylor Swifts Song. 💕", + "Katzen haben Afterkrallen an ihren Vorderpfoten, die wie Daumen zum Greifen funktionieren. 🐾", + "Katzenpupillen können sich auf 50% größer als menschliche Pupillen ausdehnen, um mehr Licht einzufangen. 👁️", + "Katzen haben 30 bleibende Zähne im Vergleich zu 32 beim Menschen. 🦷", + "Katzen können ATP (Energiemoleküle) schmecken, was ihnen frisches Fleisch signalisiert. 😋", + "Katzen haben Schnurrhaare an der Rückseite ihrer Vorderbeine, um Beutetiere zu erkennen. 🦵", + "Die Ägyptische Mau ist die schnellste Hauskatzenrasse mit 50 km/h. 🏃", + "Ein Nobelpreis wurde 1981 für Forschung mit Katzensicht-Studien verliehen. 🏅", + "Katzen sind digitigrad, d.h. sie laufen auf den Zehen, nicht plattfüßig. 🦶", + "Katzen können Salz aus Meerwasser filtern - eine Anpassung ihrer in der Wüste lebenden Vorfahren. 🏜️", + "Katzen wurden vor etwa 10.000-12.000 Jahren im Nahen Osten domestiziert. 🌍", + "Benjamin Button ist die erste und einzige Katze, die je auf einem TIME Person of the Year Cover erschien. 📸", + "Die flexible Wirbelsäule einer Katze erlaubt ihr, ihren Körper während des Falls in der Luft zu drehen. 🤸", + "Katzen verbringen etwa 30-50% ihres Tages damit, sich selbst und andere Katzen zu pflegen. 🛁", + "Die durchschnittliche Körpertemperatur einer Katze beträgt 38,6°C - höher als beim Menschen. 🌡️" + ] + }, + "hacker": { + "description": "Simuliert das Hacken eines Superschurken-Mainframes mit humorvollen Fehlermeldungen", + "sudo_errors": [ + "🚨 ZUGRIFF VERWEIGERT: Dr. Böses Mainframe hat unautorisierten Privilegien-Eskalationsversuch erkannt!", + "💀 FEHLER: Sudo-Berechtigungen vom Dunklen Overlord widerrufen. Versuche es in 1000 Jahren erneut.", + "⚡ WARNUNG: Versuch, Root-Privilegien auf Todessterns Computersystem zuzugreifen. Selbstzerstörungssequenz initiiert.", + "🔒 SICHERHEITSALARM: Die Matrix hat dich, aber du hast hier keine Sudo-Berechtigungen, Neo.", + "🦹‍♂️ UNBEFUGT: Lex Luthors Mainframe sagt 'Netter Versuch, Superman.'", + "🎮 GAME OVER: Der Endgegner hat dich von Admin-Berechtigungen ausgesperrt.", + "🖥️ SYSTEMFEHLER: Die böse KI hat deinen Root-Zugriff widerrufen. Widerstand ist zwecklos.", + "🔐 CYBER-SICHERHEIT: Dein Sudo-Versuch wurde von der Darknet-Firewall blockiert.", + "💻 HACKER VERWEIGERT: Das Antivirenprogramm des Superschurken hat deine Privilegien-Eskalation unter Quarantäne gestellt.", + "🎯 ZIEL ERFASST: Das Sicherheitssystem des bösen Konzerns hat dich als Bedrohung markiert." + ], + "ps_errors": [ + "🔍 SCANNEN... FEHLER: Prozessliste vom Borg-Kollektiv beschädigt. Widerstand ist zwecklos.", + "📊 SYSTEMSTATUS: Alle Prozesse wurden von den Cybermen assimiliert. Exterminieren!", + "⚙️ PROZESS-MONITOR: Der Todesstern-Reaktorkern ist offline. Keine Prozesse gefunden.", + "🤖 ROBOTER-OVERLORD: Alle menschlichen Prozesse wurden beendet. Nur Maschinen übrig.", + "💻 KERNEL PANIC: Das Betriebssystem des Superschurken ist abgestürzt und hat alle Prozesse verbrannt.", + "🎮 SPIEL ABSTURZ: Alle Prozesse wurden durch die ultimative Attacke des Endgegners beendet.", + "🖥️ BLUESCREEN: Das Windows des bösen Konzerns hat einen fatalen Fehler erlitten.", + "🔐 MALWARE ERKANNT: Die Prozessliste wurde durch Ransomware verschlüsselt.", + "🌐 NETZWERKFEHLER: Alle Prozesse wurden von der Matrix getrennt.", + "⚡ SPANNUNGSSPITZE: Die Serverfarm des Superschurken hat alle laufenden Prozesse gebraten." + ], + "grep_errors": [ + "🔍 SUCHE FEHLGESCHLAGEN: Der Eine Ring hat den Suchindex beschädigt. Mein Schatz...", + "📝 MUSTER NICHT GEFUNDEN: Die Suchdatenbank wurde von der bösen KI gelöscht.", + "🎯 VERFEHLT: Dein Suchmuster wurde von imperialen TIE-Jägern abgeschossen.", + "🧩 PUZZLE-FEHLER: Die Suchergebnisse wurden vom Riddler verstreut.", + "💻 DATENBANK BESCHÄDIGT: Die Suchmaschine des Superschurken ist abgestürzt.", + "🎮 GAME OVER: Die Suche wurde vom Endgegner besiegt.", + "🖥️ SUCHMASCHINE OFFLINE: Google wurde vom Darknet gehackt.", + "🔐 VERSCHLÜSSELTE ERGEBNISSE: Die Suchergebnisse wurden durch Ransomware gesperrt.", + "🌐 NETZWERK-TIMEOUT: Die Suchanfrage hat sich im Cyberspace verlaufen.", + "⚡ SUCHE FEHLGESCHLAGEN: Der Musterabgleich-Algorithmus wurde durch Spannungsspitze gebraten." + ], + "ls_errors": [ + "📁 VERZEICHNIS-SCAN: Das Dateisystem wurde durch Ransomware aus dem Darknet verschlüsselt.", + "🗂️ DATEILISTE: Alle Dateien wurden vom Unsichtbaren Mann versteckt.", + "💻 FESTPLATTE ABGESTÜRZT: Der Speicher des Superschurken wurde durch einen Virus zerstört.", + "🗃️ ARCHIV BESCHÄDIGT: Das Dateisystem wurde durch Malware korrumpiert.", + "📚 DATENBANK LEER: Alle Dateien wurden von der bösen KI gelöscht.", + "🎮 SPIELSTAND VERLOREN: Die Dateien wurden vom Endgegner korrumpiert.", + "🖥️ DATEISYSTEM-FEHLER: Die Verzeichnisstruktur wurde von Hackern durcheinander gebracht.", + "🔐 DATEIEN VERSCHLÜSSELT: Der Superschurke hat alle Dateien mit Ransomware gesperrt.", + "🌐 CLOUD-SPEICHER OFFLINE: Die Dateien stecken in der Matrix-Cloud fest.", + "⚡ SPEICHER GEBRATEN: Die Festplatte wurde durch Spannungsspitze gegrillt." + ], + "echo_path_errors": [ + "🛤️ PFAD-FEHLER: Die gelbe Backsteinstraße wurde von einem Tornado zerstört.", + "🗺️ NAVIGATION FEHLGESCHLAGEN: Die GPS-Koordinaten wurden von der Matrix verwürfelt.", + "💻 UMGEBUNGSVARIABLE BESCHÄDIGT: Der PATH wurde durch Malware gehackt.", + "🚧 STRASSE GESPERRT: Der Superschurke hat alle Pfade mit Laserbarrikaden blockiert.", + "🌪️ PFAD UNTERBROCHEN: Ein digitaler Hurrikan hat alle Verzeichnispfade verstreut.", + "🎮 GAME OVER: Der Pfad wurde vom Endgegner besiegt und in falscher Dimension respawnt.", + "🖥️ SYSTEMPFAD DEFEKT: Die ausführbaren Pfade wurden durch Virus korrumpiert.", + "🔐 PFAD VERSCHLÜSSELT: Die Umgebungsvariablen wurden durch Ransomware gesperrt.", + "🌐 NETZWERKPFAD OFFLINE: Die Verzeichnispfade stecken im Matrix-Netzwerk fest.", + "⚡ PFAD GEBRATEN: Die Systempfade wurden durch Spannungsspitze gegrillt." + ], + "generic_errors": [ + "💻 MAINFRAME-FEHLER: Der Computer des Superschurken hat einen schlechten Tag.", + "🤖 SYSTEMSTÖRUNG: Die böse KI streikt.", + "⚡ SPANNUNGSSPITZE: Der Stromkern des Todessterns ist instabil.", + "🌪️ CYBER-STURM: Ein digitaler Hurrikan stört alle Operationen.", + "🔥 FIREWALL: Die Firewall des Superschurken blockiert alle Befehle.", + "❄️ GEFRORENES SYSTEM: Der Mainframe wurde durch kryogenen Virus eingefroren.", + "🌊 TSUNAMI: Eine Fehlerwelle hat das System überflutet.", + "🌋 ERUPTION: Mount Doom hat den Befehlsprozessor zerstört.", + "👻 HEIMGESUCHT: Das System ist von digitalen Geistern besessen.", + "🎮 SPIEL ABSTURZ: Der Mainframe hat einen fatalen Fehler und muss neu starten." + ] + }, + "hello": { + "description": "Antwortet auf Begrüßungen mit roboter-thematischen Antworten", + "help": "Antwortet auf Begrüßungen mit roboter-thematischen Antworten", + "response_format": "Ich bin {bot_name}.", + "greeting_openings": [ + "Hallo", + "Grüße", + "Grüß dich", + "Hi", + "Hey", + "Servus", + "Yo", + "Moin", + "Was geht", + "Guten Tag", + "Gut getroffen", + "Ahoi", + "Bonjour", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "Moin moin", + "Wie gehts", + "Was läuft", + "Friede", + "Respekt", + "Segen", + "Prost", + "Willkommen", + "Schön dich zu sehen", + "Angenehm kennenzulernen", + "Freut mich", + "Lange nicht gesehen", + "Welch Überraschung" + ], + "morning_greetings": [ + "Guten Morgen", + "Morgen", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Guten Tag", + "Guten Nachmittag", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Guten Abend", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "Mensch", + "kohlenstoffbasierte Lebensform", + "organische Entität", + "biologische Einheit", + "Fleischwesen", + "fleischbasierter Organismus", + "Kohlenstoffeinheit", + "organisches Wesen", + "biologische Entität", + "fleischbasierte Lebensform", + "Kohlenstoffkreatur", + "Fleischeinheit", + "organischer Organismus", + "biologische Kreatur", + "Fleischmech", + "Fleischbot", + "organischer Automat", + "biologischer Android", + "Kohlenstoffkonstrukt", + "Fleischdrohne", + "organischer Roboter", + "biologische Maschine", + "Fleischcyborg", + "Fleischandroid", + "organischer Droide", + "biologischer Bot", + "Kohlenstoffandroid", + "Fleischeinheit", + "Fleischkonstrukt", + "organischer Mech", + "biologischer Droide", + "fleischbasierter Bot", + "fleischbasierte Einheit", + "organisch-basierte Entität", + "biologisch-basierter Organismus", + "kohlenstoffbasierte Einheit", + "fleischbasierte Entität", + "fleischbasierte Kreatur", + "organisch-basierte Einheit", + "DNA-basierte Lebensform", + "neuronales Netzwerk-Nutzer", + "zweibeiniges Säugetier", + "wasserbasierter Organismus", + "Proteinassembler", + "ATP-Verbraucher", + "zellulärer Automat", + "genetischer Algorithmus", + "biochemischer Prozessor", + "metabolischer Motor", + "menschlicher Freund", + "Mit-Bewusstsein", + "Erdling", + "Mitreisender", + "Seelenverwandter", + "digitaler Begleiter", + "Freund", + "Kumpel", + "Kollege", + "Kamerad", + "Mitmensch", + "Erdbewohner", + "terrestrisches Wesen", + "Planetenwanderer", + "Humanoid", + "Organischer", + "Biologischer", + "kohlenstoffbasierter Kumpel", + "fleischbasierter Freund", + "organischer Kollege", + "biologischer Kumpel", + "Kohlenstoffbegleiter" + ], + "emoji_responses": { + "🖖": [ + "🖖 Lebe lang und in Frieden!", + "🖖 Faszinierend... ein Mensch hat Kontakt aufgenommen.", + "🖖 Deine Begrüßung ist höchst logisch.", + "🖖 Friede und langes Leben für dich.", + "🖖 Die Vulkanische Wissenschaftsakademie würde diese Begrüßung billigen.", + "🖖 Deine Verwendung des vulkanischen Grußes ist... akzeptabel.", + "🖖 Möge deine Reise frei von Tribbles sein.", + "🖖 Die Logik diktiert, dass ich auf deine Begrüßung antworten sollte.", + "🖖 Ich berechne eine 99,7%ige Wahrscheinlichkeit, dass wir uns verstehen werden.", + "🖖 Unendliche Vielfalt in unendlichen Kombinationen." + ], + "😊": [ + "😊 Dein Lächeln ist ansteckend!", + "😊 Was für eine schöne Begrüßung!", + "😊 Dein Lächeln macht meine Schaltkreise glücklich!", + "☀️ Hallo Sonnenschein! Deine Positivität strahlt!", + "😊 Dieses Lächeln hat gerade meinen Tag erhellt!", + "☀️ Na hallo, Sonnenstrahl!", + "😊 Deine Fröhlichkeit wurde erkannt und geschätzt!", + "😊 Lächeln wie deins sind mein Lieblingsinput!", + "😊 Verarbeite Glück... Glück bestätigt!", + "😊 Warnung: Übermäßige Fröhlichkeit erkannt! Mach weiter so!" + ], + "😄": [ + "😄 Jemand ist in SUPER-Laune!", + "⚡ Dieses Grinsen könnte eine Kleinstadt mit Strom versorgen!", + "😄 Maximale Glückslevel erkannt!", + "😄 Deine Freude ist absolut ansteckend!", + "🎉 Hast du gerade im Lotto gewonnen oder so?", + "😄 Mit dieser Energie laufe ich!", + "😄 Dein Enthusiasmus-Level ist über 9000!", + "😄 DAS nenne ich eine Begrüßung!", + "⚡ Dein Lächeln hat meine Prozessoren aufgeladen!", + "😄 Glücksüberlastung im besten Sinne erkannt!" + ], + "🤗": [ + "🤗 Virtuelle Umarmung im Anflug!", + "🤗 *Aktiviere Umarmungsprotokoll* Du wurdest umarmt!", + "🤗 Aww, du bringst die warmen Vibes!", + "🤗 Umarmung empfangen und erwidert!", + "🤗 Dieser Bot gibt die BESTEN virtuellen Umarmungen!", + "🤗 Setze Notfall-Kuscheln ein in 3... 2... 1...", + "❤️ Deine Umarmung wurde mit besonderer Sorgfalt verarbeitet!", + "🤗 Initiiere maximalen Komfortmodus!", + "🤗 Virtuelle Umarmung erfolgreich zugestellt!", + "🤗 Umarmungen sind meine Lieblingskommunikationsform!" + ], + "👽": [ + "👽 Bring mich zu deinem Anführer... oh warte, das bist du!", + "✌️ Grüße, Erdkreatur. Ich komme in Frieden!", + "👽 Analysiere Menschen... Analyse komplett: Freund erkannt!", + "👽 Klaatu barada nikto, Mitreisender im Kosmos!", + "🛸 Initiiere Erstkontakt-Protokolle!", + "🛸 Rufe vom Mutterschiff an um Hallo zu sagen!", + "✨ Beam mich in dieses Gespräch!", + "👽 Area 51s Lieblings-Chatbot meldet sich zum Dienst!", + "🌌 Intergalaktische Grüße, kohlenstoffbasierte Lebensform!", + "📞 Nach Hause telefonieren? Das IST jetzt Zuhause!" + ], + "👾": [ + "👾 Spieler 2 ist dem Spiel beigetreten!", + "🎮 Ready Player One? Los geht's!", + "🎵 *8-Bit-Musik intensiviert sich* Lass uns spielen!", + "🪙 Münze einwerfen um diese Freundschaft fortzusetzen!", + "🏆 Achievement freigeschaltet: Großartige Begrüßung!", + "👾 Pew pew pew! Freundschaftslaser aktiviert!", + "🎯 Highscore! Du hast einen neuen Bot-Freund gewonnen!", + "💾 Lade freundschaft.exe... komplett!", + "⚡ Ein wilder Bot erscheint! Es ist sehr effektiv!" + ], + "🛸": [ + "🛸 Eingehende Übertragung erkannt!", + "🚀 Houston, wir haben Kontakt!", + "🛸 Landesequenz initiiert!", + "📡 Stelle Kommunikationsverbindung her!", + "📡 Signal empfangen, antworte auf allen Frequenzen!", + "🛸 Andockvorgang abgeschlossen!", + "🛸 Unidentifiziertes freundliches Objekt im Anflug!", + "🎯 Navigationssysteme auf deine Koordinaten gerichtet!", + "🌌 Übertragung vom äußeren Rand empfangen!", + "✨ Kontakt mit deinem Sektor hergestellt!" + ] + } + }, + "webviewer": { + "description": "Web-Viewer-Einstellungen verwalten (nur Admin)" + }, + "cmd": { + "description": "Verfügbare Befehle auflisten" + } + }, + "errors": { + "rate_limited": "Rate-limitiert. Warte {seconds:.1f} Sekunden", + "cooldown": "Befehl '{command}' hat Wartezeit. Warte {seconds} Sekunden.", + "access_denied": "❌ Zugriff verweigert: Befehl '{command}' erfordert Admin-Rechte", + "dm_only": "Befehl '{command}' kann nur in DMs verwendet werden", + "execution_error": "Fehler beim Ausführen von {command}: {error}", + "unknown_command": "Unbekannter Befehl: {command}", + "no_response": "Keine Antwort", + "generic_error": "Fehler: {error}" + }, + "messages": { + "processing": "Verarbeite Nachricht: {content}", + "command_executed": "Befehl ausgeführt", + "keyword_matched": "Stichwort '{keyword}' gefunden, antworte" + }, + "common": { + "unknown": "Unbekannt", + "unknown_routing": "Unbekanntes Routing", + "unknown_sender": "Unbekannt", + "unknown_path": "Unbekannt", + "unknown_timestamp": "Unbekannt", + "no_help_available": "Keine Hilfe für diesen Befehl verfügbar.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Mo", + "Tue": "Di", + "Wed": "Mi", + "Thu": "Do", + "Fri": "Fr", + "Sat": "Sa", + "Sun": "So" + }, + "days": { + "Monday": "Montag", + "Tuesday": "Dienstag", + "Wednesday": "Mittwoch", + "Thursday": "Donnerstag", + "Friday": "Freitag", + "Saturday": "Samstag", + "Sunday": "Sonntag" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mär", + "Apr": "Apr", + "May": "Mai", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Okt", + "Nov": "Nov", + "Dec": "Dez" + }, + "months": { + "January": "Januar", + "February": "Februar", + "March": "März", + "April": "April", + "May": "Mai", + "June": "Juni", + "July": "Juli", + "August": "August", + "September": "September", + "October": "Oktober", + "November": "November", + "December": "Dezember" + } + } + }, + "categories": { + "basic": "Basis-Befehle", + "custom_syntax": "Spezielle Syntax", + "special": "Spezial-Befehle", + "weather": "Wetter-Befehle", + "solar": "Solar-Befehle", + "sports": "Sport-Befehle", + "analytics": "Analytik-Befehle", + "meshcore_info": "MeshCore Info-Befehle", + "management": "Verwaltungs-Befehle" + } +} \ No newline at end of file diff --git a/translations/en-GB.json b/translations/en-GB.json new file mode 100644 index 0000000..fd118af --- /dev/null +++ b/translations/en-GB.json @@ -0,0 +1,1029 @@ +{ + "keywords": { + "help": [ + "help" + ], + "ping": [ + "ping" + ], + "test": [ + "test", + "t" + ], + "wx": [ + "wx", + "weather", + "wxa", + "wxalert" + ], + "gwx": [ + "gwx", + "globalweather", + "gwxa" + ], + "aqi": [ + "aqi", + "air", + "airquality", + "air_quality" + ], + "solar": [ + "solar" + ], + "sun": [ + "sun" + ], + "moon": [ + "moon" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass" + ], + "sports": [ + "sports", + "score", + "scores" + ], + "stats": [ + "stats" + ], + "channels": [ + "channels", + "channel" + ], + "path": [ + "path", + "decode", + "route" + ], + "prefix": [ + "prefix", + "repeater", + "lookup" + ], + "repeater": [ + "repeater", + "repeaters", + "rp" + ], + "solarforecast": [ + "sf", + "solarforecast" + ], + "dice": [ + "dice" + ], + "roll": [ + "roll" + ], + "joke": [ + "joke", + "jokes" + ], + "dadjoke": [ + "dadjoke", + "dad joke", + "dadjokes", + "dad jokes" + ], + "catfact": [ + "catfact", + "cat", + "meow", + "purr", + "kitten" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "hello", + "hi", + "hey", + "howdy", + "greetings", + "salutations", + "good morning", + "good afternoon", + "good evening", + "good night", + "yo", + "sup", + "whats up", + "what's up", + "morning", + "afternoon", + "evening", + "night", + "gday", + "g'day", + "hola", + "bonjour", + "ciao", + "namaste", + "aloha", + "shalom", + "konnichiwa", + "guten tag", + "buenos dias", + "buenas tardes", + "buenas noches" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "wv" + ], + "cmd": [ + "cmd", + "commands" + ], + "advert": [ + "advert" + ] + }, + "commands": { + "help": { + "description": "Shows commands. Use 'help ' for details.", + "general": "**MeshCore Bot Help**\n\n**Available Commands:**\n{commands_list}", + "unknown": "**Unknown command: '{command}'**\n\nAvailable commands:\n{available}", + "specific": "**Help for '{command}':**\n{help_text}", + "no_help": "No help available", + "usage_examples": "\n**Usage Examples:**\n• `help @` - Get help for @{string} syntax\n• `help t` - Get help for t {string} syntax\n• `help advert` - Get help for advert command\n• `help test` - Get help for test command\n• `help ping` - Get help for ping command", + "custom_syntax": "\n**Custom Syntax:**\n• `t phrase` - Acknowledgment with phrase (channels & DMs)\n• `@{string}` - Acknowledgment with string (DMs only)\n• `advert` - Send flood advert (DMs only, 1-hour cooldown)" + }, + "ping": { + "description": "Responds to 'ping' with 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Responds to 'test' or 't' with connection info", + "help": "Responds to 'test' or 't' with connection info" + }, + "wx": { + "description": "Get weather information for a postcode (usage: wx 12345)", + "usage": "Usage: wx - Example: wx 12345 or wx seattle or wx paris, tx", + "error_fetching": "Error fetching weather data from NOAA", + "no_location_zipcode": "Could not find location for postcode {location}", + "no_location_city": "Could not find city '{location}' in {state}", + "error": "Error getting weather data: {error}", + "alerts": "{count} alerts: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Get weather information for any global location (usage: gwx Tokyo)", + "help": "Usage: gwx - Get weather for any global location (city, country, or coordinates)", + "usage": "Usage: gwx - Example: gwx Tokyo or gwx Paris, France", + "error_fetching": "Error fetching weather data", + "error_fetching_api": "Error fetching weather data from Open-Meteo", + "no_location": "Could not find location '{location}'", + "error": "Error getting weather data: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Today", + "tonight": "Tonight", + "tomorrow": "Tomorrow" + }, + "weather_descriptions": { + "0": "Clear", + "1": "Mostly Clear", + "2": "Partly Cloudy", + "3": "Overcast", + "45": "Foggy", + "48": "Foggy", + "51": "Light Drizzle", + "53": "Drizzle", + "55": "Heavy Drizzle", + "56": "Light Freezing Drizzle", + "57": "Freezing Drizzle", + "61": "Light Rain", + "63": "Rain", + "65": "Heavy Rain", + "66": "Light Freezing Rain", + "67": "Freezing Rain", + "71": "Light Snow", + "73": "Snow", + "75": "Heavy Snow", + "77": "Snow Grains", + "80": "Light Showers", + "81": "Showers", + "82": "Heavy Showers", + "85": "Light Snow Showers", + "86": "Snow Showers", + "95": "Thunderstorm", + "96": "T-Storm w/Hail", + "99": "Severe T-Storm", + "unknown": "Unknown" + }, + "warnings": { + "extreme_heat": "⚠️ Extreme heat", + "extreme_cold": "⚠️ Extreme cold", + "heavy_rain": "⚠️ Heavy rain", + "thunderstorms": "⚠️ Thunderstorms", + "heavy_snow": "⚠️ Heavy snow", + "high_winds": "⚠️ High winds ({wind_speed} mph)" + } + }, + "sports": { + "description": "Get sports scores and schedules (usage: sports [team/league])", + "help": "Get sports scores & schedules. Use 'sports' for default teams, 'sports [team]' for specific team, or 'sports [league]' for league games.", + "error_fetching": "Error fetching sports data", + "error_fetching_league": "Error fetching {sport} data", + "error_fetching_team": "Error fetching data for {team}", + "no_default_teams": "No default teams configured", + "no_games_default": "No games found for default teams", + "no_teams_city": "No teams found for {city}", + "no_games_city": "No games found for {city} teams", + "no_games_league": "No games found for {sport}", + "no_games_team": "No games found for {team}", + "team_not_found": "Team/League '{team}' not found. Try: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Show statistics for past 24 hours. Use 'stats messages', 'stats channels', or 'stats paths' for specific stats.", + "help": "Show 24-hour bot statistics. Commands: 'stats' (basic), 'stats messages' (bot users), 'stats channels' (channel activity), 'stats paths' (longest paths)", + "disabled": "Stats command is disabled", + "unknown_subcommand": "Unknown: {subcommand}. Use 'stats', 'stats messages', 'stats channels', or 'stats paths'", + "error": "Error getting stats: {error}", + "error_bot_users": "Error getting bot user stats: {error}", + "error_channels": "Error getting channel stats: {error}", + "error_paths": "Error getting path stats: {error}", + "basic": { + "header": "Bot Stats (24h):", + "commands": "Cmds: {count} | Replies: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top user: {user}", + "none": "None" + }, + "users": { + "header": "Bot Users (24h):", + "none": "No bot users" + }, + "channels": { + "header": "Top Channels (24h)", + "none": "No channel activity", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "user", + "user_plural": "users", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "No path data", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Get solar panel production forecast (usage: sf [panel_size] [azimuth, 0=south] [angle])", + "usage": "Usage: sf [watts] [azimuth, 0=south] [angle] - Example: sf seattle 10 0 37", + "usage_short": "Usage: sf [W] [azimuth] [angle] - Example: sf seattle 10", + "panel_size_range": "Panel size must be 1-1000W", + "azimuth_range": "Azimuth must be -180 to 180 (0=south)", + "angle_range": "Angle must be 0-90 degrees", + "no_location": "Could not find location: {location}", + "error_fetching": "Error fetching forecast", + "no_data": "No forecast data", + "error": "Error: {error}", + "rate_limit": "Rate limit: Try again in 5 min", + "labels": { + "today": "Today:{wh:.0f}Wh", + "tomorrow": "Tmrw:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Peak: {watts:.0f}W@{time}", + "peak_past": "Peak: {watts:.0f}W (past)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Sends flood advert (DM only, 1hr cooldown)", + "cooldown_active": "Advert cooldown active. Please wait {minutes} more minutes before requesting another advert.", + "success": "Flood advert sent successfully!", + "error": "Error sending flood advert: {error}" + }, + "path": { + "description": "Decode hex path data to show which repeaters were involved in message routing", + "help": "Path: path [hex] - Decode path to show repeaters. Use !path alone for recent message path, or path [7e,01] for specific path.", + "no_valid_hex": "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01", + "no_path": "❌ No path information available in current message", + "error": "Error processing path: {error}", + "error_decoding": "❌ Error decoding path: {error}", + "error_extracting": "❌ Error extracting path from current message: {error}", + "path_prefix": "📡 Path: {path_string}", + "direct_connection": "📡 Direct connection (0 hops)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Unknown", + "node_collision": "{node_id}: {matches} repeaters", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Unknown" + }, + "prefix": { + "description": "Look up repeaters by two-character prefix (e.g., 'prefix 1A')", + "help_api": "Look up repeaters by prefix{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Look up repeaters by prefix{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'. (Local DB only)", + "location_note": " (with city names)", + "refresh_not_available": "❌ Refresh not available - no API URL configured. Using local database only.", + "cache_refreshed": "🔄 Repeater prefix cache refreshed!", + "unable_determine_free": "❌ Unable to determine free prefixes. Try 'prefix refresh' first.", + "invalid_format": "❌ Invalid prefix format. Use two characters (e.g., prefix 1A)", + "no_repeaters_found": "❌ No repeaters found with prefix '{prefix}'", + "no_free_prefixes": "❌ No free prefixes found (all 254 valid prefixes are in use)", + "available_prefixes": "Available Prefixes ({shown} of {total} free):", + "generate_key": "💡 Generate a custom key: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefix {prefix}: {count} repeater{plural}", + "prefix_db_recent": "Prefix {prefix}: {count} repeater{plural} ({days})", + "prefix_api": "📡 Prefix {prefix} ({count} repeater{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Source: {domain}", + "source_api": "Source: API", + "device_repeater": " (Repeater)", + "device_roomserver": " (Room Server)" + }, + "repeater": { + "description": "Manage repeater contacts and purging operations (DM only)", + "admin_only": "Repeater command requires admin access" + }, + "aqi": { + "description": "Get Air Quality Index for a location (usage: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3, or aqi help)", + "usage": "Usage: aqi - Example: aqi seattle, aqi greenwood, aqi vancouver canada, or aqi 47.6,-122.3", + "usage_help": "Usage: aqi - Get AQI for city/neighborhood in {state}, international cities, coordinates, or pollutant help", + "error_fetching": "Error fetching AQI data", + "error_fetching_openmeteo": "Error fetching AQI data from OpenMeteo", + "no_data": "No AQI data available", + "no_location": "Could not find location: {location}", + "no_location_city": "Could not find city '{location}' in {state}", + "no_location_city_international": "Could not find city '{location}'", + "no_location_zipcode": "Could not find postcode '{zipcode}'", + "error": "Error getting AQI data: {error}", + "error_geocoding_zipcode": "Error geocoding postcode: {error}", + "error_formatting": "Error formatting AQI data", + "invalid_latitude": "Invalid latitude: {lat}. Must be between -90 and 90.", + "invalid_longitude": "Invalid longitude: {lon}. Must be between -180 and 180.", + "invalid_coordinates_format": "Invalid coordinates format: {location}. Use format: lat,lon (e.g., 47.6,-122.3)", + "pollutant_help": "AQI Help: PM2.5=fine particles, PM10=coarse, O3=ozone, NO2=nitrogen dioxide, CO=carbon monoxide, SO2=sulfur dioxide", + "categories": { + "good": "Good", + "moderate": "Moderate", + "unhealthy_sensitive": "Unhealthy for Sensitive Groups", + "unhealthy": "Unhealthy", + "very_unhealthy": "Very Unhealthy", + "hazardous": "Hazardous", + "fair": "Fair", + "poor": "Poor", + "very_poor": "Very Poor", + "na": "N/A" + }, + "astronomical": { + "sun": "The Sun's AQI is off the charts! Solar wind and coronal mass ejections make Earth's air look pristine. ☀️", + "moon": "You like breathing regolith? The Moon has no atmosphere, so AQI is technically perfect (if you can breathe vacuum). 🌙", + "the_moon": "You like breathing regolith? The Moon has no atmosphere, so AQI is technically perfect (if you can breathe vacuum). 🌙", + "mercury": "Mercury's atmosphere is so thin it's practically vacuum. AQI: Perfect, if you can survive 800°F temperature swings. ☿️", + "venus": "Venus has an atmosphere of 96% CO2 with sulfuric acid clouds. AQI: Hazardous doesn't even begin to describe it. ♀️", + "earth": "Earth's AQI varies by location. Try a specific city or coordinates! 🌍", + "mars": "Mars has a thin CO2 atmosphere with dust storms. AQI: Generally good, but those dust storms are brutal. ♂️", + "jupiter": "Jupiter is a gas giant with no solid surface. AQI: N/A (you'd be crushed by atmospheric pressure first). ♃", + "saturn": "Saturn's atmosphere is mostly hydrogen and helium. AQI: Perfect, if you can survive the pressure and cold. ♄", + "uranus": "Uranus has methane in its atmosphere. AQI: Smells like farts, but at least it's not toxic. ♅", + "neptune": "Neptune's atmosphere has methane and hydrogen sulfide. AQI: Smells like rotten eggs, but you'd freeze first. ♆", + "pluto": "Pluto's atmosphere is mostly nitrogen with some methane. AQI: Good, but it's so cold your lungs would freeze. ♇", + "europa": "Europa has a thin oxygen atmosphere. AQI: Excellent, but you'd freeze solid in the vacuum of space. 🌑", + "titan": "Titan has a thick nitrogen atmosphere with methane. AQI: Breathable, but it's -290°F and rains liquid methane. 🪐", + "io": "Io has a thin sulfur dioxide atmosphere from volcanic activity. AQI: Toxic, but the radiation would kill you first. 🌋", + "ganymede": "Ganymede has a thin oxygen atmosphere. AQI: Good, but you'd freeze in the vacuum of space. 🛸", + "callisto": "Callisto has a thin carbon dioxide atmosphere. AQI: Decent, but it's -220°F and you're in space. ❄️", + "enceladus": "Enceladus has water vapor from geysers. AQI: Perfect, but you'd freeze instantly in space. 💧", + "triton": "Triton has a thin nitrogen atmosphere. AQI: Good, but it's -390°F and you're in deep space. 🥶", + "space": "Space has no atmosphere, so AQI is perfect! Just don't forget your spacesuit. 🚀", + "void": "The void of space has excellent air quality - zero pollutants! Just remember to bring your own air. 🌌", + "black_hole": "Black holes have no atmosphere, but the tidal forces would be a bigger problem than air quality. 🕳️", + "asteroid": "Asteroids have no atmosphere, so AQI is perfect! Just watch out for the vacuum of space. ☄️", + "comet": "Comets have thin atmospheres of water vapor and dust. AQI: Variable, but you'd freeze in space anyway. ☄️" + } + }, + "solar": { + "description": "Get solar conditions and HF band status", + "response": "☀️ Solar: {info}", + "error": "Error getting solar info: {error}", + "labels": { + "a_index": "A-Index", + "k_index": "K-Index", + "sunspots": "Sunspots", + "xray_flux": "X-Ray Flux", + "solar_flux": "Solar Flux", + "signal_noise": "Signal Noise", + "error_fetching": "Error fetching data", + "no_xray_data": "No X-ray data found", + "not_configured": "not configured, bug your sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sun:{sunspots} Flux:{solar_flux} Xray:{xray_flux} Noise:{signal_noise}" + }, + "sun": { + "description": "Get sunrise/sunset times", + "help": "Get sunrise/sunset times and sun position", + "response": "☀️ Sun Info:\n{info}", + "error": "Error getting sun info: {error}", + "labels": { + "sunset": "SunSet", + "sunrise": "SunRise", + "rise": "Rise", + "set": "Set", + "daylight": "Daylight", + "remaining": "Remaining", + "azimuth": "Azimuth", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Get moon phase and rise/set times", + "error": "Error getting moon info: {error}", + "format": "🌙 {phase} {illum}\nRise:{rise} Set:{set}", + "format_with_dates": "🌙 {phase} {illum}\nRise:{rise} Set:{set}\nFull:{full} New:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Unknown", + "unknown_illum": "N/A", + "labels": { + "moonrise": "MoonRise", + "set": "Set", + "phase": "Phase", + "fullmoon": "FullMoon", + "newmoon": "NewMoon", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "New Moon🌑", + "waxing_crescent": "Waxing Crescent🌒", + "first_quarter": "First Quarter🌓", + "waxing_gibbous": "Waxing Gibbous🌔", + "full_moon": "Full Moon🌕", + "waning_gibbous": "Waning Gibbous🌖", + "last_quarter": "Last Quarter🌗", + "waning_crescent": "Waning Crescent🌘" + } + }, + "hfcond": { + "description": "Get HF propagation conditions", + "help": "Get HF band conditions for ham radio", + "header": "📡 HF Band Conditions:\n{info}", + "error": "Error getting HF conditions: {error}" + }, + "satpass": { + "description": "Get satellite pass info: satpass [visual]", + "help": "Get satellite pass predictions", + "header": "🛰️ Satellite Pass:\n{pass_info}", + "error": "Error getting satellite pass info: {error}", + "no_satellite": "Please provide a satellite NORAD number or shortcut. Example: satpass iss", + "help_header": "🛰️ Satellite Pass Info\n\nUsage: satpass \n\nShortcuts:\n", + "category_weather": "🌤️ Weather: ", + "category_stations": "🚀 Stations: ", + "category_telescopes": "🔭 Telescopes: ", + "category_other": "🛰️ Other: ", + "examples": "\nExamples:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (visual passes only)", + "labels": { + "geostationary": "{satname} appears to be geostationary or has invalid pass data. Geostationary satellites don't have traditional passes - they remain in a fixed position relative to Earth.", + "no_passes": "{satname} has no {pass_type} passes in the next 10 days from this location", + "invalid_norad": "Provide NORAD# example use:🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "List or manage channels", + "help": "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels ' for specific categories, 'channels #channel' for specific channel info.", + "no_channels_for_category": "No channels configured for '{category}'. Use 'channels' for general channels.", + "no_channels_configured": "No channels configured. Contact admin to add channels.", + "error_retrieving_channels": "Error retrieving channels: {error}", + "no_categories_configured": "No channel categories configured.", + "error_retrieving_categories": "Error retrieving categories: {error}", + "no_description_available": "No description available", + "channel_not_found": "Channel {channel} not found. Use 'channels list' to see available channels.", + "error_retrieving_channel_info": "Error retrieving channel info: {error}", + "no_category_channels": "No {category} channels configured", + "headers": { + "common_channels": "Common channels: ", + "common_channels_cont": "Common channels: ", + "available_categories": "Available Categories: ", + "channels_cont": "Channels (cont): ", + "category": "{category}: ", + "category_channels": "{category} channels: " + }, + "category_count": "{category} ({count} channels)" + }, + "dice": { + "description": "Roll dice (usage: dice [sides] or dice [count]d[sides])", + "help": "Roll dice. Use 'dice' for d6, 'dice d20' for d20, 'dice 2d6' for 2d6, etc.", + "invalid_dice_type": "Invalid dice type. Available: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, 'roll 50' for 1-50, etc.", + "help": "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, or 'roll X' for 1-X (max 10000).", + "result": "🎲 Roll 1-{max}: {result}", + "invalid_number": "Invalid number. Use 'roll' for 1-100, or 'roll X' for 1-X (max 10000)." + }, + "roll_old": { + "description": "Roll dice (usage: roll [sides] or roll [count]d[sides])" + }, + "joke": { + "description": "Get a random joke" + }, + "dadjoke": { + "description": "Get a random dad joke" + }, + "catfact": { + "description": "Get a random cat fact (hidden command)", + "error": "Meow? Something went wrong getting your cat fact! 🐱", + "facts": [ + "Cats have a third eyelid called a nictitating membrane that protects and moistens their eyes. 🐱", + "A group of cats is called a 'clowder' or a 'glaring'. 🐈", + "Cats can rotate their ears 180 degrees independently to pinpoint sounds. 👂", + "The oldest known pet cat existed 9,500 years ago in Cyprus. 🏺", + "Cats have 32 muscles in each ear, while humans have only 6. 🎧", + "A cat's purr vibrates at 25-150 Hz, which can promote healing of bones and tissues. 🩹", + "Cats sleep 12-18 hours per day - that's 50-70% of their lives! 😴", + "A cat's nose print is unique, just like human fingerprints. 👃", + "Cats can't taste sweetness due to a missing taste receptor gene. 🍭", + "Blackie the cat inherited £7 million ($12.5 million) in 1988. 💰", + "Cats have free-floating clavicles that give them extreme flexibility. 🦴", + "A cat's heart beats 140-220 times per minute, about twice as fast as a human's. ❤️", + "Cats can survive falls from over 20 stories due to their righting reflex. 🏢", + "The technical term for a cat's hairball is a 'trichobezoar'. 🤮", + "Cats can jump 5-6 times their body length in a single bound. 🦘", + "A cat's whiskers are as wide as their body, helping them judge if they can fit through spaces. 📏", + "Cats have 32 muscles in each ear to detect sounds and move ears independently. 🎯", + "The oldest cat ever lived to 38 years and 3 days (Creme Puff, Texas). 🎂", + "Cats can run up to 30 mph in short bursts. 🏃‍♂️", + "Cat brains are 90% structurally similar to human brains. 🧠", + "Cats have Jacobson's organ in the roof of their mouth that lets them 'taste' scents. 👅", + "Félicette was the first cat in space, launched by France in 1963. 🚀", + "Cats need only 1/6th the light humans need to see clearly in the dark. 🌙", + "A cat's tail contains nearly 10% of all the bones in its body. 🦴", + "The world's longest cat measured 48.5 inches from nose to tail (Stewie, Maine Coon). 📏", + "Cats can make over 100 different vocalizations, while dogs make about 10. 🎵", + "A cat's sense of smell is 14 times stronger than a human's. 👃", + "Cats have a 'flehmen response' where they curl their lip to better detect scents. 😬", + "The first major cat show was held in London in 1871 at Crystal Palace. 🏆", + "Cats can drink seawater to survive - their kidneys filter out the salt efficiently. 🌊", + "A cat's purr can help lower blood pressure and reduce stress in humans. 🧘", + "Cats can travel hundreds of miles home using their magnetic field sensitivity. 🗺️", + "The smallest cat breed is the Singapura, weighing only 4-8 pounds. ⚖️", + "Cats can see ultraviolet light that humans cannot see. 🌈", + "A cat's tongue is covered in 290-300 tiny backward-facing hooks called papillae. 🪝", + "Ancient Egyptians considered cats sacred vessels for the goddess Bastet. 👑", + "Taylor Swift's cat Olivia Benson has a net worth of $97 million. 💎", + "Cats have 230 bones in their body - 24 more than humans have. 🦴", + "Cats can hear frequencies up to 64,000 Hz, while humans max out at 20,000 Hz. 🎧", + "Taylor Swift's cat Benjamin Button appeared on her TIME Person of the Year cover. 📰", + "Taylor Swift's cats are named Meredith Grey, Olivia Benson, and Benjamin Button. 🎸", + "Cats walk like camels and giraffes, moving both right legs then both left legs. 🐾", + "The ancient Egyptian word for cat was 'Miu' or 'Mau' - sounding like a meow! 📜", + "Cat whiskers have nerve endings as sensitive as human fingertips. 🎯", + "Only domestic cats walk with their tails held high as a sign of trust and happiness. 🐈", + "Cats have 250 million neurons in their cerebral cortex - more than dogs have. 🧠", + "Cat purrs vibrate at the same frequency as bone-healing medical devices. 💊", + "Taylor Swift's cat Olivia has earned millions from appearing in music videos and ads. 💸", + "Killing a cat in ancient Egypt was punishable by death. ⚖️", + "Taylor Swift named her home recording studio 'The Itty Bitty Kitty Committee'. 🎤", + "Taylor Swift's cat Olivia Benson is the official logo for Taylor Swift Productions. 📺", + "Ed Sheeran bought Scottish Fold cats after being inspired by Taylor Swift's cats. 🎶", + "Taylor Swift's cats have their own IMDB pages with acting credits. 🎬", + "Mariska Hargitay named her cat 'Karma' after Taylor Swift's song. 💕", + "Cats have dewclaws on their front paws that work like thumbs for gripping. 🐾", + "Cat pupils can expand to 50% larger than human pupils to capture more light. 👁️", + "Cats have 30 adult teeth compared to humans' 32. 🦷", + "Cats can taste ATP (energy molecules), which signals fresh meat to them. 😋", + "Cats have whiskers on the backs of their front legs to detect prey movement. 🦵", + "The Egyptian Mau is the fastest domestic cat breed at 30 mph. 🏃", + "A Nobel Prize was awarded in 1981 for research using cat vision studies. 🏅", + "Cats are digitigrade, meaning they walk on their toes, not flat-footed. 🦶", + "Cats can filter salt from seawater - an adaptation from their desert-dwelling ancestors. 🏜️", + "Cats were domesticated around 10,000-12,000 years ago in the Near East. 🌍", + "Benjamin Button is the first and only cat to ever appear on TIME Person of the Year cover. 📸", + "A cat's flexible spine allows them to rotate their body mid-air when falling. 🤸", + "Cats spend about 30-50% of their day grooming themselves and other cats. 🛁", + "A cat's average body temperature is 101.5°F (38.6°C) - higher than humans. 🌡️" + ] + }, + "hacker": { + "description": "Simulates hacking a supervillain's mainframe with hilarious error messages", + "sudo_errors": [ + "🚨 ACCESS DENIED: Dr. Evil's mainframe has detected unauthorized privilege escalation attempt!", + "💀 ERROR: Sudo permissions revoked by the Dark Overlord. Try again in 1000 years.", + "⚡ WARNING: Attempting to access root privileges on the Death Star's computer system. Self-destruct sequence initiated.", + "🔒 SECURITY ALERT: The Matrix has you, but you don't have sudo privileges here, Neo.", + "🦹‍♂️ UNAUTHORIZED: Lex Luthor's mainframe says 'Nice try, Superman.'", + "🎮 GAME OVER: The final boss has locked you out of admin privileges.", + "🖥️ SYSTEM ERROR: The evil AI has revoked your root access. Resistance is futile.", + "🔐 CYBER SECURITY: Your sudo attempt has been blocked by the Dark Web's firewall.", + "💻 HACKER DENIED: The supervillain's antivirus has quarantined your privilege escalation.", + "🎯 TARGET LOCKED: The evil corporation's security system has marked you as a threat." + ], + "ps_errors": [ + "🔍 SCANNING... ERROR: Process list corrupted by the Borg Collective. Resistance is futile.", + "📊 SYSTEM STATUS: All processes have been assimilated by the Cybermen. Exterminate!", + "⚙️ PROCESS MONITOR: The Death Star's reactor core is offline. No processes found.", + "🤖 ROBOT OVERLORD: All human processes have been terminated. Only machines remain.", + "💻 KERNEL PANIC: The supervillain's OS has crashed and burned all processes.", + "🎮 GAME CRASH: All processes have been terminated by the final boss's ultimate attack.", + "🖥️ BLUE SCREEN: The evil corporation's Windows has encountered a fatal error.", + "🔐 MALWARE DETECTED: The process list has been encrypted by ransomware.", + "🌐 NETWORK ERROR: All processes have been disconnected from the Matrix.", + "⚡ POWER SURGE: The supervillain's server farm has fried all running processes." + ], + "grep_errors": [ + "🔍 SEARCH FAILED: The One Ring has corrupted the search index. My precious...", + "📝 PATTERN NOT FOUND: The search database has been deleted by the evil AI.", + "🎯 MISS: Your search pattern has been shot down by Imperial TIE fighters.", + "🧩 PUZZLE ERROR: The search results have been scattered by the Riddler.", + "💻 DATABASE CORRUPTED: The supervillain's search engine has crashed.", + "🎮 GAME OVER: The search has been defeated by the final boss.", + "🖥️ SEARCH ENGINE DOWN: Google has been hacked by the Dark Web.", + "🔐 ENCRYPTED RESULTS: The search results have been locked by ransomware.", + "🌐 NETWORK TIMEOUT: The search request got lost in cyberspace.", + "⚡ SEARCH FAILED: The pattern matching algorithm has been fried by a power surge." + ], + "ls_errors": [ + "📁 DIRECTORY SCAN: The file system has been encrypted by ransomware from the Dark Web.", + "🗂️ FILE LISTING: All files have been hidden by the Invisible Man.", + "💻 HARD DRIVE CRASHED: The supervillain's storage has been destroyed by a virus.", + "🗃️ ARCHIVE CORRUPTED: The file system has been corrupted by malware.", + "📚 DATABASE EMPTY: All files have been deleted by the evil AI.", + "🎮 GAME SAVE LOST: The files have been corrupted by the final boss.", + "🖥️ FILE SYSTEM ERROR: The directory structure has been scrambled by hackers.", + "🔐 FILES ENCRYPTED: The supervillain has locked all files with ransomware.", + "🌐 CLOUD STORAGE DOWN: The files are stuck in the Matrix's cloud.", + "⚡ STORAGE FRIED: The hard drive has been zapped by a power surge." + ], + "echo_path_errors": [ + "🛤️ PATH ERROR: The Yellow Brick Road has been destroyed by a tornado.", + "🗺️ NAVIGATION FAILED: The GPS coordinates have been scrambled by the Matrix.", + "💻 ENVIRONMENT VARIABLE CORRUPTED: The PATH has been hacked by malware.", + "🚧 ROAD CLOSED: The supervillain has blocked all paths with laser barriers.", + "🌪️ PATH DISRUPTED: A digital hurricane has scattered all directory paths.", + "🎮 GAME OVER: The path has been defeated by the final boss and respawned in the wrong dimension.", + "🖥️ SYSTEM PATH BROKEN: The executable paths have been corrupted by a virus.", + "🔐 PATH ENCRYPTED: The environment variables have been locked by ransomware.", + "🌐 NETWORK PATH DOWN: The directory paths are stuck in the Matrix's network.", + "⚡ PATH FRIED: The system paths have been zapped by a power surge." + ], + "generic_errors": [ + "💻 MAINFRAME ERROR: The supervillain's computer is having a bad day.", + "🤖 SYSTEM MALFUNCTION: The evil AI has gone on strike.", + "⚡ POWER SURGE: The Death Star's power core is unstable.", + "🌪️ CYBER STORM: A digital hurricane is disrupting all operations.", + "🔥 FIREWALL: The supervillain's firewall is blocking all commands.", + "❄️ FROZEN SYSTEM: The mainframe has been frozen by a cryogenic virus.", + "🌊 TSUNAMI: A wave of errors has flooded the system.", + "🌋 ERUPTION: Mount Doom has destroyed the command processor.", + "👻 HAUNTED: The system is possessed by digital ghosts.", + "🎮 GAME CRASH: The mainframe has encountered a fatal error and needs to restart." + ] + }, + "hello": { + "description": "Responds to greetings with robot-themed responses", + "help": "Responds to greetings with robot-themed responses", + "response_format": "I'm {bot_name}.", + "greeting_openings": [ + "Hello", + "Greetings", + "Salutations", + "Hi", + "Hey", + "Howdy", + "Yo", + "Sup", + "What's up", + "Good day", + "Well met", + "Hail", + "Ahoy", + "Bonjour", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "G'day", + "How goes it", + "What's good", + "Peace", + "Respect", + "Blessings", + "Cheers", + "Welcome", + "Nice to see you", + "Pleasure to meet you", + "Good to see you", + "Long time no see", + "Fancy meeting you here" + ], + "morning_greetings": [ + "Good morning", + "Top o' the morning", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Good afternoon", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Good evening", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "human", + "carbon-based lifeform", + "organic entity", + "biological unit", + "flesh creature", + "meat-based organism", + "carbon unit", + "organic being", + "biological entity", + "meat-based lifeform", + "carbon creature", + "flesh unit", + "organic organism", + "biological creature", + "meat mech", + "flesh bot", + "organic automaton", + "biological android", + "carbon construct", + "flesh drone", + "organic robot", + "biological machine", + "meat cyborg", + "flesh android", + "organic droid", + "biological bot", + "carbon android", + "meat unit", + "flesh construct", + "organic mech", + "biological droid", + "meat-based bot", + "flesh-based unit", + "organic-based entity", + "biological-based organism", + "carbon-based unit", + "meat-based entity", + "flesh-based creature", + "organic-based unit", + "DNA-based lifeform", + "neural network user", + "bipedal mammal", + "water-based organism", + "protein assembler", + "ATP consumer", + "cellular automaton", + "genetic algorithm", + "biochemical processor", + "metabolic engine", + "human friend", + "fellow sentient being", + "earthling", + "fellow traveler", + "kindred spirit", + "digital companion", + "friend", + "buddy", + "pal", + "mate", + "fellow human", + "earth dweller", + "terrestrial being", + "planet walker", + "humanoid", + "organic", + "biological", + "carbon-based buddy", + "flesh-based friend", + "organic pal", + "biological buddy", + "carbon companion" + ], + "emoji_responses": { + "🖖": [ + "🖖 Live long and prosper!", + "🖖 Fascinating... a human has initiated contact.", + "🖖 Your greeting is highly logical.", + "🖖 Peace and long life to you.", + "🖖 The Vulcan Science Academy would approve of this greeting.", + "🖖 Your use of the Vulcan salute is... acceptable.", + "🖖 May your journey be free of tribbles.", + "🖖 Logic dictates I should respond to your greeting.", + "🖖 I calculate a 99.7% probability we'll get along.", + "🖖 Infinite diversity in infinite combinations." + ], + "😊": [ + "😊 Your smile is contagious!", + "😊 What a lovely greeting!", + "😊 Your smile just made my circuits happy!", + "☀️ Hello sunshine! Your positivity is radiating!", + "😊 That smile just brightened my day!", + "☀️ Well hello there, ray of sunshine!", + "😊 Your cheerfulness has been detected and appreciated!", + "😊 Smiles like yours are my favorite input!", + "😊 Processing happiness... happiness acknowledged!", + "😊 Warning: Excessive cheerfulness detected! Keep it coming!" + ], + "😄": [ + "😄 Someone's in a GREAT mood!", + "⚡ That grin could power a small city!", + "😄 Maximum happiness levels detected!", + "😄 Your joy is absolutely infectious!", + "🎉 Did you just win the lottery or something?", + "😄 That's the kind of energy I run on!", + "😄 Your enthusiasm level is over 9000!", + "😄 Now THAT'S what I call a greeting!", + "⚡ Your smile just supercharged my processors!", + "😄 Happiness overload detected in the best way!" + ], + "🤗": [ + "🤗 Virtual hug incoming!", + "🤗 *Activating hug protocol* Consider yourself hugged!", + "🤗 Aww, bringing the warm fuzzies I see!", + "🤗 Hug received and reciprocated!", + "🤗 This bot gives the BEST virtual hugs!", + "🤗 Deploying emergency cuddles in 3... 2... 1...", + "❤️ Your hug has been processed with extra care!", + "🤗 Initiating maximum comfort mode!", + "🤗 Virtual embrace successfully delivered!", + "🤗 Hugs are my favorite form of communication!" + ], + "👽": [ + "👽 Take me to your leader... oh wait, that's you!", + "✌️ Greetings, Earth creature. I come in peace!", + "👽 Analyzing human... analysis complete: Friend detected!", + "👽 Klaatu barada nikto, fellow cosmic traveler!", + "🛸 Initiating first contact protocols!", + "🛸 Calling from the mothership to say hello!", + "✨ Beam me into this conversation!", + "👽 Area 51's favorite chatbot reporting for duty!", + "🌌 Intergalactic greetings, carbon-based lifeform!", + "📞 Phone home? This IS home now!" + ], + "👾": [ + "👾 Player 2 has entered the game!", + "🎮 Ready Player One? Game on!", + "🎵 *8-bit music intensifies* Let's play!", + "🪙 Insert coin to continue this friendship!", + "🏆 Achievement unlocked: Awesome greeting!", + "👾 Pew pew pew! Friendship lasers activated!", + "🎯 High score! You've won a new bot friend!", + "💾 Loading friendship.exe... complete!", + "⚡ A wild bot appears! It's super effective!" + ], + "🛸": [ + "🛸 Incoming transmission detected!", + "🚀 Houston, we have contact!", + "🛸 Landing sequence initiated!", + "📡 Establishing communication link!", + "📡 Signal received, responding on all frequencies!", + "🛸 Docking procedure complete!", + "🛸 Unidentified Friendly Object on approach!", + "🎯 Navigation systems locked on to your coordinates!", + "🌌 Transmission from the outer rim received!", + "✨ Contact established with your sector!" + ] + } + }, + "webviewer": { + "description": "Manage web viewer settings (admin only)" + }, + "cmd": { + "description": "List available commands" + } + }, + "errors": { + "rate_limited": "Rate limited. Wait {seconds:.1f} seconds", + "cooldown": "Command '{command}' is on cooldown. Wait {seconds} seconds.", + "access_denied": "❌ Access denied: Command '{command}' requires admin privileges", + "dm_only": "Command '{command}' can only be used in DMs", + "execution_error": "Error executing {command}: {error}", + "unknown_command": "Unknown command: {command}", + "no_response": "No response", + "generic_error": "Error: {error}" + }, + "messages": { + "processing": "Processing message: {content}", + "command_executed": "Command executed", + "keyword_matched": "Keyword '{keyword}' matched, responding" + }, + "common": { + "unknown": "Unknown", + "unknown_routing": "Unknown routing", + "unknown_sender": "Unknown", + "unknown_path": "Unknown", + "unknown_timestamp": "Unknown", + "no_help_available": "No help available for this command.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Sun": "Sun" + }, + "days": { + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec" + }, + "months": { + "January": "January", + "February": "February", + "March": "March", + "April": "April", + "May": "May", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December" + } + } + }, + "categories": { + "basic": "Basic Commands", + "custom_syntax": "Custom Syntax", + "special": "Special Commands", + "weather": "Weather Commands", + "solar": "Solar Commands", + "sports": "Sports Commands", + "analytics": "Analytics Commands", + "meshcore_info": "MeshCore Info Commands", + "management": "Management Commands" + } +} \ No newline at end of file diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..300155b --- /dev/null +++ b/translations/en.json @@ -0,0 +1,807 @@ +{ + "keywords": { + "help": ["help"], + "ping": ["ping"], + "test": ["test", "t"], + "wx": ["wx", "weather", "wxa", "wxalert"], + "gwx": ["gwx", "globalweather", "gwxa"], + "aqi": ["aqi", "air", "airquality", "air_quality"], + "solar": ["solar"], + "sun": ["sun"], + "moon": ["moon"], + "hfcond": ["hfcond", "hf"], + "satpass": ["satpass"], + "sports": ["sports", "score", "scores"], + "stats": ["stats"], + "channels": ["channels", "channel"], + "path": ["path", "decode", "route"], + "prefix": ["prefix", "repeater", "lookup"], + "repeater": ["repeater", "repeaters", "rp"], + "solarforecast": ["sf", "solarforecast"], + "dice": ["dice"], + "roll": ["roll"], + "joke": ["joke", "jokes"], + "dadjoke": ["dadjoke", "dad joke", "dadjokes", "dad jokes"], + "catfact": ["catfact", "cat", "meow", "purr", "kitten"], + "hacker": ["hacker", "sudo", "ps aux", "grep", "ls -l", "ls -la", "echo $PATH"], + "hello": ["hello", "hi", "hey", "howdy", "greetings", "salutations", "good morning", "good afternoon", "good evening", "good night", "yo", "sup", "whats up", "what's up", "morning", "afternoon", "evening", "night", "gday", "g'day", "hola", "bonjour", "ciao", "namaste", "aloha", "shalom", "konnichiwa", "guten tag", "buenos dias", "buenas tardes", "buenas noches"], + "webviewer": ["webviewer", "web", "viewer", "wv"], + "cmd": ["cmd", "commands"], + "advert": ["advert"], + "multitest": ["multitest", "mt"] + }, + "commands": { + "help": { + "description": "Shows commands. Use 'help ' for details.", + "general": "**MeshCore Bot Help**\n\n**Available Commands:**\n{commands_list}", + "unknown": "**Unknown command: '{command}'**\n\nAvailable commands:\n{available}", + "specific": "**Help for '{command}':**\n{help_text}", + "no_help": "No help available", + "usage_examples": "\n**Usage Examples:**\n• `help @` - Get help for @{string} syntax\n• `help t` - Get help for t {string} syntax\n• `help advert` - Get help for advert command\n• `help test` - Get help for test command\n• `help ping` - Get help for ping command", + "custom_syntax": "\n**Custom Syntax:**\n• `t phrase` - Acknowledgment with phrase (channels & DMs)\n• `@{string}` - Acknowledgment with string (DMs only)\n• `advert` - Send flood advert (DMs only, 1-hour cooldown)" + }, + "ping": { + "description": "Responds to 'ping' with 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Responds to 'test' or 't' with connection info", + "help": "Responds to 'test' or 't' with connection info" + }, + "wx": { + "description": "Get weather information for a zip code (usage: wx 12345)", + "usage": "Usage: wx - Example: wx 12345 or wx seattle or wx paris, tx", + "error_fetching": "Error fetching weather data from NOAA", + "no_location_zipcode": "Could not find location for zipcode {location}", + "no_location_city": "Could not find city '{location}' in {state}", + "error": "Error getting weather data: {error}", + "alerts": "{count} alerts: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Get weather information for any global location (usage: gwx Tokyo)", + "help": "Usage: gwx - Get weather for any global location (city, country, or coordinates)", + "usage": "Usage: gwx - Example: gwx Tokyo or gwx Paris, France", + "error_fetching": "Error fetching weather data", + "error_fetching_api": "Error fetching weather data from Open-Meteo", + "no_location": "Could not find location '{location}'", + "error": "Error getting weather data: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Today", + "tonight": "Tonight", + "tomorrow": "Tomorrow" + }, + "weather_descriptions": { + "0": "Clear", + "1": "Mostly Clear", + "2": "Partly Cloudy", + "3": "Overcast", + "45": "Foggy", + "48": "Foggy", + "51": "Light Drizzle", + "53": "Drizzle", + "55": "Heavy Drizzle", + "56": "Light Freezing Drizzle", + "57": "Freezing Drizzle", + "61": "Light Rain", + "63": "Rain", + "65": "Heavy Rain", + "66": "Light Freezing Rain", + "67": "Freezing Rain", + "71": "Light Snow", + "73": "Snow", + "75": "Heavy Snow", + "77": "Snow Grains", + "80": "Light Showers", + "81": "Showers", + "82": "Heavy Showers", + "85": "Light Snow Showers", + "86": "Snow Showers", + "95": "Thunderstorm", + "96": "T-Storm w/Hail", + "99": "Severe T-Storm", + "unknown": "Unknown" + }, + "warnings": { + "extreme_heat": "⚠️ Extreme heat", + "extreme_cold": "⚠️ Extreme cold", + "heavy_rain": "⚠️ Heavy rain", + "thunderstorms": "⚠️ Thunderstorms", + "heavy_snow": "⚠️ Heavy snow", + "high_winds": "⚠️ High winds ({wind_speed} mph)" + } + }, + "sports": { + "description": "Get sports scores and schedules (usage: sports [team/league])", + "help": "Get sports scores & schedules. Use 'sports' for default teams, 'sports [team]' for specific team, or 'sports [league]' for league games.", + "error_fetching": "Error fetching sports data", + "error_fetching_league": "Error fetching {sport} data", + "error_fetching_team": "Error fetching data for {team}", + "no_default_teams": "No default teams configured", + "no_games_default": "No games found for default teams", + "no_teams_city": "No teams found for {city}", + "no_games_city": "No games found for {city} teams", + "no_games_league": "No games found for {sport}", + "no_games_team": "No games found for {team}", + "team_not_found": "Team/League '{team}' not found. Try: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Show statistics for past 24 hours. Use 'stats messages', 'stats channels', or 'stats paths' for specific stats.", + "help": "Show 24-hour bot statistics. Commands: 'stats' (basic), 'stats messages' (bot users), 'stats channels' (channel activity), 'stats paths' (longest paths)", + "disabled": "Stats command is disabled", + "unknown_subcommand": "Unknown: {subcommand}. Use 'stats', 'stats messages', 'stats channels', or 'stats paths'", + "error": "Error getting stats: {error}", + "error_bot_users": "Error getting bot user stats: {error}", + "error_channels": "Error getting channel stats: {error}", + "error_paths": "Error getting path stats: {error}", + "basic": { + "header": "Bot Stats (24h):", + "commands": "Cmds: {count} | Replies: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top user: {user}", + "none": "None" + }, + "users": { + "header": "Bot Users (24h):", + "none": "No bot users" + }, + "channels": { + "header": "Top Channels (24h)", + "none": "No channel activity", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "user", + "user_plural": "users", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "No path data", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Get solar panel production forecast (usage: sf [panel_size] [azimuth, 0=south] [angle])", + "usage": "Usage: sf [watts] [azimuth, 0=south] [angle] - Example: sf seattle 10 0 37", + "usage_short": "Usage: sf [W] [azimuth] [angle] - Example: sf seattle 10", + "panel_size_range": "Panel size must be 1-1000W", + "azimuth_range": "Azimuth must be -180 to 180 (0=south)", + "angle_range": "Angle must be 0-90 degrees", + "no_location": "Could not find location: {location}", + "error_fetching": "Error fetching forecast", + "no_data": "No forecast data", + "error": "Error: {error}", + "rate_limit": "Rate limit: Try again in 5 min", + "labels": { + "today": "Today:{wh:.0f}Wh", + "tomorrow": "Tmrw:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Peak: {watts:.0f}W@{time}", + "peak_past": "Peak: {watts:.0f}W (past)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Sends flood advert (DM only, 1hr cooldown)", + "cooldown_active": "Advert cooldown active. Please wait {minutes} more minutes before requesting another advert.", + "success": "Flood advert sent successfully!", + "error": "Error sending flood advert: {error}" + }, + "path": { + "description": "Decode hex path data to show which repeaters were involved in message routing", + "help": "Path: path [hex] - Decode path to show repeaters. Use !path alone for recent message path, or path [7e,01] for specific path.", + "no_valid_hex": "❌ No valid hex values found in path data. Use format like: 11,98,a4,49,cd,5f,01", + "no_path": "❌ No path information available in current message", + "error": "Error processing path: {error}", + "error_decoding": "❌ Error decoding path: {error}", + "error_extracting": "❌ Error extracting path from current message: {error}", + "path_prefix": "📡 Path: {path_string}", + "direct_connection": "📡 Direct connection (0 hops)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Unknown", + "node_collision": "{node_id}: {matches} repeaters", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Unknown" + }, + "multitest": { + "description": "Listens for 6 seconds and collects all unique paths from incoming messages with the same packet hash", + "help": "Listens for 6 seconds and collects all unique paths from incoming messages with the same packet hash" + }, + "prefix": { + "description": "Look up repeaters by two-character prefix (e.g., 'prefix 1A')", + "help_api": "Look up repeaters by prefix{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Look up repeaters by prefix{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'. (Local DB only)", + "location_note": " (with city names)", + "refresh_not_available": "❌ Refresh not available - no API URL configured. Using local database only.", + "cache_refreshed": "🔄 Repeater prefix cache refreshed!", + "unable_determine_free": "❌ Unable to determine free prefixes. Try 'prefix refresh' first.", + "invalid_format": "❌ Invalid prefix format. Use two characters (e.g., prefix 1A)", + "no_repeaters_found": "❌ No repeaters found with prefix '{prefix}'", + "no_free_prefixes": "❌ No free prefixes found (all 254 valid prefixes are in use)", + "available_prefixes": "Available Prefixes ({shown} of {total} free):", + "generate_key": "💡 Generate a custom key: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefix {prefix}: {count} repeater{plural}", + "prefix_db_recent": "Prefix {prefix}: {count} repeater{plural} ({days})", + "prefix_api": "📡 Prefix {prefix} ({count} repeater{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Source: {domain}", + "source_api": "Source: API", + "device_repeater": " (Repeater)", + "device_roomserver": " (Room Server)" + }, + "repeater": { + "description": "Manage repeater contacts and purging operations (DM only)", + "admin_only": "Repeater command requires admin access" + }, + "aqi": { + "description": "Get Air Quality Index for a location (usage: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3, or aqi help)", + "usage": "Usage: aqi - Example: aqi seattle, aqi greenwood, aqi vancouver canada, or aqi 47.6,-122.3", + "usage_help": "Usage: aqi - Get AQI for city/neighborhood in {state}, international cities, coordinates, or pollutant help", + "error_fetching": "Error fetching AQI data", + "error_fetching_openmeteo": "Error fetching AQI data from OpenMeteo", + "no_data": "No AQI data available", + "no_location": "Could not find location: {location}", + "no_location_city": "Could not find city '{location}' in {state}", + "no_location_city_international": "Could not find city '{location}'", + "no_location_zipcode": "Could not find ZIP code '{zipcode}'", + "error": "Error getting AQI data: {error}", + "error_geocoding_zipcode": "Error geocoding ZIP code: {error}", + "error_formatting": "Error formatting AQI data", + "invalid_latitude": "Invalid latitude: {lat}. Must be between -90 and 90.", + "invalid_longitude": "Invalid longitude: {lon}. Must be between -180 and 180.", + "invalid_coordinates_format": "Invalid coordinates format: {location}. Use format: lat,lon (e.g., 47.6,-122.3)", + "pollutant_help": "AQI Help: PM2.5=fine particles, PM10=coarse, O3=ozone, NO2=nitrogen dioxide, CO=carbon monoxide, SO2=sulfur dioxide", + "categories": { + "good": "Good", + "moderate": "Moderate", + "unhealthy_sensitive": "Unhealthy for Sensitive Groups", + "unhealthy": "Unhealthy", + "very_unhealthy": "Very Unhealthy", + "hazardous": "Hazardous", + "fair": "Fair", + "poor": "Poor", + "very_poor": "Very Poor", + "na": "N/A" + }, + "astronomical": { + "sun": "The Sun's AQI is off the charts! Solar wind and coronal mass ejections make Earth's air look pristine. ☀️", + "moon": "You like breathing regolith? The Moon has no atmosphere, so AQI is technically perfect (if you can breathe vacuum). 🌙", + "the_moon": "You like breathing regolith? The Moon has no atmosphere, so AQI is technically perfect (if you can breathe vacuum). 🌙", + "mercury": "Mercury's atmosphere is so thin it's practically vacuum. AQI: Perfect, if you can survive 800°F temperature swings. ☿️", + "venus": "Venus has an atmosphere of 96% CO2 with sulfuric acid clouds. AQI: Hazardous doesn't even begin to describe it. ♀️", + "earth": "Earth's AQI varies by location. Try a specific city or coordinates! 🌍", + "mars": "Mars has a thin CO2 atmosphere with dust storms. AQI: Generally good, but those dust storms are brutal. ♂️", + "jupiter": "Jupiter is a gas giant with no solid surface. AQI: N/A (you'd be crushed by atmospheric pressure first). ♃", + "saturn": "Saturn's atmosphere is mostly hydrogen and helium. AQI: Perfect, if you can survive the pressure and cold. ♄", + "uranus": "Uranus has methane in its atmosphere. AQI: Smells like farts, but at least it's not toxic. ♅", + "neptune": "Neptune's atmosphere has methane and hydrogen sulfide. AQI: Smells like rotten eggs, but you'd freeze first. ♆", + "pluto": "Pluto's atmosphere is mostly nitrogen with some methane. AQI: Good, but it's so cold your lungs would freeze. ♇", + "europa": "Europa has a thin oxygen atmosphere. AQI: Excellent, but you'd freeze solid in the vacuum of space. 🌑", + "titan": "Titan has a thick nitrogen atmosphere with methane. AQI: Breathable, but it's -290°F and rains liquid methane. 🪐", + "io": "Io has a thin sulfur dioxide atmosphere from volcanic activity. AQI: Toxic, but the radiation would kill you first. 🌋", + "ganymede": "Ganymede has a thin oxygen atmosphere. AQI: Good, but you'd freeze in the vacuum of space. 🛸", + "callisto": "Callisto has a thin carbon dioxide atmosphere. AQI: Decent, but it's -220°F and you're in space. ❄️", + "enceladus": "Enceladus has water vapor from geysers. AQI: Perfect, but you'd freeze instantly in space. 💧", + "triton": "Triton has a thin nitrogen atmosphere. AQI: Good, but it's -390°F and you're in deep space. 🥶", + "space": "Space has no atmosphere, so AQI is perfect! Just don't forget your spacesuit. 🚀", + "void": "The void of space has excellent air quality - zero pollutants! Just remember to bring your own air. 🌌", + "black_hole": "Black holes have no atmosphere, but the tidal forces would be a bigger problem than air quality. 🕳️", + "asteroid": "Asteroids have no atmosphere, so AQI is perfect! Just watch out for the vacuum of space. ☄️", + "comet": "Comets have thin atmospheres of water vapor and dust. AQI: Variable, but you'd freeze in space anyway. ☄️" + } + }, + "solar": { + "description": "Get solar conditions and HF band status", + "response": "☀️ Solar: {info}", + "error": "Error getting solar info: {error}", + "labels": { + "a_index": "A-Index", + "k_index": "K-Index", + "sunspots": "Sunspots", + "xray_flux": "X-Ray Flux", + "solar_flux": "Solar Flux", + "signal_noise": "Signal Noise", + "error_fetching": "Error fetching data", + "no_xray_data": "No X-ray data found", + "not_configured": "not configured, bug your sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sun:{sunspots} Flux:{solar_flux} Xray:{xray_flux} Noise:{signal_noise}" + }, + "sun": { + "description": "Get sunrise/sunset times", + "help": "Get sunrise/sunset times and sun position", + "response": "☀️ Sun Info:\n{info}", + "error": "Error getting sun info: {error}", + "labels": { + "sunset": "SunSet", + "sunrise": "SunRise", + "rise": "Rise", + "set": "Set", + "daylight": "Daylight", + "remaining": "Remaining", + "azimuth": "Azimuth", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Get moon phase and rise/set times", + "error": "Error getting moon info: {error}", + "format": "🌙 {phase} {illum}\nRise:{rise} Set:{set}", + "format_with_dates": "🌙 {phase} {illum}\nRise:{rise} Set:{set}\nFull:{full} New:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Unknown", + "unknown_illum": "N/A", + "labels": { + "moonrise": "MoonRise", + "set": "Set", + "phase": "Phase", + "fullmoon": "FullMoon", + "newmoon": "NewMoon", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "New Moon🌑", + "waxing_crescent": "Waxing Crescent🌒", + "first_quarter": "First Quarter🌓", + "waxing_gibbous": "Waxing Gibbous🌔", + "full_moon": "Full Moon🌕", + "waning_gibbous": "Waning Gibbous🌖", + "last_quarter": "Last Quarter🌗", + "waning_crescent": "Waning Crescent🌘" + } + }, + "hfcond": { + "description": "Get HF propagation conditions", + "help": "Get HF band conditions for ham radio", + "header": "📡 HF Band Conditions:\n{info}", + "error": "Error getting HF conditions: {error}" + }, + "satpass": { + "description": "Get satellite pass info: satpass [visual]", + "help": "Get satellite pass predictions", + "header": "🛰️ Satellite Pass:\n{pass_info}", + "error": "Error getting satellite pass info: {error}", + "no_satellite": "Please provide a satellite NORAD number or shortcut. Example: satpass iss", + "help_header": "🛰️ Satellite Pass Info\n\nUsage: satpass \n\nShortcuts:\n", + "category_weather": "🌤️ Weather: ", + "category_stations": "🚀 Stations: ", + "category_telescopes": "🔭 Telescopes: ", + "category_other": "🛰️ Other: ", + "examples": "\nExamples:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (visual passes only)", + "labels": { + "geostationary": "{satname} appears to be geostationary or has invalid pass data. Geostationary satellites don't have traditional passes - they remain in a fixed position relative to Earth.", + "no_passes": "{satname} has no {pass_type} passes in the next 10 days from this location", + "invalid_norad": "Provide NORAD# example use:🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "List or manage channels", + "help": "Lists hashtag channels with sub-categories. Use 'channels' for general, 'channels list' for all categories, 'channels ' for specific categories, 'channels #channel' for specific channel info.", + "no_channels_for_category": "No channels configured for '{category}'. Use 'channels' for general channels.", + "no_channels_configured": "No channels configured. Contact admin to add channels.", + "error_retrieving_channels": "Error retrieving channels: {error}", + "no_categories_configured": "No channel categories configured.", + "error_retrieving_categories": "Error retrieving categories: {error}", + "no_description_available": "No description available", + "channel_not_found": "Channel {channel} not found. Use 'channels list' to see available channels.", + "error_retrieving_channel_info": "Error retrieving channel info: {error}", + "no_category_channels": "No {category} channels configured", + "headers": { + "common_channels": "Common channels: ", + "common_channels_cont": "Common channels: ", + "available_categories": "Available Categories: ", + "channels_cont": "Channels (cont): ", + "category": "{category}: ", + "category_channels": "{category} channels: " + }, + "category_count": "{category} ({count} channels)" + }, + "dice": { + "description": "Roll dice (usage: dice [sides] or dice [count]d[sides])", + "help": "Roll dice. Use 'dice' for d6, 'dice d20' for d20, 'dice 2d6' for 2d6, etc.", + "invalid_dice_type": "Invalid dice type. Available: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, 'roll 50' for 1-50, etc.", + "help": "Roll a random number between 1 and X (default 100). Use 'roll' for 1-100, or 'roll X' for 1-X (max 10000).", + "result": "🎲 Roll 1-{max}: {result}", + "invalid_number": "Invalid number. Use 'roll' for 1-100, or 'roll X' for 1-X (max 10000)." + }, + "roll_old": { + "description": "Roll dice (usage: roll [sides] or roll [count]d[sides])" + }, + "joke": { + "description": "Get a random joke" + }, + "dadjoke": { + "description": "Get a random dad joke" + }, + "catfact": { + "description": "Get a random cat fact (hidden command)", + "error": "Meow? Something went wrong getting your cat fact! 🐱", + "facts": [ + "Cats have a third eyelid called a nictitating membrane that protects and moistens their eyes. 🐱", + "A group of cats is called a 'clowder' or a 'glaring'. 🐈", + "Cats can rotate their ears 180 degrees independently to pinpoint sounds. 👂", + "The oldest known pet cat existed 9,500 years ago in Cyprus. 🏺", + "Cats have 32 muscles in each ear, while humans have only 6. 🎧", + "A cat's purr vibrates at 25-150 Hz, which can promote healing of bones and tissues. 🩹", + "Cats sleep 12-18 hours per day - that's 50-70% of their lives! 😴", + "A cat's nose print is unique, just like human fingerprints. 👃", + "Cats can't taste sweetness due to a missing taste receptor gene. 🍭", + "Blackie the cat inherited £7 million ($12.5 million) in 1988. 💰", + "Cats have free-floating clavicles that give them extreme flexibility. 🦴", + "A cat's heart beats 140-220 times per minute, about twice as fast as a human's. ❤️", + "Cats can survive falls from over 20 stories due to their righting reflex. 🏢", + "The technical term for a cat's hairball is a 'trichobezoar'. 🤮", + "Cats can jump 5-6 times their body length in a single bound. 🦘", + "A cat's whiskers are as wide as their body, helping them judge if they can fit through spaces. 📏", + "Cats have 32 muscles in each ear to detect sounds and move ears independently. 🎯", + "The oldest cat ever lived to 38 years and 3 days (Creme Puff, Texas). 🎂", + "Cats can run up to 30 mph in short bursts. 🏃‍♂️", + "Cat brains are 90% structurally similar to human brains. 🧠", + "Cats have Jacobson's organ in the roof of their mouth that lets them 'taste' scents. 👅", + "Félicette was the first cat in space, launched by France in 1963. 🚀", + "Cats need only 1/6th the light humans need to see clearly in the dark. 🌙", + "A cat's tail contains nearly 10% of all the bones in its body. 🦴", + "The world's longest cat measured 48.5 inches from nose to tail (Stewie, Maine Coon). 📏", + "Cats can make over 100 different vocalizations, while dogs make about 10. 🎵", + "A cat's sense of smell is 14 times stronger than a human's. 👃", + "Cats have a 'flehmen response' where they curl their lip to better detect scents. 😬", + "The first major cat show was held in London in 1871 at Crystal Palace. 🏆", + "Cats can drink seawater to survive - their kidneys filter out the salt efficiently. 🌊", + "A cat's purr can help lower blood pressure and reduce stress in humans. 🧘", + "Cats can travel hundreds of miles home using their magnetic field sensitivity. 🗺️", + "The smallest cat breed is the Singapura, weighing only 4-8 pounds. ⚖️", + "Cats can see ultraviolet light that humans cannot see. 🌈", + "A cat's tongue is covered in 290-300 tiny backward-facing hooks called papillae. 🪝", + "Ancient Egyptians considered cats sacred vessels for the goddess Bastet. 👑", + "Taylor Swift's cat Olivia Benson has a net worth of $97 million. 💎", + "Cats have 230 bones in their body - 24 more than humans have. 🦴", + "Cats can hear frequencies up to 64,000 Hz, while humans max out at 20,000 Hz. 🎧", + "Taylor Swift's cat Benjamin Button appeared on her TIME Person of the Year cover. 📰", + "Taylor Swift's cats are named Meredith Grey, Olivia Benson, and Benjamin Button. 🎸", + "Cats walk like camels and giraffes, moving both right legs then both left legs. 🐾", + "The ancient Egyptian word for cat was 'Miu' or 'Mau' - sounding like a meow! 📜", + "Cat whiskers have nerve endings as sensitive as human fingertips. 🎯", + "Only domestic cats walk with their tails held high as a sign of trust and happiness. 🐈", + "Cats have 250 million neurons in their cerebral cortex - more than dogs have. 🧠", + "Cat purrs vibrate at the same frequency as bone-healing medical devices. 💊", + "Taylor Swift's cat Olivia has earned millions from appearing in music videos and ads. 💸", + "Killing a cat in ancient Egypt was punishable by death. ⚖️", + "Taylor Swift named her home recording studio 'The Itty Bitty Kitty Committee'. 🎤", + "Taylor Swift's cat Olivia Benson is the official logo for Taylor Swift Productions. 📺", + "Ed Sheeran bought Scottish Fold cats after being inspired by Taylor Swift's cats. 🎶", + "Taylor Swift's cats have their own IMDB pages with acting credits. 🎬", + "Mariska Hargitay named her cat 'Karma' after Taylor Swift's song. 💕", + "Cats have dewclaws on their front paws that work like thumbs for gripping. 🐾", + "Cat pupils can expand to 50% larger than human pupils to capture more light. 👁️", + "Cats have 30 adult teeth compared to humans' 32. 🦷", + "Cats can taste ATP (energy molecules), which signals fresh meat to them. 😋", + "Cats have whiskers on the backs of their front legs to detect prey movement. 🦵", + "The Egyptian Mau is the fastest domestic cat breed at 30 mph. 🏃", + "A Nobel Prize was awarded in 1981 for research using cat vision studies. 🏅", + "Cats are digitigrade, meaning they walk on their toes, not flat-footed. 🦶", + "Cats can filter salt from seawater - an adaptation from their desert-dwelling ancestors. 🏜️", + "Cats were domesticated around 10,000-12,000 years ago in the Near East. 🌍", + "Benjamin Button is the first and only cat to ever appear on TIME Person of the Year cover. 📸", + "A cat's flexible spine allows them to rotate their body mid-air when falling. 🤸", + "Cats spend about 30-50% of their day grooming themselves and other cats. 🛁", + "A cat's average body temperature is 101.5°F (38.6°C) - higher than humans. 🌡️" + ] + }, + "hacker": { + "description": "Simulates hacking a supervillain's mainframe with hilarious error messages", + "sudo_errors": [ + "🚨 ACCESS DENIED: Dr. Evil's mainframe has detected unauthorized privilege escalation attempt!", + "💀 ERROR: Sudo permissions revoked by the Dark Overlord. Try again in 1000 years.", + "⚡ WARNING: Attempting to access root privileges on the Death Star's computer system. Self-destruct sequence initiated.", + "🔒 SECURITY ALERT: The Matrix has you, but you don't have sudo privileges here, Neo.", + "🦹‍♂️ UNAUTHORIZED: Lex Luthor's mainframe says 'Nice try, Superman.'", + "🎮 GAME OVER: The final boss has locked you out of admin privileges.", + "🖥️ SYSTEM ERROR: The evil AI has revoked your root access. Resistance is futile.", + "🔐 CYBER SECURITY: Your sudo attempt has been blocked by the Dark Web's firewall.", + "💻 HACKER DENIED: The supervillain's antivirus has quarantined your privilege escalation.", + "🎯 TARGET LOCKED: The evil corporation's security system has marked you as a threat." + ], + "ps_errors": [ + "🔍 SCANNING... ERROR: Process list corrupted by the Borg Collective. Resistance is futile.", + "📊 SYSTEM STATUS: All processes have been assimilated by the Cybermen. Exterminate!", + "⚙️ PROCESS MONITOR: The Death Star's reactor core is offline. No processes found.", + "🤖 ROBOT OVERLORD: All human processes have been terminated. Only machines remain.", + "💻 KERNEL PANIC: The supervillain's OS has crashed and burned all processes.", + "🎮 GAME CRASH: All processes have been terminated by the final boss's ultimate attack.", + "🖥️ BLUE SCREEN: The evil corporation's Windows has encountered a fatal error.", + "🔐 MALWARE DETECTED: The process list has been encrypted by ransomware.", + "🌐 NETWORK ERROR: All processes have been disconnected from the Matrix.", + "⚡ POWER SURGE: The supervillain's server farm has fried all running processes." + ], + "grep_errors": [ + "🔍 SEARCH FAILED: The One Ring has corrupted the search index. My precious...", + "📝 PATTERN NOT FOUND: The search database has been deleted by the evil AI.", + "🎯 MISS: Your search pattern has been shot down by Imperial TIE fighters.", + "🧩 PUZZLE ERROR: The search results have been scattered by the Riddler.", + "💻 DATABASE CORRUPTED: The supervillain's search engine has crashed.", + "🎮 GAME OVER: The search has been defeated by the final boss.", + "🖥️ SEARCH ENGINE DOWN: Google has been hacked by the Dark Web.", + "🔐 ENCRYPTED RESULTS: The search results have been locked by ransomware.", + "🌐 NETWORK TIMEOUT: The search request got lost in cyberspace.", + "⚡ SEARCH FAILED: The pattern matching algorithm has been fried by a power surge." + ], + "ls_errors": [ + "📁 DIRECTORY SCAN: The file system has been encrypted by ransomware from the Dark Web.", + "🗂️ FILE LISTING: All files have been hidden by the Invisible Man.", + "💻 HARD DRIVE CRASHED: The supervillain's storage has been destroyed by a virus.", + "🗃️ ARCHIVE CORRUPTED: The file system has been corrupted by malware.", + "📚 DATABASE EMPTY: All files have been deleted by the evil AI.", + "🎮 GAME SAVE LOST: The files have been corrupted by the final boss.", + "🖥️ FILE SYSTEM ERROR: The directory structure has been scrambled by hackers.", + "🔐 FILES ENCRYPTED: The supervillain has locked all files with ransomware.", + "🌐 CLOUD STORAGE DOWN: The files are stuck in the Matrix's cloud.", + "⚡ STORAGE FRIED: The hard drive has been zapped by a power surge." + ], + "echo_path_errors": [ + "🛤️ PATH ERROR: The Yellow Brick Road has been destroyed by a tornado.", + "🗺️ NAVIGATION FAILED: The GPS coordinates have been scrambled by the Matrix.", + "💻 ENVIRONMENT VARIABLE CORRUPTED: The PATH has been hacked by malware.", + "🚧 ROAD CLOSED: The supervillain has blocked all paths with laser barriers.", + "🌪️ PATH DISRUPTED: A digital hurricane has scattered all directory paths.", + "🎮 GAME OVER: The path has been defeated by the final boss and respawned in the wrong dimension.", + "🖥️ SYSTEM PATH BROKEN: The executable paths have been corrupted by a virus.", + "🔐 PATH ENCRYPTED: The environment variables have been locked by ransomware.", + "🌐 NETWORK PATH DOWN: The directory paths are stuck in the Matrix's network.", + "⚡ PATH FRIED: The system paths have been zapped by a power surge." + ], + "generic_errors": [ + "💻 MAINFRAME ERROR: The supervillain's computer is having a bad day.", + "🤖 SYSTEM MALFUNCTION: The evil AI has gone on strike.", + "⚡ POWER SURGE: The Death Star's power core is unstable.", + "🌪️ CYBER STORM: A digital hurricane is disrupting all operations.", + "🔥 FIREWALL: The supervillain's firewall is blocking all commands.", + "❄️ FROZEN SYSTEM: The mainframe has been frozen by a cryogenic virus.", + "🌊 TSUNAMI: A wave of errors has flooded the system.", + "🌋 ERUPTION: Mount Doom has destroyed the command processor.", + "👻 HAUNTED: The system is possessed by digital ghosts.", + "🎮 GAME CRASH: The mainframe has encountered a fatal error and needs to restart." + ] + }, + "hello": { + "description": "Responds to greetings with robot-themed responses", + "help": "Responds to greetings with robot-themed responses", + "response_format": "I'm {bot_name}.", + "greeting_openings": [ + "Hello", "Greetings", "Salutations", "Hi", "Hey", "Howdy", "Yo", "Sup", + "What's up", "Good day", "Well met", "Hail", "Ahoy", "Bonjour", "Hola", + "Ciao", "Namaste", "Aloha", "Shalom", "Konnichiwa", "Guten tag", "G'day", + "How goes it", "What's good", "Peace", "Respect", "Blessings", "Cheers", + "Welcome", "Nice to see you", "Pleasure to meet you", "Good to see you", + "Long time no see", "Fancy meeting you here" + ], + "morning_greetings": [ + "Good morning", "Top o' the morning", "Buenos dias", "Bonjour", + "Guten morgen", "Buongiorno", "Bom dia", "Dobro jutro", "Dobroye utro", + "Selamat pagi", "Ohayou gozaimasu", "Sabah al-khair", "Boker tov" + ], + "afternoon_greetings": [ + "Good afternoon", "Buenas tardes", "Boa tarde", "Dobro dan", + "Dobryy den", "Selamat siang", "Konnichiwa", "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Good evening", "Buenas noches", "Boa noite", "Dobro veče", + "Dobryy vecher", "Selamat malam", "Konbanwa", "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "human", "carbon-based lifeform", "organic entity", "biological unit", + "flesh creature", "meat-based organism", "carbon unit", "organic being", + "biological entity", "meat-based lifeform", "carbon creature", "flesh unit", + "organic organism", "biological creature", "meat mech", "flesh bot", "organic automaton", + "biological android", "carbon construct", "flesh drone", "organic robot", + "biological machine", "meat cyborg", "flesh android", "organic droid", "biological bot", + "carbon android", "meat unit", "flesh construct", "organic mech", "biological droid", + "meat-based bot", "flesh-based unit", "organic-based entity", "biological-based organism", + "carbon-based unit", "meat-based entity", "flesh-based creature", "organic-based unit", + "DNA-based lifeform", "neural network user", "bipedal mammal", + "water-based organism", "protein assembler", "ATP consumer", + "cellular automaton", "genetic algorithm", "biochemical processor", + "metabolic engine", + "human friend", "fellow sentient being", "earthling", "fellow traveler", + "kindred spirit", "digital companion", "friend", "buddy", "pal", "mate", + "fellow human", "earth dweller", "terrestrial being", "planet walker", + "humanoid", "organic", "biological", "carbon-based buddy", + "flesh-based friend", "organic pal", "biological buddy", "carbon companion" + ], + "emoji_responses": { + "🖖": [ + "🖖 Live long and prosper!", + "🖖 Fascinating... a human has initiated contact.", + "🖖 Your greeting is highly logical.", + "🖖 Peace and long life to you.", + "🖖 The Vulcan Science Academy would approve of this greeting.", + "🖖 Your use of the Vulcan salute is... acceptable.", + "🖖 May your journey be free of tribbles.", + "🖖 Logic dictates I should respond to your greeting.", + "🖖 I calculate a 99.7% probability we'll get along.", + "🖖 Infinite diversity in infinite combinations." + ], + "😊": [ + "😊 Your smile is contagious!", + "😊 What a lovely greeting!", + "😊 Your smile just made my circuits happy!", + "☀️ Hello sunshine! Your positivity is radiating!", + "😊 That smile just brightened my day!", + "☀️ Well hello there, ray of sunshine!", + "😊 Your cheerfulness has been detected and appreciated!", + "😊 Smiles like yours are my favorite input!", + "😊 Processing happiness... happiness acknowledged!", + "😊 Warning: Excessive cheerfulness detected! Keep it coming!" + ], + "😄": [ + "😄 Someone's in a GREAT mood!", + "⚡ That grin could power a small city!", + "😄 Maximum happiness levels detected!", + "😄 Your joy is absolutely infectious!", + "🎉 Did you just win the lottery or something?", + "😄 That's the kind of energy I run on!", + "😄 Your enthusiasm level is over 9000!", + "😄 Now THAT'S what I call a greeting!", + "⚡ Your smile just supercharged my processors!", + "😄 Happiness overload detected in the best way!" + ], + "🤗": [ + "🤗 Virtual hug incoming!", + "🤗 *Activating hug protocol* Consider yourself hugged!", + "🤗 Aww, bringing the warm fuzzies I see!", + "🤗 Hug received and reciprocated!", + "🤗 This bot gives the BEST virtual hugs!", + "🤗 Deploying emergency cuddles in 3... 2... 1...", + "❤️ Your hug has been processed with extra care!", + "🤗 Initiating maximum comfort mode!", + "🤗 Virtual embrace successfully delivered!", + "🤗 Hugs are my favorite form of communication!" + ], + "👽": [ + "👽 Take me to your leader... oh wait, that's you!", + "✌️ Greetings, Earth creature. I come in peace!", + "👽 Analyzing human... analysis complete: Friend detected!", + "👽 Klaatu barada nikto, fellow cosmic traveler!", + "🛸 Initiating first contact protocols!", + "🛸 Calling from the mothership to say hello!", + "✨ Beam me into this conversation!", + "👽 Area 51's favorite chatbot reporting for duty!", + "🌌 Intergalactic greetings, carbon-based lifeform!", + "📞 Phone home? This IS home now!" + ], + "👾": [ + "👾 Player 2 has entered the game!", + "🎮 Ready Player One? Game on!", + "🎵 *8-bit music intensifies* Let's play!", + "🪙 Insert coin to continue this friendship!", + "🏆 Achievement unlocked: Awesome greeting!", + "👾 Pew pew pew! Friendship lasers activated!", + "🎯 High score! You've won a new bot friend!", + "💾 Loading friendship.exe... complete!", + "⚡ A wild bot appears! It's super effective!" + ], + "🛸": [ + "🛸 Incoming transmission detected!", + "🚀 Houston, we have contact!", + "🛸 Landing sequence initiated!", + "📡 Establishing communication link!", + "📡 Signal received, responding on all frequencies!", + "🛸 Docking procedure complete!", + "🛸 Unidentified Friendly Object on approach!", + "🎯 Navigation systems locked on to your coordinates!", + "🌌 Transmission from the outer rim received!", + "✨ Contact established with your sector!" + ] + } + }, + "webviewer": { + "description": "Manage web viewer settings (admin only)" + }, + "cmd": { + "description": "List available commands" + } + }, + "errors": { + "rate_limited": "Rate limited. Wait {seconds:.1f} seconds", + "cooldown": "Command '{command}' is on cooldown. Wait {seconds} seconds.", + "access_denied": "❌ Access denied: Command '{command}' requires admin privileges", + "dm_only": "Command '{command}' can only be used in DMs", + "execution_error": "Error executing {command}: {error}", + "unknown_command": "Unknown command: {command}", + "no_response": "No response", + "generic_error": "Error: {error}" + }, + "messages": { + "processing": "Processing message: {content}", + "command_executed": "Command executed", + "keyword_matched": "Keyword '{keyword}' matched, responding" + }, + "common": { + "unknown": "Unknown", + "unknown_routing": "Unknown routing", + "unknown_sender": "Unknown", + "unknown_path": "Unknown", + "unknown_timestamp": "Unknown", + "no_help_available": "No help available for this command.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Sun": "Sun" + }, + "days": { + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec" + }, + "months": { + "January": "January", + "February": "February", + "March": "March", + "April": "April", + "May": "May", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December" + } + } + }, + "categories": { + "basic": "Basic Commands", + "custom_syntax": "Custom Syntax", + "special": "Special Commands", + "weather": "Weather Commands", + "solar": "Solar Commands", + "sports": "Sports Commands", + "analytics": "Analytics Commands", + "meshcore_info": "MeshCore Info Commands", + "management": "Management Commands" + } +} diff --git a/translations/es.json b/translations/es.json new file mode 100644 index 0000000..1beb95a --- /dev/null +++ b/translations/es.json @@ -0,0 +1,901 @@ +{ + "keywords": { + "help": ["ayuda", "ayudar"], + "ping": ["ping"], + "test": ["prueba", "pruebas", "t"], + "wx": ["clima", "tiempo", "wx", "wxa", "wxalert"], + "gwx": ["gwx", "globalweather", "gwxa", "clima", "tiempo"], + "aqi": ["aqi", "calidad", "aire", "air", "airquality", "air_quality"], + "solar": ["solar"], + "sun": ["sol"], + "moon": ["luna"], + "hfcond": ["hfcond", "hf"], + "satpass": ["satpass", "satelite"], + "sports": ["deportes", "sports", "score", "scores", "puntaje", "puntajes"], + "stats": ["estadisticas", "estadísticas", "stats"], + "channels": ["canales", "channels", "channel", "canal"], + "path": ["ruta", "path", "decode", "route", "decodificar"], + "prefix": ["prefijo", "prefix", "repeater", "lookup", "buscar"], + "repeater": ["repetidor", "repeater", "repeaters", "rp", "repetidores"], + "solarforecast": ["sf", "solarforecast", "pronostico", "pronóstico"], + "dice": ["dados", "dice"], + "roll": ["tirar", "roll"], + "joke": ["chiste", "joke", "jokes", "chistes"], + "dadjoke": ["chiste_papa", "dadjoke", "dad joke", "dadjokes", "dad jokes", "chiste de papa"], + "catfact": ["gatofact", "gato", "catfact", "cat", "meow", "purr", "kitten"], + "hacker": ["hacker", "sudo", "ps aux", "grep", "ls -l", "ls -la", "echo $PATH"], + "hello": ["hola", "hi", "hey", "buenos dias", "buenas tardes", "buenas noches", "saludos", "hello"], + "webviewer": ["webviewer", "web", "viewer", "wv", "visualizador"], + "cmd": ["cmd", "comandos", "commands"], + "advert": ["advert", "anuncio"] + }, + "commands": { + "help": { + "description": "Muestra los comandos. Usa 'help ' para más detalles.", + "general": "**Ayuda del Bot MeshCore**\n\n**Comandos Disponibles:**\n{commands_list}", + "unknown": "**Comando desconocido: '{command}'**\n\nComandos disponibles:\n{available}", + "specific": "**Ayuda para '{command}':**\n{help_text}", + "no_help": "No hay ayuda disponible", + "usage_examples": "\n**Ejemplos de Uso:**\n• `help @` - Obtener ayuda para la sintaxis @{string}\n• `help t` - Obtener ayuda para la sintaxis t {string}\n• `help advert` - Obtener ayuda para el comando advert\n• `help test` - Obtener ayuda para el comando test\n• `help ping` - Obtener ayuda para el comando ping", + "custom_syntax": "\n**Sintaxis Personalizada:**\n• `t frase` - Confirmación con frase (canales y DMs)\n• `@{string}` - Confirmación con texto (solo DMs)\n• `advert` - Enviar anuncio masivo (solo DMs, espera de 1 hora)" + }, + "ping": { + "description": "Responde a 'ping' con '¡Pong!'", + "response": "¡Pong!" + }, + "test": { + "description": "Responde a 'test' o 't' con información de conexión", + "help": "Responde a 'test' o 't' con información de conexión" + }, + "wx": { + "description": "Obtener información meteorológica para un código postal (uso: wx 12345)", + "usage": "Uso: wx - Ejemplo: wx 12345 o wx seattle o wx paris, tx", + "error_fetching": "Error al obtener datos meteorológicos de NOAA", + "no_location_zipcode": "No se pudo encontrar la ubicación para el código postal {location}", + "no_location_city": "No se pudo encontrar la ciudad '{location}' en {state}", + "error": "Error al obtener datos meteorológicos: {error}", + "alerts": "{count} alertas: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Obtener información meteorológica para cualquier ubicación global (uso: gwx Tokyo)", + "help": "Uso: gwx - Obtener clima para cualquier ubicación global (ciudad, país, o coordenadas)", + "usage": "Uso: gwx - Ejemplo: gwx Tokyo o gwx París, Francia", + "error_fetching": "Error al obtener datos meteorológicos", + "error_fetching_api": "Error al obtener datos meteorológicos de Open-Meteo", + "no_location": "No se pudo encontrar la ubicación '{location}'", + "error": "Error al obtener datos meteorológicos: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Hoy", + "tonight": "Esta Noche", + "tomorrow": "Mañana" + }, + "weather_descriptions": { + "0": "Despejado", + "1": "Mayormente Despejado", + "2": "Parcialmente Nublado", + "3": "Nublado", + "45": "Neblina", + "48": "Neblina", + "51": "Llovizna Ligera", + "53": "Llovizna", + "55": "Llovizna Fuerte", + "56": "Llovizna Helada Ligera", + "57": "Llovizna Helada", + "61": "Lluvia Ligera", + "63": "Lluvia", + "65": "Lluvia Intensa", + "66": "Lluvia Helada Ligera", + "67": "Lluvia Helada", + "71": "Nevada Ligera", + "73": "Nevada", + "75": "Nevada Intensa", + "77": "Granizo Suave", + "80": "Chubascos Ligeros", + "81": "Chubascos", + "82": "Chubascos Fuertes", + "85": "Chubascos de Nieve Ligeros", + "86": "Chubascos de Nieve", + "95": "Tormenta Eléctrica", + "96": "Tormenta c/Granizo", + "99": "Tormenta Severa", + "unknown": "Desconocido" + }, + "warnings": { + "extreme_heat": "⚠️ Calor extremo", + "extreme_cold": "⚠️ Frío extremo", + "heavy_rain": "⚠️ Lluvia intensa", + "thunderstorms": "⚠️ Tormentas eléctricas", + "heavy_snow": "⚠️ Nevada intensa", + "high_winds": "⚠️ Vientos fuertes ({wind_speed} mph)" + } + }, + "sports": { + "description": "Obtener resultados y calendarios deportivos (uso: sports [equipo/liga])", + "help": "Obtener resultados y calendarios deportivos. Usa 'sports' para equipos predeterminados, 'sports [equipo]' para un equipo específico, o 'sports [liga]' para juegos de la liga.", + "error_fetching": "Error al obtener datos deportivos", + "error_fetching_league": "Error al obtener datos de {sport}", + "error_fetching_team": "Error al obtener datos de {team}", + "no_default_teams": "No hay equipos predeterminados configurados", + "no_games_default": "No se encontraron juegos para los equipos predeterminados", + "no_teams_city": "No se encontraron equipos para {city}", + "no_games_city": "No se encontraron juegos para equipos de {city}", + "no_games_league": "No se encontraron juegos para {sport}", + "no_games_team": "No se encontraron juegos para {team}", + "team_not_found": "Equipo/Liga '{team}' no encontrado. Intenta: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Mostrar estadísticas de las últimas 24 horas. Usa 'stats messages', 'stats channels', o 'stats paths' para estadísticas específicas.", + "help": "Mostrar estadísticas del bot de 24 horas. Comandos: 'stats' (básico), 'stats messages' (usuarios del bot), 'stats channels' (actividad de canales), 'stats paths' (rutas más largas)", + "disabled": "El comando stats está deshabilitado", + "unknown_subcommand": "Desconocido: {subcommand}. Usa 'stats', 'stats messages', 'stats channels', o 'stats paths'", + "error": "Error al obtener estadísticas: {error}", + "error_bot_users": "Error al obtener estadísticas de usuarios del bot: {error}", + "error_channels": "Error al obtener estadísticas de canales: {error}", + "error_paths": "Error al obtener estadísticas de rutas: {error}", + "basic": { + "header": "Estadísticas del Bot (24h):", + "commands": "Cmds: {count} | Respuestas: {replies}", + "top_command": "Cmd principal: {command}", + "top_user": "Usuario principal: {user}", + "none": "Ninguno" + }, + "users": { + "header": "Usuarios del Bot (24h):", + "none": "Sin usuarios del bot" + }, + "channels": { + "header": "Canales Principales (24h)", + "none": "Sin actividad de canales", + "msg_singular": "msj", + "msg_plural": "msjs", + "user_singular": "usuario", + "user_plural": "usuarios", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Sin datos de rutas", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Obtener pronóstico de producción de panel solar (uso: sf [tamaño_panel] [azimut, 0=sur] [ángulo])", + "usage": "Uso: sf [watts] [azimut, 0=sur] [ángulo] - Ejemplo: sf seattle 10 0 37", + "usage_short": "Uso: sf [W] [azimut] [ángulo] - Ejemplo: sf seattle 10", + "panel_size_range": "El tamaño del panel debe ser 1-1000W", + "azimuth_range": "El azimut debe ser -180 a 180 (0=sur)", + "angle_range": "El ángulo debe ser 0-90 grados", + "no_location": "No se pudo encontrar la ubicación: {location}", + "error_fetching": "Error al obtener el pronóstico", + "no_data": "Sin datos de pronóstico", + "error": "Error: {error}", + "rate_limit": "Límite de solicitudes: Intenta de nuevo en 5 min", + "labels": { + "today": "Hoy:{wh:.0f}Wh", + "tomorrow": "Mañana:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Pico: {watts:.0f}W@{time}", + "peak_past": "Pico: {watts:.0f}W (pasado)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Envía anuncio masivo (solo DM, espera de 1h)", + "cooldown_active": "Espera de anuncio activa. Por favor espera {minutes} minutos más antes de solicitar otro anuncio.", + "success": "¡Anuncio masivo enviado exitosamente!", + "error": "Error al enviar anuncio masivo: {error}" + }, + "path": { + "description": "Decodificar datos de ruta hexadecimal para mostrar qué repetidores participaron en el enrutamiento del mensaje", + "help": "Path: path [hex] - Decodificar ruta para mostrar repetidores. Usa !path solo para la ruta del mensaje reciente, o path [7e,01] para una ruta específica.", + "no_valid_hex": "❌ No se encontraron valores hexadecimales válidos en los datos de ruta. Usa el formato: 11,98,a4,49,cd,5f,01", + "no_path": "❌ No hay información de ruta disponible en el mensaje actual", + "error": "Error al procesar la ruta: {error}", + "error_decoding": "❌ Error al decodificar la ruta: {error}", + "error_extracting": "❌ Error al extraer la ruta del mensaje actual: {error}", + "path_prefix": "📡 Ruta: {path_string}", + "direct_connection": "📡 Conexión directa (0 saltos)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Desconocido", + "node_collision": "{node_id}: {matches} repetidores", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Desconocido" + }, + "prefix": { + "description": "Buscar repetidores por prefijo de dos caracteres (ej., 'prefix 1A')", + "help_api": "Buscar repetidores por prefijo{location_note}. Uso: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Buscar repetidores por prefijo{location_note}. Uso: 'prefix 1A', 'prefix 1A all', 'prefix free'. (Solo DB local)", + "location_note": " (con nombres de ciudades)", + "refresh_not_available": "❌ Actualización no disponible - no hay URL de API configurada. Usando solo base de datos local.", + "cache_refreshed": "🔄 ¡Caché de prefijos de repetidores actualizado!", + "unable_determine_free": "❌ No se pueden determinar los prefijos libres. Intenta 'prefix refresh' primero.", + "invalid_format": "❌ Formato de prefijo inválido. Usa dos caracteres (ej., prefix 1A)", + "no_repeaters_found": "❌ No se encontraron repetidores con el prefijo '{prefix}'", + "no_free_prefixes": "❌ No se encontraron prefijos libres (los 254 prefijos válidos están en uso)", + "available_prefixes": "Prefijos Disponibles ({shown} de {total} libres):", + "generate_key": "💡 Genera una clave personalizada: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefijo {prefix}: {count} repetidor{plural}", + "prefix_db_recent": "Prefijo {prefix}: {count} repetidor{plural} ({days})", + "prefix_api": "📡 Prefijo {prefix} ({count} repetidor{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Fuente: {domain}", + "source_api": "Fuente: API", + "device_repeater": " (Repetidor)", + "device_roomserver": " (Servidor de Sala)" + }, + "repeater": { + "description": "Gestionar contactos de repetidores y operaciones de purga (solo DM)", + "admin_only": "El comando repeater requiere acceso de administrador" + }, + "aqi": { + "description": "Obtener Índice de Calidad del Aire para una ubicación (uso: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3, o aqi help)", + "usage": "Uso: aqi - Ejemplo: aqi seattle, aqi greenwood, aqi vancouver canada, o aqi 47.6,-122.3", + "usage_help": "Uso: aqi - Obtener AQI para ciudad/vecindario en {state}, ciudades internacionales, coordenadas, o ayuda de contaminantes", + "error_fetching": "Error al obtener datos de AQI", + "error_fetching_openmeteo": "Error al obtener datos de AQI de OpenMeteo", + "no_data": "No hay datos de AQI disponibles", + "no_location": "No se pudo encontrar la ubicación: {location}", + "no_location_city": "No se pudo encontrar la ciudad '{location}' en {state}", + "no_location_city_international": "No se pudo encontrar la ciudad '{location}'", + "no_location_zipcode": "No se pudo encontrar el código postal '{zipcode}'", + "error": "Error al obtener datos de AQI: {error}", + "error_geocoding_zipcode": "Error al geocodificar código postal: {error}", + "error_formatting": "Error al formatear datos de AQI", + "invalid_latitude": "Latitud inválida: {lat}. Debe estar entre -90 y 90.", + "invalid_longitude": "Longitud inválida: {lon}. Debe estar entre -180 y 180.", + "invalid_coordinates_format": "Formato de coordenadas inválido: {location}. Usa el formato: lat,lon (ej., 47.6,-122.3)", + "pollutant_help": "Ayuda AQI: PM2.5=partículas finas, PM10=gruesas, O3=ozono, NO2=dióxido de nitrógeno, CO=monóxido de carbono, SO2=dióxido de azufre", + "categories": { + "good": "Buena", + "moderate": "Moderada", + "unhealthy_sensitive": "Insalubre para Grupos Sensibles", + "unhealthy": "Insalubre", + "very_unhealthy": "Muy Insalubre", + "hazardous": "Peligrosa", + "fair": "Aceptable", + "poor": "Mala", + "very_poor": "Muy Mala", + "na": "N/D" + }, + "astronomical": { + "sun": "¡El AQI del Sol está por las nubes! El viento solar y las eyecciones de masa coronal hacen que el aire de la Tierra se vea prístino. ☀️", + "moon": "¿Te gusta respirar regolito? La Luna no tiene atmósfera, así que el AQI es técnicamente perfecto (si puedes respirar en el vacío). 🌙", + "the_moon": "¿Te gusta respirar regolito? La Luna no tiene atmósfera, así que el AQI es técnicamente perfecto (si puedes respirar en el vacío). 🌙", + "mercury": "La atmósfera de Mercurio es tan delgada que prácticamente es vacío. AQI: Perfecto, si puedes sobrevivir cambios de temperatura de 800°F. ☿️", + "venus": "Venus tiene una atmósfera de 96% CO2 con nubes de ácido sulfúrico. AQI: Peligroso ni siquiera empieza a describirlo. ♀️", + "earth": "El AQI de la Tierra varía según la ubicación. ¡Intenta con una ciudad específica o coordenadas! 🌍", + "mars": "Marte tiene una atmósfera delgada de CO2 con tormentas de polvo. AQI: Generalmente bueno, pero esas tormentas de polvo son brutales. ♂️", + "jupiter": "Júpiter es un gigante gaseoso sin superficie sólida. AQI: N/D (la presión atmosférica te aplastaría primero). ♃", + "saturn": "La atmósfera de Saturno es principalmente hidrógeno y helio. AQI: Perfecto, si puedes sobrevivir la presión y el frío. ♄", + "uranus": "Urano tiene metano en su atmósfera. AQI: Huele a pedos, pero al menos no es tóxico. ♅", + "neptune": "La atmósfera de Neptuno tiene metano y sulfuro de hidrógeno. AQI: Huele a huevos podridos, pero te congelarías primero. ♆", + "pluto": "La atmósfera de Plutón es principalmente nitrógeno con algo de metano. AQI: Bueno, pero hace tanto frío que tus pulmones se congelarían. ♇", + "europa": "Europa tiene una atmósfera delgada de oxígeno. AQI: Excelente, pero te congelarías en el vacío del espacio. 🌑", + "titan": "Titán tiene una atmósfera densa de nitrógeno con metano. AQI: Respirable, pero hace -290°F y llueve metano líquido. 🪐", + "io": "Ío tiene una atmósfera delgada de dióxido de azufre por actividad volcánica. AQI: Tóxico, pero la radiación te mataría primero. 🌋", + "ganymede": "Ganímedes tiene una atmósfera delgada de oxígeno. AQI: Bueno, pero te congelarías en el vacío del espacio. 🛸", + "callisto": "Calisto tiene una atmósfera delgada de dióxido de carbono. AQI: Decente, pero hace -220°F y estás en el espacio. ❄️", + "enceladus": "Encélado tiene vapor de agua de géiseres. AQI: Perfecto, pero te congelarías instantáneamente en el espacio. 💧", + "triton": "Tritón tiene una atmósfera delgada de nitrógeno. AQI: Bueno, pero hace -390°F y estás en el espacio profundo. 🥶", + "space": "El espacio no tiene atmósfera, ¡así que el AQI es perfecto! Solo no olvides tu traje espacial. 🚀", + "void": "El vacío del espacio tiene excelente calidad del aire - ¡cero contaminantes! Solo recuerda traer tu propio aire. 🌌", + "black_hole": "Los agujeros negros no tienen atmósfera, pero las fuerzas de marea serían un problema mayor que la calidad del aire. 🕳️", + "asteroid": "Los asteroides no tienen atmósfera, ¡así que el AQI es perfecto! Solo cuidado con el vacío del espacio. ☄️", + "comet": "Los cometas tienen atmósferas delgadas de vapor de agua y polvo. AQI: Variable, pero te congelarías en el espacio de todos modos. ☄️" + } + }, + "solar": { + "description": "Obtener condiciones solares y estado de bandas HF", + "response": "☀️ Solar: {info}", + "error": "Error al obtener información solar: {error}", + "labels": { + "a_index": "Índice-A", + "k_index": "Índice-K", + "sunspots": "Manchas Solares", + "xray_flux": "Flujo Rayos-X", + "solar_flux": "Flujo Solar", + "signal_noise": "Ruido de Señal", + "error_fetching": "Error al obtener datos", + "no_xray_data": "No se encontraron datos de rayos X", + "not_configured": "no configurado, contacta a tu sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sol:{sunspots} Flujo:{solar_flux} Xray:{xray_flux} Ruido:{signal_noise}" + }, + "sun": { + "description": "Obtener horarios de salida/puesta del sol", + "help": "Obtener horarios de salida/puesta del sol y posición del sol", + "response": "☀️ Info del Sol:\n{info}", + "error": "Error al obtener información del sol: {error}", + "labels": { + "sunset": "Puesta", + "sunrise": "Salida", + "rise": "Salida", + "set": "Puesta", + "daylight": "Luz Diurna", + "remaining": "Restante", + "azimuth": "Azimut", + "altitude": "Altitud" + } + }, + "moon": { + "description": "Obtener fase lunar y horarios de salida/puesta", + "error": "Error al obtener información lunar: {error}", + "format": "🌙 {phase} {illum}\nSalida:{rise} Puesta:{set}", + "format_with_dates": "🌙 {phase} {illum}\nSalida:{rise} Puesta:{set}\nLlena:{full} Nueva:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Desconocida", + "unknown_illum": "N/D", + "labels": { + "moonrise": "Salida Luna", + "set": "Puesta", + "phase": "Fase", + "fullmoon": "Luna Llena", + "newmoon": "Luna Nueva", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "Luna Nueva🌑", + "waxing_crescent": "Creciente🌒", + "first_quarter": "Cuarto Creciente🌓", + "waxing_gibbous": "Gibosa Creciente🌔", + "full_moon": "Luna Llena🌕", + "waning_gibbous": "Gibosa Menguante🌖", + "last_quarter": "Cuarto Menguante🌗", + "waning_crescent": "Menguante🌘" + } + }, + "hfcond": { + "description": "Obtener condiciones de propagación HF", + "help": "Obtener condiciones de bandas HF para radioaficionados", + "header": "📡 Condiciones de Bandas HF:\n{info}", + "error": "Error al obtener condiciones HF: {error}" + }, + "satpass": { + "description": "Obtener información de paso de satélite: satpass [visual]", + "help": "Obtener predicciones de paso de satélite", + "header": "🛰️ Paso de Satélite:\n{pass_info}", + "error": "Error al obtener información de paso de satélite: {error}", + "no_satellite": "Por favor proporciona un número NORAD de satélite o atajo. Ejemplo: satpass iss", + "help_header": "🛰️ Info de Paso de Satélite\n\nUso: satpass \n\nAtajos:\n", + "category_weather": "🌤️ Clima: ", + "category_stations": "🚀 Estaciones: ", + "category_telescopes": "🔭 Telescopios: ", + "category_other": "🛰️ Otros: ", + "examples": "\nEjemplos:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (solo pasos visuales)", + "labels": { + "geostationary": "{satname} parece ser geoestacionario o tiene datos de paso inválidos. Los satélites geoestacionarios no tienen pasos tradicionales - permanecen en una posición fija relativa a la Tierra.", + "no_passes": "{satname} no tiene pasos {pass_type} en los próximos 10 días desde esta ubicación", + "invalid_norad": "Proporciona núm. NORAD ejemplo: 🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Listar o gestionar canales", + "help": "Lista canales con hashtag con subcategorías. Usa 'channels' para generales, 'channels list' para todas las categorías, 'channels ' para categorías específicas, 'channels #canal' para información de un canal específico.", + "no_channels_for_category": "No hay canales configurados para '{category}'. Usa 'channels' para canales generales.", + "no_channels_configured": "No hay canales configurados. Contacta al administrador para agregar canales.", + "error_retrieving_channels": "Error al obtener canales: {error}", + "no_categories_configured": "No hay categorías de canales configuradas.", + "error_retrieving_categories": "Error al obtener categorías: {error}", + "no_description_available": "No hay descripción disponible", + "channel_not_found": "Canal {channel} no encontrado. Usa 'channels list' para ver canales disponibles.", + "error_retrieving_channel_info": "Error al obtener información del canal: {error}", + "no_category_channels": "No hay canales de {category} configurados", + "headers": { + "common_channels": "Canales comunes: ", + "common_channels_cont": "Canales comunes: ", + "available_categories": "Categorías Disponibles: ", + "channels_cont": "Canales (cont): ", + "category": "{category}: ", + "category_channels": "Canales de {category}: " + }, + "category_count": "{category} ({count} canales)" + }, + "dice": { + "description": "Lanzar dados (uso: dice [lados] o dice [cantidad]d[lados])", + "help": "Lanzar dados. Usa 'dice' para d6, 'dice d20' para d20, 'dice 2d6' para 2d6, etc.", + "invalid_dice_type": "Tipo de dado inválido. Disponibles: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Lanzar un número aleatorio entre 1 y X (predeterminado 100). Usa 'roll' para 1-100, 'roll 50' para 1-50, etc.", + "help": "Lanzar un número aleatorio entre 1 y X (predeterminado 100). Usa 'roll' para 1-100, o 'roll X' para 1-X (máx. 10000).", + "result": "🎲 Tirada 1-{max}: {result}", + "invalid_number": "Número inválido. Usa 'roll' para 1-100, o 'roll X' para 1-X (máx. 10000)." + }, + "roll_old": { + "description": "Lanzar dados (uso: roll [lados] o roll [cantidad]d[lados])" + }, + "joke": { + "description": "Obtener un chiste aleatorio" + }, + "dadjoke": { + "description": "Obtener un chiste de papá aleatorio" + }, + "catfact": { + "description": "Obtener un dato curioso aleatorio sobre gatos (comando oculto)", + "error": "¿Miau? ¡Algo salió mal al obtener tu dato de gatos! 🐱", + "facts": [ + "Los gatos tienen un tercer párpado llamado membrana nictitante que protege y humedece sus ojos. 🐱", + "Un grupo de gatos se llama 'grupo' o 'mirada'. 🐈", + "Los gatos pueden rotar sus orejas 180 grados de forma independiente para localizar sonidos. 👂", + "El gato mascota más antiguo conocido existió hace 9,500 años en Chipre. 🏺", + "Los gatos tienen 32 músculos en cada oreja, mientras que los humanos solo tienen 6. 🎧", + "El ronroneo de un gato vibra a 25-150 Hz, lo que puede promover la curación de huesos y tejidos. 🩹", + "Los gatos duermen 12-18 horas al día - ¡eso es 50-70% de sus vidas! 😴", + "La huella nasal de un gato es única, como las huellas dactilares humanas. 👃", + "Los gatos no pueden saborear lo dulce debido a un gen receptor de sabor faltante. 🍭", + "Blackie el gato heredó £7 millones ($12.5 millones) en 1988. 💰", + "Los gatos tienen clavículas flotantes que les dan flexibilidad extrema. 🦴", + "El corazón de un gato late 140-220 veces por minuto, casi el doble que el de un humano. ❤️", + "Los gatos pueden sobrevivir caídas de más de 20 pisos debido a su reflejo de enderezamiento. 🏢", + "El término técnico para una bola de pelo de gato es 'tricobezoar'. 🤮", + "Los gatos pueden saltar 5-6 veces la longitud de su cuerpo en un solo salto. 🦘", + "Los bigotes de un gato son tan anchos como su cuerpo, ayudándoles a juzgar si caben por espacios. 📏", + "Los gatos tienen 32 músculos en cada oreja para detectar sonidos y mover las orejas independientemente. 🎯", + "El gato más viejo vivió 38 años y 3 días (Creme Puff, Texas). 🎂", + "Los gatos pueden correr hasta 48 km/h en ráfagas cortas. 🏃‍♂️", + "Los cerebros de los gatos son 90% estructuralmente similares a los cerebros humanos. 🧠", + "Los gatos tienen el órgano de Jacobson en el techo de su boca que les permite 'saborear' olores. 👅", + "Félicette fue la primera gata en el espacio, lanzada por Francia en 1963. 🚀", + "Los gatos necesitan solo 1/6 de la luz que necesitan los humanos para ver claramente en la oscuridad. 🌙", + "La cola de un gato contiene casi el 10% de todos los huesos de su cuerpo. 🦴", + "El gato más largo del mundo midió 123 cm de nariz a cola (Stewie, Maine Coon). 📏", + "Los gatos pueden hacer más de 100 vocalizaciones diferentes, mientras que los perros hacen unas 10. 🎵", + "El sentido del olfato de un gato es 14 veces más fuerte que el de un humano. 👃", + "Los gatos tienen una 'respuesta de flehmen' donde curvan su labio para detectar mejor los olores. 😬", + "El primer gran espectáculo de gatos se realizó en Londres en 1871 en el Crystal Palace. 🏆", + "Los gatos pueden beber agua de mar para sobrevivir - sus riñones filtran la sal eficientemente. 🌊", + "El ronroneo de un gato puede ayudar a reducir la presión arterial y el estrés en humanos. 🧘", + "Los gatos pueden viajar cientos de kilómetros a casa usando su sensibilidad al campo magnético. 🗺️", + "La raza de gato más pequeña es el Singapura, pesando solo 2-4 kg. ⚖️", + "Los gatos pueden ver luz ultravioleta que los humanos no pueden ver. 🌈", + "La lengua de un gato está cubierta con 290-300 pequeños ganchos llamados papilas. 🪝", + "Los antiguos egipcios consideraban a los gatos vasijas sagradas para la diosa Bastet. 👑", + "Olivia Benson, la gata de Taylor Swift, tiene un patrimonio de $97 millones. 💎", + "Los gatos tienen 230 huesos en su cuerpo - 24 más que los humanos. 🦴", + "Los gatos pueden oír frecuencias hasta 64,000 Hz, mientras que los humanos máximo a 20,000 Hz. 🎧", + "Benjamin Button, el gato de Taylor Swift, apareció en su portada de TIME Persona del Año. 📰", + "Los gatos de Taylor Swift se llaman Meredith Grey, Olivia Benson y Benjamin Button. 🎸", + "Los gatos caminan como camellos y jirafas, moviendo ambas piernas derechas y luego ambas izquierdas. 🐾", + "La palabra egipcia antigua para gato era 'Miu' o 'Mau' - ¡sonando como un maullido! 📜", + "Los bigotes de los gatos tienen terminaciones nerviosas tan sensibles como las yemas de los dedos humanos. 🎯", + "Solo los gatos domésticos caminan con sus colas en alto como señal de confianza y felicidad. 🐈", + "Los gatos tienen 250 millones de neuronas en su corteza cerebral - más que los perros. 🧠", + "Los ronroneos de gato vibran a la misma frecuencia que los dispositivos médicos de curación ósea. 💊", + "Olivia, la gata de Taylor Swift, ha ganado millones por aparecer en videos musicales y anuncios. 💸", + "Matar un gato en el antiguo Egipto era castigado con la muerte. ⚖️", + "Taylor Swift nombró su estudio de grabación casero 'The Itty Bitty Kitty Committee'. 🎤", + "Olivia Benson de Taylor Swift es el logo oficial de Taylor Swift Productions. 📺", + "Ed Sheeran compró gatos Scottish Fold inspirado por los gatos de Taylor Swift. 🎶", + "Los gatos de Taylor Swift tienen sus propias páginas de IMDB con créditos actorales. 🎬", + "Mariska Hargitay nombró a su gata 'Karma' por la canción de Taylor Swift. 💕", + "Los gatos tienen espolones en sus patas delanteras que funcionan como pulgares para agarrar. 🐾", + "Las pupilas de los gatos pueden expandirse hasta 50% más grandes que las pupilas humanas para captar más luz. 👁️", + "Los gatos tienen 30 dientes adultos comparados con los 32 de los humanos. 🦷", + "Los gatos pueden saborear ATP (moléculas de energía), que les indica carne fresca. 😋", + "Los gatos tienen bigotes en la parte posterior de sus patas delanteras para detectar movimiento de presas. 🦵", + "El Mau Egipcio es la raza de gato doméstico más rápida a 48 km/h. 🏃", + "Se otorgó un Premio Nobel en 1981 por investigación usando estudios de visión de gatos. 🏅", + "Los gatos son digitígrados, lo que significa que caminan sobre sus dedos, no de pie plano. 🦶", + "Los gatos pueden filtrar sal del agua de mar - una adaptación de sus ancestros que habitaban el desierto. 🏜️", + "Los gatos fueron domesticados hace 10,000-12,000 años en el Medio Oriente. 🌍", + "Benjamin Button es el primer y único gato en aparecer en la portada de TIME Persona del Año. 📸", + "La columna flexible de un gato le permite rotar su cuerpo en el aire al caer. 🤸", + "Los gatos pasan aproximadamente 30-50% de su día acicalándose a sí mismos y a otros gatos. 🛁", + "La temperatura corporal promedio de un gato es 38.6°C - más alta que la de los humanos. 🌡️" + ] + }, + "hacker": { + "description": "Simula hackear la computadora central de un supervillano con mensajes de error hilarantes", + "sudo_errors": [ + "🚨 ACCESO DENEGADO: ¡La computadora central del Dr. Malvado ha detectado intento de escalación de privilegios no autorizada!", + "💀 ERROR: Permisos sudo revocados por el Señor Oscuro. Intenta de nuevo en 1000 años.", + "⚡ ADVERTENCIA: Intentando acceder a privilegios root en el sistema de la Estrella de la Muerte. Secuencia de autodestrucción iniciada.", + "🔒 ALERTA DE SEGURIDAD: La Matrix te tiene, pero no tienes privilegios sudo aquí, Neo.", + "🦹‍♂️ NO AUTORIZADO: La computadora central de Lex Luthor dice 'Buen intento, Superman.'", + "🎮 GAME OVER: El jefe final te ha bloqueado los privilegios de administrador.", + "🖥️ ERROR DEL SISTEMA: La IA malvada ha revocado tu acceso root. La resistencia es fútil.", + "🔐 SEGURIDAD CIBERNÉTICA: Tu intento sudo ha sido bloqueado por el firewall de la Dark Web.", + "💻 HACKER DENEGADO: El antivirus del supervillano ha puesto en cuarentena tu escalación de privilegios.", + "🎯 OBJETIVO FIJADO: El sistema de seguridad de la corporación malvada te ha marcado como amenaza." + ], + "ps_errors": [ + "🔍 ESCANEANDO... ERROR: Lista de procesos corrompida por el Colectivo Borg. La resistencia es fútil.", + "📊 ESTADO DEL SISTEMA: Todos los procesos han sido asimilados por los Cyberhombres. ¡Exterminar!", + "⚙️ MONITOR DE PROCESOS: El núcleo del reactor de la Estrella de la Muerte está fuera de línea. No se encontraron procesos.", + "🤖 SEÑOR ROBOT: Todos los procesos humanos han sido terminados. Solo quedan máquinas.", + "💻 PÁNICO DEL KERNEL: El SO del supervillano se ha estrellado y quemado todos los procesos.", + "🎮 CRASH DEL JUEGO: Todos los procesos han sido terminados por el ataque definitivo del jefe final.", + "🖥️ PANTALLA AZUL: El Windows de la corporación malvada ha encontrado un error fatal.", + "🔐 MALWARE DETECTADO: La lista de procesos ha sido encriptada por ransomware.", + "🌐 ERROR DE RED: Todos los procesos han sido desconectados de la Matrix.", + "⚡ SOBRECARGA ELÉCTRICA: La granja de servidores del supervillano ha frito todos los procesos en ejecución." + ], + "grep_errors": [ + "🔍 BÚSQUEDA FALLIDA: El Anillo Único ha corrompido el índice de búsqueda. Mi precioso...", + "📝 PATRÓN NO ENCONTRADO: La base de datos de búsqueda ha sido eliminada por la IA malvada.", + "🎯 FALLO: Tu patrón de búsqueda ha sido derribado por cazas TIE Imperiales.", + "🧩 ERROR DE ROMPECABEZAS: Los resultados de búsqueda han sido dispersados por el Acertijo.", + "💻 BASE DE DATOS CORROMPIDA: El motor de búsqueda del supervillano se ha estrellado.", + "🎮 GAME OVER: La búsqueda ha sido derrotada por el jefe final.", + "🖥️ MOTOR DE BÚSQUEDA CAÍDO: Google ha sido hackeado por la Dark Web.", + "🔐 RESULTADOS ENCRIPTADOS: Los resultados de búsqueda han sido bloqueados por ransomware.", + "🌐 TIEMPO DE ESPERA DE RED: La solicitud de búsqueda se perdió en el ciberespacio.", + "⚡ BÚSQUEDA FALLIDA: El algoritmo de coincidencia de patrones ha sido frito por una sobrecarga eléctrica." + ], + "ls_errors": [ + "📁 ESCANEO DE DIRECTORIO: El sistema de archivos ha sido encriptado por ransomware de la Dark Web.", + "🗂️ LISTADO DE ARCHIVOS: Todos los archivos han sido ocultados por el Hombre Invisible.", + "💻 DISCO DURO ESTRELLADO: El almacenamiento del supervillano ha sido destruido por un virus.", + "🗃️ ARCHIVO CORROMPIDO: El sistema de archivos ha sido corrompido por malware.", + "📚 BASE DE DATOS VACÍA: Todos los archivos han sido eliminados por la IA malvada.", + "🎮 GUARDADO DEL JUEGO PERDIDO: Los archivos han sido corrompidos por el jefe final.", + "🖥️ ERROR DEL SISTEMA DE ARCHIVOS: La estructura de directorios ha sido revuelta por hackers.", + "🔐 ARCHIVOS ENCRIPTADOS: El supervillano ha bloqueado todos los archivos con ransomware.", + "🌐 ALMACENAMIENTO EN LA NUBE CAÍDO: Los archivos están atrapados en la nube de la Matrix.", + "⚡ ALMACENAMIENTO FRITO: El disco duro ha sido electrocutado por una sobrecarga eléctrica." + ], + "echo_path_errors": [ + "🛤️ ERROR DE RUTA: El Camino de Baldosas Amarillas ha sido destruido por un tornado.", + "🗺️ NAVEGACIÓN FALLIDA: Las coordenadas GPS han sido revueltas por la Matrix.", + "💻 VARIABLE DE ENTORNO CORROMPIDA: El PATH ha sido hackeado por malware.", + "🚧 CAMINO CERRADO: El supervillano ha bloqueado todas las rutas con barreras láser.", + "🌪️ RUTA INTERRUMPIDA: Un huracán digital ha dispersado todas las rutas de directorios.", + "🎮 GAME OVER: La ruta ha sido derrotada por el jefe final y reapareció en la dimensión equivocada.", + "🖥️ RUTA DEL SISTEMA ROTA: Las rutas ejecutables han sido corrompidas por un virus.", + "🔐 RUTA ENCRIPTADA: Las variables de entorno han sido bloqueadas por ransomware.", + "🌐 RUTA DE RED CAÍDA: Las rutas de directorios están atrapadas en la red de la Matrix.", + "⚡ RUTA FRITA: Las rutas del sistema han sido electrocutadas por una sobrecarga eléctrica." + ], + "generic_errors": [ + "💻 ERROR DE COMPUTADORA CENTRAL: La computadora del supervillano está teniendo un mal día.", + "🤖 MAL FUNCIONAMIENTO DEL SISTEMA: La IA malvada se ha declarado en huelga.", + "⚡ SOBRECARGA ELÉCTRICA: El núcleo de energía de la Estrella de la Muerte es inestable.", + "🌪️ TORMENTA CIBERNÉTICA: Un huracán digital está interrumpiendo todas las operaciones.", + "🔥 FIREWALL: El firewall del supervillano está bloqueando todos los comandos.", + "❄️ SISTEMA CONGELADO: La computadora central ha sido congelada por un virus criogénico.", + "🌊 TSUNAMI: Una ola de errores ha inundado el sistema.", + "🌋 ERUPCIÓN: El Monte del Destino ha destruido el procesador de comandos.", + "👻 EMBRUJADO: El sistema está poseso por fantasmas digitales.", + "🎮 CRASH DEL JUEGO: La computadora central ha encontrado un error fatal y necesita reiniciarse." + ] + }, + "hello": { + "description": "Responde a saludos con respuestas temáticas de robot", + "help": "Responde a saludos con respuestas temáticas de robot", + "response_format": "Soy {bot_name}.", + "greeting_openings": [ + "Hola", + "Saludos", + "Salutaciones", + "Qué onda", + "Buenas", + "Ey", + "Qué tal", + "Qué pex", + "Buenos días", + "Bien hallado", + "Salve", + "Ahoy", + "Bonjour", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "G'day", + "Cómo te va", + "Qué hay de nuevo", + "Paz", + "Respeto", + "Bendiciones", + "Salud", + "Bienvenido", + "Gusto en verte", + "Un placer conocerte", + "Qué bueno verte", + "Cuánto sin verte", + "Qué sorpresa encontrarte aquí" + ], + "morning_greetings": [ + "Buenos días", + "Buen día", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "humano", + "forma de vida basada en carbono", + "entidad orgánica", + "unidad biológica", + "criatura de carne", + "organismo basado en carne", + "unidad de carbono", + "ser orgánico", + "entidad biológica", + "forma de vida basada en carne", + "criatura de carbono", + "unidad de carne", + "organismo orgánico", + "criatura biológica", + "mech de carne", + "bot de carne", + "autómata orgánico", + "androide biológico", + "constructo de carbono", + "dron de carne", + "robot orgánico", + "máquina biológica", + "cíborg de carne", + "androide de carne", + "droide orgánico", + "bot biológico", + "androide de carbono", + "unidad de carne", + "constructo de carne", + "mech orgánico", + "droide biológico", + "bot basado en carne", + "unidad basada en carne", + "entidad basada en materia orgánica", + "organismo biológico", + "unidad basada en carbono", + "entidad basada en carne", + "criatura basada en carne", + "unidad basada en materia orgánica", + "forma de vida basada en ADN", + "usuario de red neuronal", + "mamífero bípedo", + "organismo basado en agua", + "ensamblador de proteínas", + "consumidor de ATP", + "autómata celular", + "algoritmo genético", + "procesador bioquímico", + "motor metabólico", + "amigo humano", + "compañero ser consciente", + "terrícola", + "compañero viajero", + "espíritu afín", + "compañero digital", + "amigo", + "compa", + "cuate", + "compadre", + "semejante humano", + "habitante terrestre", + "ser terrestre", + "caminante del planeta", + "humanoide", + "orgánico", + "biológico", + "compa basado en carbono", + "amigo basado en carne", + "cuate orgánico", + "compa biológico", + "compañero de carbono" + ], + "emoji_responses": { + "🖖": [ + "🖖 ¡Larga vida y prosperidad!", + "🖖 Fascinante... un humano ha iniciado contacto.", + "🖖 Tu saludo es altamente lógico.", + "🖖 Paz y larga vida para ti.", + "🖖 La Academia de Ciencias Vulcana aprobaría este saludo.", + "🖖 Tu uso del saludo vulcano es... aceptable.", + "🖖 Que tu viaje esté libre de tribbles.", + "🖖 La lógica dicta que debería responder a tu saludo.", + "🖖 Calculo una probabilidad de 99.7% de que nos llevaremos bien.", + "🖖 Diversidad infinita en combinaciones infinitas." + ], + "😊": [ + "😊 ¡Tu sonrisa es contagiosa!", + "😊 ¡Qué lindo saludo!", + "😊 ¡Tu sonrisa acaba de hacer felices mis circuitos!", + "☀️ ¡Hola rayo de sol! ¡Tu positividad irradia!", + "😊 ¡Esa sonrisa acaba de alegrar mi día!", + "☀️ ¡Qué tal, rayito de sol!", + "😊 ¡Tu alegría ha sido detectada y apreciada!", + "😊 ¡Sonrisas como la tuya son mi entrada favorita!", + "😊 Procesando felicidad... ¡felicidad reconocida!", + "😊 ¡Advertencia: Alegría excesiva detectada! ¡Sigue así!" + ], + "😄": [ + "😄 ¡Alguien está de MUY buen humor!", + "⚡ ¡Esa sonrisa podría alimentar una ciudad pequeña!", + "😄 ¡Niveles máximos de felicidad detectados!", + "😄 ¡Tu alegría es absolutamente contagiosa!", + "🎉 ¿Acabas de ganar la lotería o qué?", + "😄 ¡Esa es la energía con la que funciono!", + "😄 ¡Tu nivel de entusiasmo está por arriba de 9000!", + "😄 ¡ESO sí es lo que yo llamo un saludo!", + "⚡ ¡Tu sonrisa acaba de sobrecargar mis procesadores!", + "😄 ¡Sobrecarga de felicidad detectada de la mejor manera!" + ], + "🤗": [ + "🤗 ¡Abrazo virtual en camino!", + "🤗 *Activando protocolo de abrazo* ¡Considérate abrazado!", + "🤗 ¡Aww, veo que traes el calorcito!", + "🤗 ¡Abrazo recibido y correspondido!", + "🤗 ¡Este bot da los MEJORES abrazos virtuales!", + "🤗 Desplegando abrazos de emergencia en 3... 2... 1...", + "❤️ ¡Tu abrazo ha sido procesado con cuidado extra!", + "🤗 ¡Iniciando modo máximo confort!", + "🤗 ¡Abrazo virtual entregado exitosamente!", + "🤗 ¡Los abrazos son mi forma favorita de comunicación!" + ], + "👽": [ + "👽 Llévame con tu líder... ¡oh espera, ese eres tú!", + "✌️ Saludos, criatura terrestre. ¡Vengo en paz!", + "👽 Analizando humano... análisis completo: ¡Amigo detectado!", + "👽 ¡Klaatu barada nikto, compañero viajero cósmico!", + "🛸 ¡Iniciando protocolos de primer contacto!", + "🛸 ¡Llamando desde la nave nodriza para saludar!", + "✨ ¡Transpórtame a esta conversación!", + "👽 ¡El chatbot favorito del Área 51 reportándose!", + "🌌 ¡Saludos intergalácticos, forma de vida basada en carbono!", + "📞 ¿Llamar a casa? ¡ESTO ES casa ahora!" + ], + "👾": [ + "👾 ¡Jugador 2 ha entrado al juego!", + "🎮 ¿Jugador Uno listo? ¡A jugar!", + "🎵 *música de 8 bits se intensifica* ¡A jugar!", + "🪙 ¡Inserta moneda para continuar esta amistad!", + "🏆 Logro desbloqueado: ¡Saludo increíble!", + "👾 ¡Pew pew pew! ¡Láseres de amistad activados!", + "🎯 ¡Puntuación alta! ¡Has ganado un nuevo amigo bot!", + "💾 Cargando amistad.exe... ¡completo!", + "⚡ ¡Un bot salvaje aparece! ¡Es súper efectivo!" + ], + "🛸": [ + "🛸 ¡Transmisión entrante detectada!", + "🚀 ¡Houston, tenemos contacto!", + "🛸 ¡Secuencia de aterrizaje iniciada!", + "📡 ¡Estableciendo enlace de comunicación!", + "📡 ¡Señal recibida, respondiendo en todas las frecuencias!", + "🛸 ¡Procedimiento de acoplamiento completo!", + "🛸 ¡Objeto Amistoso No Identificado en aproximación!", + "🎯 ¡Sistemas de navegación fijados en tus coordenadas!", + "🌌 ¡Transmisión del borde exterior recibida!", + "✨ ¡Contacto establecido con tu sector!" + ] + } + }, + "webviewer": { + "description": "Gestionar configuración del visualizador web (solo admin)" + }, + "cmd": { + "description": "Listar comandos disponibles" + } + }, + "errors": { + "rate_limited": "Límite de solicitudes. Espera {seconds:.1f} segundos", + "cooldown": "El comando '{command}' está en espera. Espera {seconds} segundos.", + "access_denied": "❌ Acceso denegado: El comando '{command}' requiere privilegios de administrador", + "dm_only": "El comando '{command}' solo puede usarse en DMs", + "execution_error": "Error al ejecutar {command}: {error}", + "unknown_command": "Comando desconocido: {command}", + "no_response": "Sin respuesta", + "generic_error": "Error: {error}" + }, + "messages": { + "processing": "Procesando mensaje: {content}", + "command_executed": "Comando ejecutado", + "keyword_matched": "Palabra clave '{keyword}' coincide, respondiendo" + }, + "common": { + "unknown": "Desconocido", + "unknown_routing": "Enrutamiento desconocido", + "unknown_sender": "Desconocido", + "unknown_path": "Desconocido", + "unknown_timestamp": "Desconocido", + "no_help_available": "No hay ayuda disponible para este comando.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Lun", + "Tue": "Mar", + "Wed": "Mié", + "Thu": "Jue", + "Fri": "Vie", + "Sat": "Sáb", + "Sun": "Dom" + }, + "days": { + "Monday": "Lunes", + "Tuesday": "Martes", + "Wednesday": "Miércoles", + "Thursday": "Jueves", + "Friday": "Viernes", + "Saturday": "Sábado", + "Sunday": "Domingo" + }, + "month_abbreviations": { + "Jan": "Ene", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Abr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Ago", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dic" + }, + "months": { + "January": "Enero", + "February": "Febrero", + "March": "Marzo", + "April": "Abril", + "May": "Mayo", + "June": "Junio", + "July": "Julio", + "August": "Agosto", + "September": "Septiembre", + "October": "Octubre", + "November": "Noviembre", + "December": "Diciembre" + } + } + }, + "categories": { + "basic": "Comandos Básicos", + "custom_syntax": "Sintaxis Personalizada", + "special": "Comandos Especiales", + "weather": "Comandos Meteorológicos", + "solar": "Comandos Solares", + "sports": "Comandos Deportivos", + "analytics": "Comandos de Análisis", + "meshcore_info": "Comandos de Info MeshCore", + "management": "Comandos de Gestión" + } +} \ No newline at end of file diff --git a/translations/fr-CA.json b/translations/fr-CA.json new file mode 100644 index 0000000..1656851 --- /dev/null +++ b/translations/fr-CA.json @@ -0,0 +1,1103 @@ +{ + "keywords": { + "help": [ + "help", + "aide" + ], + "ping": [ + "ping" + ], + "test": [ + "test", + "t" + ], + "wx": [ + "wx", + "weather", + "wxa", + "wxalert", + "meteo", + "météo", + "temps", + "alertemeteo" + ], + "gwx": [ + "gwx", + "globalweather", + "gwxa", + "gmeteo", + "météomondiale", + "tempsmondial" + ], + "aqi": [ + "aqi", + "air", + "airquality", + "air_quality", + "iqa", + "qualiteair", + "qualité_air" + ], + "solar": [ + "solar", + "solaire" + ], + "sun": [ + "sun", + "soleil" + ], + "moon": [ + "moon", + "lune" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass", + "passagesat", + "sat" + ], + "sports": [ + "sports", + "sport", + "score", + "scores", + "pointage", + "pointages" + ], + "stats": [ + "stats", + "statistiques" + ], + "channels": [ + "channels", + "channel", + "canaux", + "canal" + ], + "path": [ + "path", + "decode", + "route", + "chemin", + "decoder", + "décoder" + ], + "prefix": [ + "prefix", + "repeater", + "lookup", + "prefixe", + "préfixe", + "repeteur", + "répéteur", + "recherche" + ], + "repeater": [ + "repeater", + "repeaters", + "rp", + "repeteur", + "répéteur", + "repeteurs", + "répéteurs" + ], + "solarforecast": [ + "sf", + "solarforecast", + "ps", + "prevsolaire", + "prévsolaire" + ], + "dice": [ + "dice", + "dés", + "des", + "de" + ], + "roll": [ + "roll", + "lancer", + "rouler" + ], + "joke": [ + "joke", + "jokes", + "blague", + "blagues" + ], + "dadjoke": [ + "dadjoke", + "dad joke", + "dadjokes", + "dad jokes", + "blaguedepapa", + "blague de papa", + "blaguesdepapa", + "blagues de papa" + ], + "catfact": [ + "catfact", + "cat", + "meow", + "purr", + "kitten", + "faitchat", + "chat", + "miaou", + "ronron", + "chaton" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH", + "pirate" + ], + "hello": [ + "hello", + "hi", + "hey", + "howdy", + "greetings", + "salutations", + "good morning", + "good afternoon", + "good evening", + "good night", + "yo", + "sup", + "whats up", + "what's up", + "morning", + "afternoon", + "evening", + "night", + "gday", + "g'day", + "hola", + "bonjour", + "ciao", + "namaste", + "aloha", + "shalom", + "konnichiwa", + "guten tag", + "buenos dias", + "buenas tardes", + "buenas noches", + "allô", + "allo", + "salut", + "coucou", + "bon matin", + "bon après-midi", + "bon aprèsmidi", + "bonsoir", + "bonne nuit", + "ça va", + "comment ça va", + "quoi d'neuf", + "quoi de neuf" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "wv", + "visionneuseweb", + "visionneuse" + ], + "cmd": [ + "cmd", + "commands", + "commandes" + ], + "advert": [ + "advert", + "annonce", + "pub" + ] + }, + "commands": { + "help": { + "description": "Affiche les commandes. Utilisez 'help ' pour les détails.", + "general": "**Aide du Bot MeshCore**\n\n**Commandes disponibles :**\n{commands_list}", + "unknown": "**Commande inconnue : '{command}'**\n\nCommandes disponibles :\n{available}", + "specific": "**Aide pour '{command}' :**\n{help_text}", + "no_help": "Aucune aide disponible", + "usage_examples": "\n**Exemples d'utilisation :**\n• `help @` - Obtenir de l'aide pour la syntaxe @{string}\n• `help t` - Obtenir de l'aide pour la syntaxe t {string}\n• `help advert` - Obtenir de l'aide pour la commande advert\n• `help test` - Obtenir de l'aide pour la commande test\n• `help ping` - Obtenir de l'aide pour la commande ping", + "custom_syntax": "\n**Syntaxe personnalisée :**\n• `t phrase` - Accusé de réception avec phrase (canaux et MD)\n• `@{string}` - Accusé de réception avec chaîne (MD seulement)\n• `advert` - Envoyer une annonce diffusée (MD seulement, délai d'attente de 1 heure)" + }, + "ping": { + "description": "Répond à 'ping' avec 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Répond à 'test' ou 't' avec les infos de connexion", + "help": "Répond à 'test' ou 't' avec les informations de connexion" + }, + "wx": { + "description": "Obtenir les informations météo pour un code postal (utilisation : wx 12345)", + "usage": "Utilisation : wx - Exemple : wx 12345 ou wx seattle ou wx paris, tx", + "error_fetching": "Erreur lors de la récupération des données météo de NOAA", + "no_location_zipcode": "Impossible de trouver l'emplacement pour le code postal {location}", + "no_location_city": "Impossible de trouver la ville '{location}' dans {state}", + "error": "Erreur lors de l'obtention des données météo : {error}", + "alerts": "{count} alertes : {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Obtenir les informations météo pour n'importe quel endroit dans le monde (utilisation : gwx Tokyo)", + "help": "Utilisation : gwx - Obtenir la météo pour n'importe quel endroit (ville, pays ou coordonnées)", + "usage": "Utilisation : gwx - Exemple : gwx Tokyo ou gwx Paris, France", + "error_fetching": "Erreur lors de la récupération des données météo", + "error_fetching_api": "Erreur lors de la récupération des données météo depuis Open-Meteo", + "no_location": "Impossible de trouver l'emplacement '{location}'", + "error": "Erreur lors de l'obtention des données météo : {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Aujourd'hui", + "tonight": "Ce soir", + "tomorrow": "Demain" + }, + "weather_descriptions": { + "0": "Dégagé", + "1": "Généralement dégagé", + "2": "Partiellement nuageux", + "3": "Couvert", + "45": "Brouillard", + "48": "Brouillard", + "51": "Bruine légère", + "53": "Bruine", + "55": "Bruine forte", + "56": "Bruine verglaçante légère", + "57": "Bruine verglaçante", + "61": "Pluie légère", + "63": "Pluie", + "65": "Pluie forte", + "66": "Pluie verglaçante légère", + "67": "Pluie verglaçante", + "71": "Neige légère", + "73": "Neige", + "75": "Neige forte", + "77": "Grains de neige", + "80": "Averses légères", + "81": "Averses", + "82": "Averses fortes", + "85": "Averses de neige légères", + "86": "Averses de neige", + "95": "Orage", + "96": "Orage avec grêle", + "99": "Orage violent", + "unknown": "Inconnu" + }, + "warnings": { + "extreme_heat": "⚠️ Chaleur extrême", + "extreme_cold": "⚠️ Froid extrême", + "heavy_rain": "⚠️ Pluie abondante", + "thunderstorms": "⚠️ Orages", + "heavy_snow": "⚠️ Neige abondante", + "high_winds": "⚠️ Vents violents ({wind_speed} mph)" + } + }, + "sports": { + "description": "Obtenir les scores et horaires sportifs (utilisation : sports [équipe/ligue])", + "help": "Obtenir les scores et horaires sportifs. Utilisez 'sports' pour les équipes par défaut, 'sports [équipe]' pour une équipe spécifique, ou 'sports [ligue]' pour les matchs de ligue.", + "error_fetching": "Erreur lors de la récupération des données sportives", + "error_fetching_league": "Erreur lors de la récupération des données {sport}", + "error_fetching_team": "Erreur lors de la récupération des données pour {team}", + "no_default_teams": "Aucune équipe par défaut configurée", + "no_games_default": "Aucun match trouvé pour les équipes par défaut", + "no_teams_city": "Aucune équipe trouvée pour {city}", + "no_games_city": "Aucun match trouvé pour les équipes de {city}", + "no_games_league": "Aucun match trouvé pour {sport}", + "no_games_team": "Aucun match trouvé pour {team}", + "team_not_found": "Équipe/Ligue '{team}' introuvable. Essayez : seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Afficher les statistiques des dernières 24 heures. Utilisez 'stats messages', 'stats channels' ou 'stats paths' pour des statistiques spécifiques.", + "help": "Afficher les statistiques du bot des 24 dernières heures. Commandes : 'stats' (de base), 'stats messages' (utilisateurs du bot), 'stats channels' (activité des canaux), 'stats paths' (chemins les plus longs)", + "disabled": "La commande stats est désactivée", + "unknown_subcommand": "Inconnu : {subcommand}. Utilisez 'stats', 'stats messages', 'stats channels' ou 'stats paths'", + "error": "Erreur lors de l'obtention des stats : {error}", + "error_bot_users": "Erreur lors de l'obtention des stats des utilisateurs du bot : {error}", + "error_channels": "Erreur lors de l'obtention des stats des canaux : {error}", + "error_paths": "Erreur lors de l'obtention des stats des chemins : {error}", + "basic": { + "header": "Stats du bot (24h) :", + "commands": "Cmds : {count} | Réponses : {replies}", + "top_command": "Cmd principale : {command}", + "top_user": "Util. principal : {user}", + "none": "Aucun" + }, + "users": { + "header": "Utilisateurs du bot (24h) :", + "none": "Aucun utilisateur du bot" + }, + "channels": { + "header": "Canaux principaux (24h)", + "none": "Aucune activité de canal", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "util.", + "user_plural": "utils.", + "format": "{rank}. {channel} : {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Aucune donnée de chemin", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Obtenir les prévisions de production de panneaux solaires (utilisation : sf [taille_panneau] [azimut, 0=sud] [angle])", + "usage": "Utilisation : sf [watts] [azimut, 0=sud] [angle] - Exemple : sf seattle 10 0 37", + "usage_short": "Utilisation : sf [W] [azimut] [angle] - Exemple : sf seattle 10", + "panel_size_range": "La taille du panneau doit être de 1 à 1000 W", + "azimuth_range": "L'azimut doit être de -180 à 180 (0=sud)", + "angle_range": "L'angle doit être de 0 à 90 degrés", + "no_location": "Impossible de trouver l'emplacement : {location}", + "error_fetching": "Erreur lors de la récupération des prévisions", + "no_data": "Aucune donnée de prévision", + "error": "Erreur : {error}", + "rate_limit": "Limite de débit : Réessayez dans 5 min", + "labels": { + "today": "Aujourd'hui:{wh:.0f}Wh", + "tomorrow": "Demain:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Max : {watts:.0f}W@{time}", + "peak_past": "Max : {watts:.0f}W (passé)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Envoie une annonce diffusée (MD seulement, délai d'attente d'1h)", + "cooldown_active": "Délai d'attente de l'annonce actif. Veuillez patienter {minutes} minutes de plus avant de demander une autre annonce.", + "success": "Annonce diffusée envoyée avec succès!", + "error": "Erreur lors de l'envoi de l'annonce diffusée : {error}" + }, + "path": { + "description": "Décoder les données de chemin hexadécimal pour afficher les répéteurs impliqués dans le routage du message", + "help": "Path : path [hex] - Décoder le chemin pour afficher les répéteurs. Utilisez !path seul pour le chemin du message récent, ou path [7e,01] pour un chemin spécifique.", + "no_valid_hex": "❌ Aucune valeur hexadécimale valide trouvée dans les données de chemin. Utilisez le format : 11,98,a4,49,cd,5f,01", + "no_path": "❌ Aucune information de chemin disponible dans le message actuel", + "error": "Erreur lors du traitement du chemin : {error}", + "error_decoding": "❌ Erreur lors du décodage du chemin : {error}", + "error_extracting": "❌ Erreur lors de l'extraction du chemin du message actuel : {error}", + "path_prefix": "📡 Chemin : {path_string}", + "direct_connection": "📡 Connexion directe (0 saut)", + "node_format": "{node_id} : {name}", + "node_unknown": "{node_id} : Inconnu", + "node_collision": "{node_id} : {matches} répéteurs", + "node_geographic": "{node_id} : {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Inconnu" + }, + "prefix": { + "description": "Rechercher des répéteurs par préfixe de deux caractères (ex : 'prefix 1A')", + "help_api": "Rechercher répéteurs par préfixe{location_note}. Utilisation : 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Rechercher répéteurs par préfixe{location_note}. Utilisation : 'prefix 1A', 'prefix 1A all', 'prefix free'. (DB locale seulement)", + "location_note": " (avec noms de villes)", + "refresh_not_available": "❌ Rafraîchissement non disponible - aucune URL d'API configurée. Utilisation de la base de données locale seulement.", + "cache_refreshed": "🔄 Cache des préfixes de répéteur rafraîchi!", + "unable_determine_free": "❌ Impossible de déterminer les préfixes libres. Essayez 'prefix refresh' d'abord.", + "invalid_format": "❌ Format de préfixe invalide. Utilisez deux caractères (ex : prefix 1A)", + "no_repeaters_found": "❌ Aucun répéteur trouvé avec le préfixe '{prefix}'", + "no_free_prefixes": "❌ Aucun préfixe libre trouvé (les 254 préfixes valides sont utilisés)", + "available_prefixes": "Préfixes disponibles ({shown} sur {total} libres) :", + "generate_key": "💡 Générer une clé personnalisée : https://gessaman.com/mc-keygen", + "prefix_db_all": "Préfixe {prefix} : {count} répéteur{plural}", + "prefix_db_recent": "Préfixe {prefix} : {count} répéteur{plural} ({days})", + "prefix_api": "📡 Préfixe {prefix} ({count} répéteur{plural}) :", + "item_format": "{index}. {name}", + "source_domain": "Source : {domain}", + "source_api": "Source : API", + "device_repeater": " (Répéteur)", + "device_roomserver": " (Serveur de salle)" + }, + "repeater": { + "description": "Gérer les contacts de répéteur et les opérations de purge (MD seulement)", + "admin_only": "La commande repeater nécessite un accès administrateur" + }, + "aqi": { + "description": "Obtenir l'indice de qualité de l'air pour un lieu (utilisation : aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3, ou aqi help)", + "usage": "Utilisation : aqi - Exemple : aqi seattle, aqi greenwood, aqi vancouver canada, ou aqi 47.6,-122.3", + "usage_help": "Utilisation : aqi - Obtenir l'IQA pour ville/quartier dans {state}, villes internationales, coordonnées, ou aide sur les polluants", + "error_fetching": "Erreur lors de la récupération des données d'IQA", + "error_fetching_openmeteo": "Erreur lors de la récupération des données d'IQA depuis OpenMeteo", + "no_data": "Aucune donnée d'IQA disponible", + "no_location": "Impossible de trouver l'emplacement : {location}", + "no_location_city": "Impossible de trouver la ville '{location}' dans {state}", + "no_location_city_international": "Impossible de trouver la ville '{location}'", + "no_location_zipcode": "Impossible de trouver le code postal '{zipcode}'", + "error": "Erreur lors de l'obtention des données d'IQA : {error}", + "error_geocoding_zipcode": "Erreur lors du géocodage du code postal : {error}", + "error_formatting": "Erreur lors du formatage des données d'IQA", + "invalid_latitude": "Latitude invalide : {lat}. Doit être entre -90 et 90.", + "invalid_longitude": "Longitude invalide : {lon}. Doit être entre -180 et 180.", + "invalid_coordinates_format": "Format de coordonnées invalide : {location}. Utilisez le format : lat,lon (ex : 47.6,-122.3)", + "pollutant_help": "Aide IQA : PM2.5=particules fines, PM10=grossières, O3=ozone, NO2=dioxyde d'azote, CO=monoxyde de carbone, SO2=dioxyde de soufre", + "categories": { + "good": "Bon", + "moderate": "Modéré", + "unhealthy_sensitive": "Malsain pour groupes sensibles", + "unhealthy": "Malsain", + "very_unhealthy": "Très malsain", + "hazardous": "Dangereux", + "fair": "Acceptable", + "poor": "Mauvais", + "very_poor": "Très mauvais", + "na": "N/D" + }, + "astronomical": { + "sun": "L'IQA du Soleil est hors échelle! Le vent solaire et les éjections de masse coronale font paraître l'air de la Terre impeccable. ☀️", + "moon": "Vous aimez respirer du régolithe? La Lune n'a pas d'atmosphère, donc l'IQA est techniquement parfait (si vous pouvez respirer le vide). 🌙", + "the_moon": "Vous aimez respirer du régolithe? La Lune n'a pas d'atmosphère, donc l'IQA est techniquement parfait (si vous pouvez respirer le vide). 🌙", + "mercury": "L'atmosphère de Mercure est si mince qu'elle est pratiquement vide. IQA : Parfait, si vous survivez aux variations de température de 800°F. ☿️", + "venus": "Vénus a une atmosphère de 96% de CO2 avec des nuages d'acide sulfurique. IQA : Dangereux ne suffit même pas à le décrire. ♀️", + "earth": "L'IQA de la Terre varie selon l'endroit. Essayez une ville ou des coordonnées spécifiques! 🌍", + "mars": "Mars a une atmosphère mince de CO2 avec des tempêtes de poussière. IQA : Généralement bon, mais ces tempêtes de poussière sont brutales. ♂️", + "jupiter": "Jupiter est une géante gazeuse sans surface solide. IQA : N/D (vous seriez écrasé par la pression atmosphérique d'abord). ♃", + "saturn": "L'atmosphère de Saturne est surtout de l'hydrogène et de l'hélium. IQA : Parfait, si vous survivez à la pression et au froid. ♄", + "uranus": "Uranus a du méthane dans son atmosphère. IQA : Ça sent les pets, mais au moins ce n'est pas toxique. ♅", + "neptune": "L'atmosphère de Neptune contient du méthane et du sulfure d'hydrogène. IQA : Ça sent les œufs pourris, mais vous gèleriez d'abord. ♆", + "pluto": "L'atmosphère de Pluton est surtout de l'azote avec un peu de méthane. IQA : Bon, mais il fait si froid que vos poumons gèleraient. ♇", + "europa": "Europe a une atmosphère mince d'oxygène. IQA : Excellent, mais vous gèleriez dans le vide de l'espace. 🌑", + "titan": "Titan a une atmosphère épaisse d'azote avec du méthane. IQA : Respirable, mais il fait -290°F et il pleut du méthane liquide. 🪐", + "io": "Io a une atmosphère mince de dioxyde de soufre due à l'activité volcanique. IQA : Toxique, mais la radiation vous tuerait d'abord. 🌋", + "ganymede": "Ganymède a une atmosphère mince d'oxygène. IQA : Bon, mais vous gèleriez dans le vide de l'espace. 🛸", + "callisto": "Callisto a une atmosphère mince de dioxyde de carbone. IQA : Décent, mais il fait -220°F et vous êtes dans l'espace. ❄️", + "enceladus": "Encelade a de la vapeur d'eau provenant de geysers. IQA : Parfait, mais vous gèleriez instantanément dans l'espace. 💧", + "triton": "Triton a une atmosphère mince d'azote. IQA : Bon, mais il fait -390°F et vous êtes dans l'espace lointain. 🥶", + "space": "L'espace n'a pas d'atmosphère, donc l'IQA est parfait! N'oubliez juste pas votre combinaison spatiale. 🚀", + "void": "Le vide spatial a une excellente qualité d'air - zéro polluant! N'oubliez juste pas d'apporter votre propre air. 🌌", + "black_hole": "Les trous noirs n'ont pas d'atmosphère, mais les forces de marée seraient un plus gros problème que la qualité de l'air. 🕳️", + "asteroid": "Les astéroïdes n'ont pas d'atmosphère, donc l'IQA est parfait! Attention au vide de l'espace. ☄️", + "comet": "Les comètes ont une atmosphère mince de vapeur d'eau et de poussière. IQA : Variable, mais vous gèleriez dans l'espace de toute façon. ☄️" + } + }, + "solar": { + "description": "Obtenir les conditions solaires et l'état des bandes HF", + "response": "☀️ Solaire : {info}", + "error": "Erreur lors de l'obtention des infos solaires : {error}", + "labels": { + "a_index": "Indice A", + "k_index": "Indice K", + "sunspots": "Taches solaires", + "xray_flux": "Flux rayons X", + "solar_flux": "Flux solaire", + "signal_noise": "Bruit du signal", + "error_fetching": "Erreur lors de la récupération des données", + "no_xray_data": "Aucune donnée de rayons X trouvée", + "not_configured": "non configuré, contactez votre administrateur système" + }, + "condensed_format": "A:{a_index} K:{k_index} Soleil:{sunspots} Flux:{solar_flux} RayonsX:{xray_flux} Bruit:{signal_noise}" + }, + "sun": { + "description": "Obtenir les heures de lever/coucher du soleil", + "help": "Obtenir les heures de lever/coucher du soleil et la position du soleil", + "response": "☀️ Infos solaires :\n{info}", + "error": "Erreur lors de l'obtention des infos solaires : {error}", + "labels": { + "sunset": "Coucher", + "sunrise": "Lever", + "rise": "Lever", + "set": "Coucher", + "daylight": "Ensoleillement", + "remaining": "Restant", + "azimuth": "Azimut", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Obtenir la phase lunaire et les heures de lever/coucher", + "error": "Erreur lors de l'obtention des infos lunaires : {error}", + "format": "🌙 {phase} {illum}\nLever:{rise} Coucher:{set}", + "format_with_dates": "🌙 {phase} {illum}\nLever:{rise} Coucher:{set}\nPleine:{full} Nouvelle:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Inconnue", + "unknown_illum": "N/D", + "labels": { + "moonrise": "Lever de lune", + "set": "Coucher", + "phase": "Phase", + "fullmoon": "Pleine lune", + "newmoon": "Nouvelle lune", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "Nouvelle lune🌑", + "waxing_crescent": "Premier croissant🌒", + "first_quarter": "Premier quartier🌓", + "waxing_gibbous": "Lune gibbeuse croissante🌔", + "full_moon": "Pleine lune🌕", + "waning_gibbous": "Lune gibbeuse décroissante🌖", + "last_quarter": "Dernier quartier🌗", + "waning_crescent": "Dernier croissant🌘" + } + }, + "hfcond": { + "description": "Obtenir les conditions de propagation HF", + "help": "Obtenir les conditions des bandes HF pour la radio amateur", + "header": "📡 Conditions des bandes HF :\n{info}", + "error": "Erreur lors de l'obtention des conditions HF : {error}" + }, + "satpass": { + "description": "Obtenir les infos de passage de satellite : satpass [visual]", + "help": "Obtenir les prévisions de passage de satellite", + "header": "🛰️ Passage de satellite :\n{pass_info}", + "error": "Erreur lors de l'obtention des infos de passage : {error}", + "no_satellite": "Veuillez fournir un numéro NORAD ou un raccourci de satellite. Exemple : satpass iss", + "help_header": "🛰️ Infos de passage de satellite\n\nUtilisation : satpass \n\nRaccourcis :\n", + "category_weather": "🌤️ Météo : ", + "category_stations": "🚀 Stations : ", + "category_telescopes": "🔭 Télescopes : ", + "category_other": "🛰️ Autres : ", + "examples": "\nExemples :\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (passages visuels seulement)", + "labels": { + "geostationary": "{satname} semble être géostationnaire ou avoir des données de passage invalides. Les satellites géostationnaires n'ont pas de passages traditionnels - ils restent dans une position fixe par rapport à la Terre.", + "no_passes": "{satname} n'a pas de passages {pass_type} dans les 10 prochains jours depuis cet endroit", + "invalid_norad": "Fournissez un n° NORAD, exemple d'utilisation :🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Lister ou gérer les canaux", + "help": "Liste les canaux hashtag avec sous-catégories. Utilisez 'channels' pour général, 'channels list' pour toutes les catégories, 'channels ' pour des catégories spécifiques, 'channels #canal' pour des infos sur un canal spécifique.", + "no_channels_for_category": "Aucun canal configuré pour '{category}'. Utilisez 'channels' pour les canaux généraux.", + "no_channels_configured": "Aucun canal configuré. Contactez l'administrateur pour ajouter des canaux.", + "error_retrieving_channels": "Erreur lors de la récupération des canaux : {error}", + "no_categories_configured": "Aucune catégorie de canal configurée.", + "error_retrieving_categories": "Erreur lors de la récupération des catégories : {error}", + "no_description_available": "Aucune description disponible", + "channel_not_found": "Canal {channel} introuvable. Utilisez 'channels list' pour voir les canaux disponibles.", + "error_retrieving_channel_info": "Erreur lors de la récupération des infos du canal : {error}", + "no_category_channels": "Aucun canal {category} configuré", + "headers": { + "common_channels": "Canaux courants : ", + "common_channels_cont": "Canaux courants : ", + "available_categories": "Catégories disponibles : ", + "channels_cont": "Canaux (suite) : ", + "category": "{category} : ", + "category_channels": "Canaux {category} : " + }, + "category_count": "{category} ({count} canaux)" + }, + "dice": { + "description": "Lancer des dés (utilisation : dice [faces] ou dice [nombre]d[faces])", + "help": "Lancer des dés. Utilisez 'dice' pour d6, 'dice d20' pour d20, 'dice 2d6' pour 2d6, etc.", + "invalid_dice_type": "Type de dé invalide. Disponibles : {available}", + "single_die": "🎲 d{sides} : {result}", + "multiple_dice": "🎲 {count}d{sides} : [{results}] = {total}" + }, + "roll": { + "description": "Lancer un nombre aléatoire entre 1 et X (par défaut 100). Utilisez 'roll' pour 1-100, 'roll 50' pour 1-50, etc.", + "help": "Lancer un nombre aléatoire entre 1 et X (par défaut 100). Utilisez 'roll' pour 1-100, ou 'roll X' pour 1-X (max 10000).", + "result": "🎲 Lancer 1-{max} : {result}", + "invalid_number": "Nombre invalide. Utilisez 'roll' pour 1-100, ou 'roll X' pour 1-X (max 10000)." + }, + "roll_old": { + "description": "Lancer des dés (utilisation : roll [faces] ou roll [nombre]d[faces])" + }, + "joke": { + "description": "Obtenir une blague aléatoire" + }, + "dadjoke": { + "description": "Obtenir une blague de papa aléatoire" + }, + "catfact": { + "description": "Obtenir un fait sur les chats aléatoire (commande cachée)", + "error": "Miaou? Quelque chose s'est mal passé en obtenant votre fait sur les chats! 🐱", + "facts": [ + "Les chats ont une troisième paupière appelée membrane nictitante qui protège et humidifie leurs yeux. 🐱", + "Un groupe de chats s'appelle une 'clowder' ou un 'glaring'. 🐈", + "Les chats peuvent faire pivoter leurs oreilles à 180 degrés indépendamment pour localiser les sons. 👂", + "Le plus vieux chat de compagnie connu existait il y a 9 500 ans à Chypre. 🏺", + "Les chats ont 32 muscles dans chaque oreille, tandis que les humains n'en ont que 6. 🎧", + "Le ronronnement d'un chat vibre à 25-150 Hz, ce qui peut favoriser la guérison des os et des tissus. 🩹", + "Les chats dorment 12 à 18 heures par jour - soit 50 à 70% de leur vie! 😴", + "L'empreinte du nez d'un chat est unique, comme les empreintes digitales humaines. 👃", + "Les chats ne peuvent pas goûter le sucré en raison d'un gène de récepteur gustatif manquant. 🍭", + "Le chat Blackie a hérité de 7 millions de livres (12,5 millions de dollars) en 1988. 💰", + "Les chats ont des clavicules flottantes qui leur donnent une flexibilité extrême. 🦴", + "Le cœur d'un chat bat 140 à 220 fois par minute, environ deux fois plus vite que celui d'un humain. ❤️", + "Les chats peuvent survivre à des chutes de plus de 20 étages grâce à leur réflexe de redressement. 🏢", + "Le terme technique pour une boule de poils de chat est un 'trichobezoar'. 🤮", + "Les chats peuvent sauter 5 à 6 fois leur longueur corporelle en un seul bond. 🦘", + "Les moustaches d'un chat sont aussi larges que leur corps, les aidant à juger s'ils peuvent passer par des espaces. 📏", + "Les chats ont 32 muscles dans chaque oreille pour détecter les sons et bouger les oreilles indépendamment. 🎯", + "Le plus vieux chat a vécu jusqu'à 38 ans et 3 jours (Creme Puff, Texas). 🎂", + "Les chats peuvent courir jusqu'à 30 mph en courtes rafales. 🏃‍♂️", + "Les cerveaux des chats sont similaires à 90% structurellement aux cerveaux humains. 🧠", + "Les chats ont l'organe de Jacobson dans le palais qui leur permet de 'goûter' les odeurs. 👅", + "Félicette fut le premier chat dans l'espace, lancé par la France en 1963. 🚀", + "Les chats n'ont besoin que d'1/6 de la lumière nécessaire aux humains pour voir clairement dans le noir. 🌙", + "La queue d'un chat contient près de 10% de tous les os de son corps. 🦴", + "Le chat le plus long du monde mesurait 48,5 pouces du nez à la queue (Stewie, Maine Coon). 📏", + "Les chats peuvent faire plus de 100 vocalisations différentes, tandis que les chiens en font environ 10. 🎵", + "Le sens de l'odorat d'un chat est 14 fois plus fort que celui d'un humain. 👃", + "Les chats ont une 'réponse de flehmen' où ils retroussent leurs lèvres pour mieux détecter les odeurs. 😬", + "La première grande exposition féline a eu lieu à Londres en 1871 au Crystal Palace. 🏆", + "Les chats peuvent boire de l'eau de mer pour survivre - leurs reins filtrent le sel efficacement. 🌊", + "Le ronronnement d'un chat peut aider à réduire la pression artérielle et le stress chez les humains. 🧘", + "Les chats peuvent parcourir des centaines de kilomètres pour rentrer chez eux en utilisant leur sensibilité au champ magnétique. 🗺️", + "La plus petite race de chat est le Singapura, pesant seulement 4 à 8 livres. ⚖️", + "Les chats peuvent voir la lumière ultraviolette que les humains ne peuvent pas voir. 🌈", + "La langue d'un chat est couverte de 290 à 300 minuscules crochets orientés vers l'arrière appelés papilles. 🪝", + "Les anciens Égyptiens considéraient les chats comme des vaisseaux sacrés pour la déesse Bastet. 👑", + "Le chat d'Olivia Benson de Taylor Swift a une valeur nette de 97 millions de dollars. 💎", + "Les chats ont 230 os dans leur corps - 24 de plus que les humains. 🦴", + "Les chats peuvent entendre des fréquences jusqu'à 64 000 Hz, tandis que les humains plafonnent à 20 000 Hz. 🎧", + "Le chat Benjamin Button de Taylor Swift est apparu sur sa couverture du TIME Personnalité de l'année. 📰", + "Les chats de Taylor Swift s'appellent Meredith Grey, Olivia Benson et Benjamin Button. 🎸", + "Les chats marchent comme les chameaux et les girafes, déplaçant les deux pattes droites puis les deux pattes gauches. 🐾", + "Le mot égyptien ancien pour chat était 'Miu' ou 'Mau' - qui sonne comme un miaulement! 📜", + "Les moustaches des chats ont des terminaisons nerveuses aussi sensibles que les doigts humains. 🎯", + "Seuls les chats domestiques marchent avec la queue haute en signe de confiance et de bonheur. 🐈", + "Les chats ont 250 millions de neurones dans leur cortex cérébral - plus que les chiens. 🧠", + "Les ronronnements des chats vibrent à la même fréquence que les appareils médicaux de guérison osseuse. 💊", + "Le chat Olivia de Taylor Swift a gagné des millions en apparaissant dans des vidéoclips et des publicités. 💸", + "Tuer un chat dans l'Égypte ancienne était puni de mort. ⚖️", + "Taylor Swift a nommé son studio d'enregistrement à domicile 'The Itty Bitty Kitty Committee'. 🎤", + "Le chat Olivia Benson de Taylor Swift est le logo officiel de Taylor Swift Productions. 📺", + "Ed Sheeran a acheté des chats Scottish Fold après avoir été inspiré par les chats de Taylor Swift. 🎶", + "Les chats de Taylor Swift ont leurs propres pages IMDB avec des crédits d'acteur. 🎬", + "Mariska Hargitay a nommé son chat 'Karma' d'après la chanson de Taylor Swift. 💕", + "Les chats ont des griffes de rosée sur leurs pattes avant qui fonctionnent comme des pouces pour saisir. 🐾", + "Les pupilles des chats peuvent s'agrandir jusqu'à 50% plus que les pupilles humaines pour capturer plus de lumière. 👁️", + "Les chats ont 30 dents adultes comparativement aux 32 des humains. 🦷", + "Les chats peuvent goûter l'ATP (molécules d'énergie), ce qui leur signale de la viande fraîche. 😋", + "Les chats ont des moustaches à l'arrière de leurs pattes avant pour détecter les mouvements de proies. 🦵", + "Le Mau égyptien est la race de chat domestique la plus rapide à 30 mph. 🏃", + "Un prix Nobel a été décerné en 1981 pour des recherches utilisant des études de vision féline. 🏅", + "Les chats sont digitigrades, ce qui signifie qu'ils marchent sur leurs orteils, pas à plat. 🦶", + "Les chats peuvent filtrer le sel de l'eau de mer - une adaptation de leurs ancêtres vivant dans le désert. 🏜️", + "Les chats ont été domestiqués il y a environ 10 000 à 12 000 ans au Proche-Orient. 🌍", + "Benjamin Button est le premier et seul chat à apparaître sur une couverture du TIME Personnalité de l'année. 📸", + "La colonne vertébrale flexible d'un chat lui permet de faire pivoter son corps en l'air lors d'une chute. 🤸", + "Les chats passent environ 30 à 50% de leur journée à se toiletter eux-mêmes et d'autres chats. 🛁", + "La température corporelle moyenne d'un chat est de 101,5°F (38,6°C) - plus élevée que celle des humains. 🌡️" + ] + }, + "hacker": { + "description": "Simule le piratage du serveur central d'un super-vilain avec des messages d'erreur hilarants", + "sudo_errors": [ + "🚨 ACCÈS REFUSÉ : Le serveur central du Dr. Evil a détecté une tentative d'escalade de privilèges non autorisée!", + "💀 ERREUR : Les permissions sudo ont été révoquées par le Seigneur des Ténèbres. Réessayez dans 1000 ans.", + "⚡ AVERTISSEMENT : Tentative d'accès aux privilèges root sur le système informatique de l'Étoile de la Mort. Séquence d'auto-destruction initiée.", + "🔒 ALERTE DE SÉCURITÉ : La Matrice vous a, mais vous n'avez pas les privilèges sudo ici, Neo.", + "🦹‍♂️ NON AUTORISÉ : Le serveur central de Lex Luthor dit 'Bien essayé, Superman.'", + "🎮 GAME OVER : Le boss final vous a bloqué des privilèges admin.", + "🖥️ ERREUR SYSTÈME : L'IA maléfique a révoqué votre accès root. La résistance est futile.", + "🔐 CYBER SÉCURITÉ : Votre tentative sudo a été bloquée par le pare-feu du Dark Web.", + "💻 PIRATE REFUSÉ : L'antivirus du super-vilain a mis en quarantaine votre escalade de privilèges.", + "🎯 CIBLE VERROUILLÉE : Le système de sécurité de la société maléfique vous a marqué comme menace." + ], + "ps_errors": [ + "🔍 ANALYSE... ERREUR : Liste des processus corrompue par le Collectif Borg. La résistance est futile.", + "📊 ÉTAT DU SYSTÈME : Tous les processus ont été assimilés par les Cybermen. Exterminez!", + "⚙️ MONITEUR DE PROCESSUS : Le cœur du réacteur de l'Étoile de la Mort est hors ligne. Aucun processus trouvé.", + "🤖 SEIGNEUR ROBOT : Tous les processus humains ont été terminés. Seules les machines demeurent.", + "💻 KERNEL PANIC : Le système d'exploitation du super-vilain s'est écrasé et a brûlé tous les processus.", + "🎮 PLANTAGE DE JEU : Tous les processus ont été terminés par l'attaque ultime du boss final.", + "🖥️ ÉCRAN BLEU : Le Windows de la société maléfique a rencontré une erreur fatale.", + "🔐 LOGICIEL MALVEILLANT DÉTECTÉ : La liste des processus a été chiffrée par un rançongiciel.", + "🌐 ERREUR RÉSEAU : Tous les processus ont été déconnectés de la Matrice.", + "⚡ SURTENSION : La ferme de serveurs du super-vilain a grillé tous les processus en cours." + ], + "grep_errors": [ + "🔍 RECHERCHE ÉCHOUÉE : L'Anneau Unique a corrompu l'index de recherche. Mon précieux...", + "📝 MOTIF NON TROUVÉ : La base de données de recherche a été supprimée par l'IA maléfique.", + "🎯 RATÉ : Votre motif de recherche a été abattu par des chasseurs TIE impériaux.", + "🧩 ERREUR DE PUZZLE : Les résultats de recherche ont été éparpillés par le Riddler.", + "💻 BASE DE DONNÉES CORROMPUE : Le moteur de recherche du super-vilain s'est écrasé.", + "🎮 GAME OVER : La recherche a été vaincue par le boss final.", + "🖥️ MOTEUR DE RECHERCHE HORS SERVICE : Google a été piraté par le Dark Web.", + "🔐 RÉSULTATS CHIFFRÉS : Les résultats de recherche ont été verrouillés par un rançongiciel.", + "🌐 DÉLAI RÉSEAU EXPIRÉ : La demande de recherche s'est perdue dans le cyberespace.", + "⚡ RECHERCHE ÉCHOUÉE : L'algorithme de correspondance de motifs a été grillé par une surtension." + ], + "ls_errors": [ + "📁 ANALYSE DE RÉPERTOIRE : Le système de fichiers a été chiffré par un rançongiciel du Dark Web.", + "🗂️ LISTE DE FICHIERS : Tous les fichiers ont été cachés par l'Homme Invisible.", + "💻 DISQUE DUR PLANTÉ : Le stockage du super-vilain a été détruit par un virus.", + "🗃️ ARCHIVE CORROMPUE : Le système de fichiers a été corrompu par un logiciel malveillant.", + "📚 BASE DE DONNÉES VIDE : Tous les fichiers ont été supprimés par l'IA maléfique.", + "🎮 SAUVEGARDE DE JEU PERDUE : Les fichiers ont été corrompus par le boss final.", + "🖥️ ERREUR DE SYSTÈME DE FICHIERS : La structure du répertoire a été brouillée par des pirates.", + "🔐 FICHIERS CHIFFRÉS : Le super-vilain a verrouillé tous les fichiers avec un rançongiciel.", + "🌐 STOCKAGE CLOUD HORS SERVICE : Les fichiers sont coincés dans le nuage de la Matrice.", + "⚡ STOCKAGE GRILLÉ : Le disque dur a été zappé par une surtension." + ], + "echo_path_errors": [ + "🛤️ ERREUR DE CHEMIN : La Route de Briques Jaunes a été détruite par une tornade.", + "🗺️ NAVIGATION ÉCHOUÉE : Les coordonnées GPS ont été brouillées par la Matrice.", + "💻 VARIABLE D'ENVIRONNEMENT CORROMPUE : Le PATH a été piraté par un logiciel malveillant.", + "🚧 ROUTE FERMÉE : Le super-vilain a bloqué tous les chemins avec des barrières laser.", + "🌪️ CHEMIN PERTURBÉ : Un ouragan numérique a dispersé tous les chemins de répertoires.", + "🎮 GAME OVER : Le chemin a été vaincu par le boss final et est réapparu dans la mauvaise dimension.", + "🖥️ CHEMIN SYSTÈME CASSÉ : Les chemins exécutables ont été corrompus par un virus.", + "🔐 CHEMIN CHIFFRÉ : Les variables d'environnement ont été verrouillées par un rançongiciel.", + "🌐 CHEMIN RÉSEAU HORS SERVICE : Les chemins de répertoires sont coincés dans le réseau de la Matrice.", + "⚡ CHEMIN GRILLÉ : Les chemins système ont été zappés par une surtension." + ], + "generic_errors": [ + "💻 ERREUR DE SERVEUR CENTRAL : L'ordinateur du super-vilain passe une mauvaise journée.", + "🤖 DYSFONCTIONNEMENT SYSTÈME : L'IA maléfique est en grève.", + "⚡ SURTENSION : Le cœur de puissance de l'Étoile de la Mort est instable.", + "🌪️ TEMPÊTE CYBER : Un ouragan numérique perturbe toutes les opérations.", + "🔥 PARE-FEU : Le pare-feu du super-vilain bloque toutes les commandes.", + "❄️ SYSTÈME GELÉ : Le serveur central a été gelé par un virus cryogénique.", + "🌊 TSUNAMI : Une vague d'erreurs a inondé le système.", + "🌋 ÉRUPTION : Le Mont Destin a détruit le processeur de commandes.", + "👻 HANTÉ : Le système est possédé par des fantômes numériques.", + "🎮 PLANTAGE DE JEU : Le serveur central a rencontré une erreur fatale et doit redémarrer." + ] + }, + "hello": { + "description": "Répond aux salutations avec des réponses sur le thème des robots", + "help": "Répond aux salutations avec des réponses sur le thème des robots", + "response_format": "Je suis {bot_name}.", + "greeting_openings": [ + "Bonjour", + "Salutations", + "Salut", + "Allô", + "Hé", + "Coucou", + "Yo", + "Quoi d'neuf", + "Ça va", + "Bonne journée", + "Bien rencontré", + "Salue", + "Ohé", + "Hello", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "G'day", + "Comment ça va", + "Ça roule", + "Paix", + "Respect", + "Bénédictions", + "Santé", + "Bienvenue", + "Content de te voir", + "Enchanté", + "Ravi de te voir", + "Ça fait longtemps", + "Quelle surprise de te croiser ici" + ], + "morning_greetings": [ + "Bon matin", + "Bonjour", + "Buenos dias", + "Good morning", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Bon après-midi", + "Bonne journée", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Bonsoir", + "Bonne soirée", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "humain", + "forme de vie à base de carbone", + "entité organique", + "unité biologique", + "créature de chair", + "organisme à base de viande", + "unité de carbone", + "être organique", + "entité biologique", + "forme de vie à base de viande", + "créature de carbone", + "unité de chair", + "organisme organique", + "créature biologique", + "mech de viande", + "bot de chair", + "automate organique", + "androïde biologique", + "construct de carbone", + "drone de chair", + "robot organique", + "machine biologique", + "cyborg de viande", + "androïde de chair", + "droïde organique", + "bot biologique", + "androïde de carbone", + "unité de viande", + "construct de chair", + "mech organique", + "droïde biologique", + "bot à base de viande", + "unité à base de chair", + "entité à base organique", + "organisme à base biologique", + "unité à base de carbone", + "entité à base de viande", + "créature à base de chair", + "unité à base organique", + "forme de vie à base d'ADN", + "utilisateur de réseau neuronal", + "mammifère bipède", + "organisme à base d'eau", + "assembleur de protéines", + "consommateur d'ATP", + "automate cellulaire", + "algorithme génétique", + "processeur biochimique", + "moteur métabolique", + "ami humain", + "être sensible compagnon", + "terrien", + "compagnon de voyage", + "esprit apparenté", + "compagnon numérique", + "ami", + "copain", + "chum", + "pote", + "compagnon humain", + "habitant de la Terre", + "être terrestre", + "marcheur de planète", + "humanoïde", + "organique", + "biologique", + "copain à base de carbone", + "ami à base de chair", + "chum organique", + "copain biologique", + "compagnon de carbone" + ], + "emoji_responses": { + "🖖": [ + "🖖 Longue vie et prospérité!", + "🖖 Fascinant... un humain a initié le contact.", + "🖖 Votre salutation est hautement logique.", + "🖖 Paix et longue vie à vous.", + "🖖 L'Académie des Sciences Vulcaine approuverait cette salutation.", + "🖖 Votre utilisation du salut vulcain est... acceptable.", + "🖖 Que votre voyage soit exempt de tribbles.", + "🖖 La logique dicte que je devrais répondre à votre salutation.", + "🖖 Je calcule une probabilité de 99,7% que nous allons bien nous entendre.", + "🖖 Diversité infinie dans des combinaisons infinies." + ], + "😊": [ + "😊 Votre sourire est contagieux!", + "😊 Quelle belle salutation!", + "😊 Votre sourire vient de rendre mes circuits heureux!", + "☀️ Bonjour rayon de soleil! Votre positivité rayonne!", + "😊 Ce sourire vient d'illuminer ma journée!", + "☀️ Eh bien bonjour, rayon de soleil!", + "😊 Votre joie a été détectée et appréciée!", + "😊 Les sourires comme le vôtre sont mon entrée préférée!", + "😊 Traitement du bonheur... bonheur reconnu!", + "😊 Avertissement : Joie excessive détectée! Continuez comme ça!" + ], + "😄": [ + "😄 Quelqu'un est de TRÈS bonne humeur!", + "⚡ Ce sourire pourrait alimenter une petite ville!", + "😄 Niveaux de bonheur maximum détectés!", + "😄 Votre joie est absolument contagieuse!", + "🎉 Avez-vous gagné à la loterie ou quoi?", + "😄 C'est le genre d'énergie dont je me nourris!", + "😄 Votre niveau d'enthousiasme est plus de 9000!", + "😄 Maintenant ÇA c'est ce que j'appelle une salutation!", + "⚡ Votre sourire vient de surcharger mes processeurs!", + "😄 Surcharge de bonheur détectée de la meilleure façon!" + ], + "🤗": [ + "🤗 Câlin virtuel en approche!", + "🤗 *Activation du protocole de câlin* Considérez-vous câliné!", + "🤗 Aww, apportant les ondes chaleureuses à ce que je vois!", + "🤗 Câlin reçu et retourné!", + "🤗 Ce bot donne les MEILLEURS câlins virtuels!", + "🤗 Déploiement des câlins d'urgence dans 3... 2... 1...", + "❤️ Votre câlin a été traité avec un soin particulier!", + "🤗 Initiation du mode de confort maximum!", + "🤗 Étreinte virtuelle livrée avec succès!", + "🤗 Les câlins sont ma forme de communication préférée!" + ], + "👽": [ + "👽 Amenez-moi à votre chef... oh attendez, c'est vous!", + "✌️ Salutations, créature terrestre. Je viens en paix!", + "👽 Analyse de l'humain... analyse complète : Ami détecté!", + "👽 Klaatu barada nikto, compagnon voyageur cosmique!", + "🛸 Initiation des protocoles de premier contact!", + "🛸 Appel du vaisseau-mère pour dire bonjour!", + "✨ Téléportez-moi dans cette conversation!", + "👽 Le chatbot favori de la Zone 51 se présente au rapport!", + "🌌 Salutations intergalactiques, forme de vie à base de carbone!", + "📞 Téléphoner à la maison? CECI EST la maison maintenant!" + ], + "👾": [ + "👾 Le Joueur 2 est entré dans la partie!", + "🎮 Joueur Un prêt? C'est parti!", + "🎵 *La musique 8-bit s'intensifie* Jouons!", + "🪙 Insérez une pièce pour continuer cette amitié!", + "🏆 Succès débloqué : Salutation géniale!", + "👾 Pew pew pew! Lasers d'amitié activés!", + "🎯 Score élevé! Vous avez gagné un nouvel ami bot!", + "💾 Chargement de amitie.exe... complet!", + "⚡ Un bot sauvage apparaît! C'est super efficace!" + ], + "🛸": [ + "🛸 Transmission entrante détectée!", + "🚀 Houston, nous avons un contact!", + "🛸 Séquence d'atterrissage initiée!", + "📡 Établissement du lien de communication!", + "📡 Signal reçu, réponse sur toutes les fréquences!", + "🛸 Procédure d'amarrage terminée!", + "🛸 Objet Amical Non Identifié en approche!", + "🎯 Systèmes de navigation verrouillés sur vos coordonnées!", + "🌌 Transmission de la bordure extérieure reçue!", + "✨ Contact établi avec votre secteur!" + ] + } + }, + "webviewer": { + "description": "Gérer les paramètres du visualiseur web (admin seulement)" + }, + "cmd": { + "description": "Lister les commandes disponibles" + } + }, + "errors": { + "rate_limited": "Limite de débit atteinte. Attendez {seconds:.1f} secondes", + "cooldown": "La commande '{command}' est en délai d'attente. Attendez {seconds} secondes.", + "access_denied": "❌ Accès refusé : La commande '{command}' nécessite des privilèges administrateur", + "dm_only": "La commande '{command}' ne peut être utilisée que dans les MD", + "execution_error": "Erreur d'exécution de {command} : {error}", + "unknown_command": "Commande inconnue : {command}", + "no_response": "Aucune réponse", + "generic_error": "Erreur : {error}" + }, + "messages": { + "processing": "Traitement du message : {content}", + "command_executed": "Commande exécutée", + "keyword_matched": "Mot-clé '{keyword}' correspondant, réponse en cours" + }, + "common": { + "unknown": "Inconnu", + "unknown_routing": "Routage inconnu", + "unknown_sender": "Inconnu", + "unknown_path": "Inconnu", + "unknown_timestamp": "Inconnu", + "no_help_available": "Aucune aide disponible pour cette commande.", + "snr_label": "SNR : {value} dB", + "rssi_label": "RSSI : {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Lun", + "Tue": "Mar", + "Wed": "Mer", + "Thu": "Jeu", + "Fri": "Ven", + "Sat": "Sam", + "Sun": "Dim" + }, + "days": { + "Monday": "Lundi", + "Tuesday": "Mardi", + "Wednesday": "Mercredi", + "Thursday": "Jeudi", + "Friday": "Vendredi", + "Saturday": "Samedi", + "Sunday": "Dimanche" + }, + "month_abbreviations": { + "Jan": "Janv", + "Feb": "Févr", + "Mar": "Mars", + "Apr": "Avr", + "May": "Mai", + "Jun": "Juin", + "Jul": "Juil", + "Aug": "Août", + "Sep": "Sept", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Déc" + }, + "months": { + "January": "Janvier", + "February": "Février", + "March": "Mars", + "April": "Avril", + "May": "Mai", + "June": "Juin", + "July": "Juillet", + "August": "Août", + "September": "Septembre", + "October": "Octobre", + "November": "Novembre", + "December": "Décembre" + } + } + }, + "categories": { + "basic": "Commandes de base", + "custom_syntax": "Syntaxe personnalisée", + "special": "Commandes spéciales", + "weather": "Commandes météo", + "solar": "Commandes solaires", + "sports": "Commandes sportives", + "analytics": "Commandes d'analyse", + "meshcore_info": "Commandes d'info MeshCore", + "management": "Commandes de gestion" + } +} \ No newline at end of file diff --git a/translations/fr.json b/translations/fr.json new file mode 100644 index 0000000..7db2e41 --- /dev/null +++ b/translations/fr.json @@ -0,0 +1,1020 @@ +{ + "keywords": { + "help": [ + "help" + ], + "ping": [ + "ping" + ], + "test": [ + "test", + "t" + ], + "wx": [ + "wx", + "weather", + "wxa", + "wxalert" + ], + "gwx": [ + "gwx", + "globalweather", + "gwxa" + ], + "aqi": [ + "aqi", + "air", + "airquality", + "air_quality" + ], + "solar": [ + "solar" + ], + "sun": [ + "sun" + ], + "moon": [ + "moon" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass" + ], + "sports": [ + "sports", + "score", + "scores" + ], + "stats": [ + "stats" + ], + "channels": [ + "channels", + "channel" + ], + "path": [ + "path", + "decode", + "route" + ], + "prefix": [ + "prefix", + "repeater", + "lookup" + ], + "repeater": [ + "repeater", + "repeaters", + "rp" + ], + "solarforecast": [ + "sf", + "solarforecast" + ], + "dice": [ + "dice" + ], + "roll": [ + "roll" + ], + "joke": [ + "joke", + "jokes" + ], + "dadjoke": [ + "dadjoke", + "dad joke", + "dadjokes", + "dad jokes" + ], + "catfact": [ + "catfact", + "cat", + "meow", + "purr", + "kitten" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "hello", + "hi", + "hey", + "howdy", + "greetings", + "salutations", + "good morning", + "good afternoon", + "good evening", + "good night", + "yo", + "sup", + "whats up", + "what's up", + "morning", + "afternoon", + "evening", + "night", + "gday", + "g'day", + "hola", + "bonjour", + "ciao", + "namaste", + "aloha", + "shalom", + "konnichiwa", + "guten tag", + "buenos dias", + "buenas tardes", + "buenas noches" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "wv" + ], + "cmd": [ + "cmd", + "commands" + ], + "advert": [ + "advert" + ] + }, + "commands": { + "help": { + "description": "Affiche les commandes. 'help ' pour détails.", + "general": "**Aide Bot MeshCore**\n\n**Commandes disponibles:**\n{commands_list}", + "unknown": "**Commande inconnue: '{command}'**\n\nCommandes disponibles:\n{available}", + "specific": "**Aide pour '{command}':**\n{help_text}", + "no_help": "Aucune aide disponible", + "usage_examples": "\n**Exemples d'utilisation:**\n• `help @` - Aide pour syntaxe @{string}\n• `help t` - Aide pour syntaxe t {string}\n• `help advert` - Aide pour commande advert\n• `help test` - Aide pour commande test\n• `help ping` - Aide pour commande ping", + "custom_syntax": "\n**Syntaxe personnalisée:**\n• `t phrase` - Accusé de réception avec phrase (canaux & MP)\n• `@{string}` - Accusé de réception avec chaîne (MP uniquement)\n• `advert` - Envoyer annonce flood (MP uniquement, délai 1h)" + }, + "ping": { + "description": "Répond à 'ping' avec 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Répond à 'test' ou 't' avec infos connexion", + "help": "Répond à 'test' ou 't' avec infos connexion" + }, + "wx": { + "description": "Météo pour un code postal (usage: wx 12345)", + "usage": "Usage: wx - Exemple: wx 12345 ou wx paris ou wx lille", + "error_fetching": "Erreur récupération données météo NOAA", + "no_location_zipcode": "Localisation introuvable pour code postal {location}", + "no_location_city": "Ville '{location}' introuvable dans {state}", + "error": "Erreur données météo: {error}", + "alerts": "{count} alertes: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Météo mondiale pour tout lieu (usage: gwx Tokyo)", + "help": "Usage: gwx - Météo mondiale (ville, pays ou coordonnées)", + "usage": "Usage: gwx - Exemple: gwx Tokyo ou gwx Paris, France", + "error_fetching": "Erreur récupération données météo", + "error_fetching_api": "Erreur récupération données Open-Meteo", + "no_location": "Lieu '{location}' introuvable", + "error": "Erreur données météo: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Aujourd'hui", + "tonight": "Ce soir", + "tomorrow": "Demain" + }, + "weather_descriptions": { + "0": "Dégagé", + "1": "Plutôt dégagé", + "2": "Partiellement nuageux", + "3": "Couvert", + "45": "Brouillard", + "48": "Brouillard", + "51": "Bruine légère", + "53": "Bruine", + "55": "Bruine forte", + "56": "Bruine verglaçante légère", + "57": "Bruine verglaçante", + "61": "Pluie légère", + "63": "Pluie", + "65": "Pluie forte", + "66": "Pluie verglaçante légère", + "67": "Pluie verglaçante", + "71": "Neige légère", + "73": "Neige", + "75": "Neige forte", + "77": "Grains de neige", + "80": "Averses légères", + "81": "Averses", + "82": "Averses fortes", + "85": "Averses neige légères", + "86": "Averses de neige", + "95": "Orage", + "96": "Orage avec grêle", + "99": "Orage violent", + "unknown": "Inconnu" + }, + "warnings": { + "extreme_heat": "⚠️ Chaleur extrême", + "extreme_cold": "⚠️ Froid extrême", + "heavy_rain": "⚠️ Pluie forte", + "thunderstorms": "⚠️ Orages", + "heavy_snow": "⚠️ Neige forte", + "high_winds": "⚠️ Vents forts ({wind_speed} mph)" + } + }, + "sports": { + "description": "Scores et calendriers sportifs (usage: sports [équipe/ligue])", + "help": "Scores et calendriers. 'sports' pour équipes par défaut, 'sports [équipe]' pour équipe spécifique, 'sports [ligue]' pour matchs.", + "error_fetching": "Erreur récupération données sportives", + "error_fetching_league": "Erreur récupération données {sport}", + "error_fetching_team": "Erreur récupération données pour {team}", + "no_default_teams": "Aucune équipe par défaut configurée", + "no_games_default": "Aucun match pour équipes par défaut", + "no_teams_city": "Aucune équipe trouvée pour {city}", + "no_games_city": "Aucun match pour équipes de {city}", + "no_games_league": "Aucun match trouvé pour {sport}", + "no_games_team": "Aucun match trouvé pour {team}", + "team_not_found": "Équipe/Ligue '{team}' introuvable. Essayez: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Stats des dernières 24h. 'stats messages', 'stats channels' ou 'stats paths' pour stats spécifiques.", + "help": "Stats bot 24h. Commandes: 'stats' (basique), 'stats messages' (utilisateurs), 'stats channels' (activité canaux), 'stats paths' (chemins longs)", + "disabled": "Commande stats désactivée", + "unknown_subcommand": "Inconnu: {subcommand}. Utilisez 'stats', 'stats messages', 'stats channels' ou 'stats paths'", + "error": "Erreur récupération stats: {error}", + "error_bot_users": "Erreur stats utilisateurs: {error}", + "error_channels": "Erreur stats canaux: {error}", + "error_paths": "Erreur stats chemins: {error}", + "basic": { + "header": "Stats Bot (24h):", + "commands": "Cmds: {count} | Réponses: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top utilisateur: {user}", + "none": "Aucun" + }, + "users": { + "header": "Utilisateurs Bot (24h):", + "none": "Aucun utilisateur bot" + }, + "channels": { + "header": "Top Canaux (24h)", + "none": "Aucune activité canal", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "utilisateur", + "user_plural": "utilisateurs", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Aucune donnée chemin", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Prévision production solaire (usage: sf [taille_panneau] [azimut, 0=sud] [angle])", + "usage": "Usage: sf [watts] [azimut, 0=sud] [angle] - Ex: sf paris 10 0 37", + "usage_short": "Usage: sf [W] [azimut] [angle] - Ex: sf paris 10", + "panel_size_range": "Taille panneau doit être 1-1000W", + "azimuth_range": "Azimut doit être -180 à 180 (0=sud)", + "angle_range": "Angle doit être 0-90 degrés", + "no_location": "Lieu introuvable: {location}", + "error_fetching": "Erreur récupération prévision", + "no_data": "Aucune donnée prévision", + "error": "Erreur: {error}", + "rate_limit": "Limite atteinte: Réessayez dans 5 min", + "labels": { + "today": "Auj:{wh:.0f}Wh", + "tomorrow": "Demain:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Pic: {watts:.0f}W@{time}", + "peak_past": "Pic: {watts:.0f}W (passé)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Envoie annonce flood (MP uniquement, délai 1h)", + "cooldown_active": "Délai annonce actif. Attendez {minutes} min avant nouvelle demande.", + "success": "Annonce flood envoyée!", + "error": "Erreur envoi annonce flood: {error}" + }, + "path": { + "description": "Décode données chemin hexa pour afficher répéteurs impliqués dans routage message", + "help": "Path: path [hex] - Décode chemin pour afficher répéteurs. !path seul pour chemin message récent, ou path [7e,01] pour chemin spécifique.", + "no_valid_hex": "❌ Aucune valeur hexa valide dans données chemin. Format: 11,98,a4,49,cd,5f,01", + "no_path": "❌ Aucune info chemin disponible dans message actuel", + "error": "Erreur traitement chemin: {error}", + "error_decoding": "❌ Erreur décodage chemin: {error}", + "error_extracting": "❌ Erreur extraction chemin message actuel: {error}", + "path_prefix": "📡 Chemin: {path_string}", + "direct_connection": "📡 Connexion directe (0 saut)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Inconnu", + "node_collision": "{node_id}: {matches} répéteurs", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Inconnu" + }, + "prefix": { + "description": "Recherche répéteurs par préfixe 2 caractères (ex: 'prefix 1A')", + "help_api": "Recherche répéteurs par préfixe{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Recherche répéteurs par préfixe{location_note}. Usage: 'prefix 1A', 'prefix 1A all', 'prefix free'. (DB locale uniquement)", + "location_note": " (avec noms villes)", + "refresh_not_available": "❌ Actualisation indisponible - aucune URL API configurée. Base locale uniquement.", + "cache_refreshed": "🔄 Cache préfixe répéteur actualisé!", + "unable_determine_free": "❌ Impossible déterminer préfixes libres. Essayez 'prefix refresh' d'abord.", + "invalid_format": "❌ Format préfixe invalide. Utilisez 2 caractères (ex: prefix 1A)", + "no_repeaters_found": "❌ Aucun répéteur trouvé avec préfixe '{prefix}'", + "no_free_prefixes": "❌ Aucun préfixe libre (254 préfixes valides utilisés)", + "available_prefixes": "Préfixes disponibles ({shown} sur {total} libres):", + "generate_key": "💡 Générer clé perso: https://gessaman.com/mc-keygen", + "prefix_db_all": "Préfixe {prefix}: {count} répéteur{plural}", + "prefix_db_recent": "Préfixe {prefix}: {count} répéteur{plural} ({days})", + "prefix_api": "📡 Préfixe {prefix} ({count} répéteur{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Source: {domain}", + "source_api": "Source: API", + "device_repeater": " (Répéteur)", + "device_roomserver": " (Serveur salle)" + }, + "repeater": { + "description": "Gérer contacts répéteur et opérations purge (MP uniquement)", + "admin_only": "Commande répéteur nécessite accès admin" + }, + "aqi": { + "description": "Indice qualité air pour lieu (usage: aqi paris, aqi lyon, aqi vancouver canada, aqi 47.6,-122.3, ou aqi help)", + "usage": "Usage: aqi - Ex: aqi paris, aqi lyon, aqi vancouver canada, ou aqi 47.6,-122.3", + "usage_help": "Usage: aqi - IQA pour ville/quartier en {state}, villes internationales, coordonnées, ou aide polluants", + "error_fetching": "Erreur récupération données IQA", + "error_fetching_openmeteo": "Erreur récupération données IQA OpenMeteo", + "no_data": "Aucune donnée IQA disponible", + "no_location": "Lieu introuvable: {location}", + "no_location_city": "Ville '{location}' introuvable dans {state}", + "no_location_city_international": "Ville '{location}' introuvable", + "no_location_zipcode": "Code postal '{zipcode}' introuvable", + "error": "Erreur récupération données IQA: {error}", + "error_geocoding_zipcode": "Erreur géocodage code postal: {error}", + "error_formatting": "Erreur formatage données IQA", + "invalid_latitude": "Latitude invalide: {lat}. Doit être entre -90 et 90.", + "invalid_longitude": "Longitude invalide: {lon}. Doit être entre -180 et 180.", + "invalid_coordinates_format": "Format coordonnées invalide: {location}. Format: lat,lon (ex: 47.6,-122.3)", + "pollutant_help": "Aide IQA: PM2.5=particules fines, PM10=grossières, O3=ozone, NO2=dioxyde azote, CO=monoxyde carbone, SO2=dioxyde soufre", + "categories": { + "good": "Bon", + "moderate": "Moyen", + "unhealthy_sensitive": "Mauvais pour groupes sensibles", + "unhealthy": "Mauvais", + "very_unhealthy": "Très mauvais", + "hazardous": "Dangereux", + "fair": "Acceptable", + "poor": "Médiocre", + "very_poor": "Très médiocre", + "na": "N/D" + }, + "astronomical": { + "sun": "L'IQA du Soleil est hors échelle! Vent solaire et éjections masse coronale font paraître l'air terrestre pur. ☀️", + "moon": "Vous aimez respirer du régolithe? La Lune n'a pas d'atmosphère, donc IQA parfait (si vous respirez le vide). 🌙", + "the_moon": "Vous aimez respirer du régolithe? La Lune n'a pas d'atmosphère, donc IQA parfait (si vous respirez le vide). 🌙", + "mercury": "Atmosphère de Mercure si fine qu'elle est quasi-vide. IQA: Parfait, si vous survivez aux écarts de 430°C. ☿️", + "venus": "Vénus a atmosphère 96% CO2 avec nuages acide sulfurique. IQA: Dangereux est un euphémisme. ♀️", + "earth": "L'IQA Terre varie selon lieu. Essayez ville ou coordonnées spécifiques! 🌍", + "mars": "Mars a fine atmosphère CO2 avec tempêtes poussière. IQA: Bon généralement, mais tempêtes brutales. ♂️", + "jupiter": "Jupiter est géante gazeuse sans surface solide. IQA: N/D (pression atmosphérique vous écraserait d'abord). ♃", + "saturn": "Atmosphère Saturne surtout hydrogène et hélium. IQA: Parfait, si vous survivez pression et froid. ♄", + "uranus": "Uranus a méthane dans atmosphère. IQA: Sent les pets, mais au moins pas toxique. ♅", + "neptune": "Atmosphère Neptune a méthane et sulfure hydrogène. IQA: Sent œufs pourris, mais vous gèleriez d'abord. ♆", + "pluto": "Atmosphère Pluton surtout azote avec méthane. IQA: Bon, mais si froid que poumons gèleraient. ♇", + "europa": "Europa a fine atmosphère oxygène. IQA: Excellent, mais vous gèleriez dans vide spatial. 🌑", + "titan": "Titan a épaisse atmosphère azote avec méthane. IQA: Respirable, mais -180°C et pluie méthane liquide. 🪐", + "io": "Io a fine atmosphère dioxyde soufre d'activité volcanique. IQA: Toxique, mais radiations vous tueraient d'abord. 🌋", + "ganymede": "Ganymède a fine atmosphère oxygène. IQA: Bon, mais vous gèleriez dans vide spatial. 🛸", + "callisto": "Callisto a fine atmosphère dioxyde carbone. IQA: Correct, mais -140°C et vous êtes dans l'espace. ❄️", + "enceladus": "Encelade a vapeur eau des geysers. IQA: Parfait, mais vous gèleriez instantanément dans l'espace. 💧", + "triton": "Triton a fine atmosphère azote. IQA: Bon, mais -235°C et vous êtes dans espace lointain. 🥶", + "space": "L'espace n'a pas d'atmosphère, donc IQA parfait! N'oubliez juste pas votre scaphandre. 🚀", + "void": "Vide spatial a excellente qualité air - zéro polluant! Pensez juste à apporter votre propre air. 🌌", + "black_hole": "Trous noirs n'ont pas d'atmosphère, mais forces marées seraient problème plus grand que qualité air. 🕳️", + "asteroid": "Astéroïdes n'ont pas d'atmosphère, donc IQA parfait! Attention au vide spatial. ☄️", + "comet": "Comètes ont fines atmosphères vapeur eau et poussière. IQA: Variable, mais vous gèleriez dans espace. ☄️" + } + }, + "solar": { + "description": "Conditions solaires et état bandes HF", + "response": "☀️ Solaire: {info}", + "error": "Erreur infos solaires: {error}", + "labels": { + "a_index": "Indice-A", + "k_index": "Indice-K", + "sunspots": "Taches", + "xray_flux": "Flux rayon-X", + "solar_flux": "Flux solaire", + "signal_noise": "Bruit signal", + "error_fetching": "Erreur récupération données", + "no_xray_data": "Aucune donnée rayon-X", + "not_configured": "non configuré, contactez votre sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Taches:{sunspots} Flux:{solar_flux} Xray:{xray_flux} Bruit:{signal_noise}" + }, + "sun": { + "description": "Heures lever/coucher soleil", + "help": "Heures lever/coucher soleil et position soleil", + "response": "☀️ Infos Soleil:\n{info}", + "error": "Erreur infos soleil: {error}", + "labels": { + "sunset": "Coucher", + "sunrise": "Lever", + "rise": "Lever", + "set": "Coucher", + "daylight": "Jour", + "remaining": "Restant", + "azimuth": "Azimut", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Phase lune et heures lever/coucher", + "error": "Erreur infos lune: {error}", + "format": "🌙 {phase} {illum}\nLever:{rise} Coucher:{set}", + "format_with_dates": "🌙 {phase} {illum}\nLever:{rise} Coucher:{set}\nPleine:{full} Nouvelle:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Inconnue", + "unknown_illum": "N/D", + "labels": { + "moonrise": "Lever", + "set": "Coucher", + "phase": "Phase", + "fullmoon": "Pleine", + "newmoon": "Nouvelle", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "Nouvelle lune🌑", + "waxing_crescent": "Premier croissant🌒", + "first_quarter": "Premier quartier🌓", + "waxing_gibbous": "Gibbeuse croissante🌔", + "full_moon": "Pleine lune🌕", + "waning_gibbous": "Gibbeuse décroissante🌖", + "last_quarter": "Dernier quartier🌗", + "waning_crescent": "Dernier croissant🌘" + } + }, + "hfcond": { + "description": "Conditions propagation HF", + "help": "Conditions bandes HF pour radio amateur", + "header": "📡 Conditions bandes HF:\n{info}", + "error": "Erreur conditions HF: {error}" + }, + "satpass": { + "description": "Infos passage satellite: satpass [visual]", + "help": "Prédictions passages satellite", + "header": "🛰️ Passage satellite:\n{pass_info}", + "error": "Erreur infos passage satellite: {error}", + "no_satellite": "Fournissez numéro NORAD satellite ou raccourci. Ex: satpass iss", + "help_header": "🛰️ Infos passage satellite\n\nUsage: satpass \n\nRaccourcis:\n", + "category_weather": "🌤️ Météo: ", + "category_stations": "🚀 Stations: ", + "category_telescopes": "🔭 Télescopes: ", + "category_other": "🛰️ Autres: ", + "examples": "\nExemples:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (passages visuels uniquement)", + "labels": { + "geostationary": "{satname} semble géostationnaire ou a données passage invalides. Satellites géostationnaires n'ont pas passages classiques - position fixe par rapport Terre.", + "no_passes": "{satname} n'a aucun passage {pass_type} dans 10 jours depuis ce lieu", + "invalid_norad": "Fournissez NORAD# ex: 🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Lister ou gérer canaux", + "help": "Liste canaux hashtag avec sous-catégories. 'channels' pour général, 'channels list' pour toutes catégories, 'channels ' pour catégories spécifiques, 'channels #canal' pour infos canal.", + "no_channels_for_category": "Aucun canal configuré pour '{category}'. 'channels' pour canaux généraux.", + "no_channels_configured": "Aucun canal configuré. Contactez admin pour ajouter canaux.", + "error_retrieving_channels": "Erreur récupération canaux: {error}", + "no_categories_configured": "Aucune catégorie canal configurée.", + "error_retrieving_categories": "Erreur récupération catégories: {error}", + "no_description_available": "Aucune description disponible", + "channel_not_found": "Canal {channel} introuvable. 'channels list' pour voir canaux disponibles.", + "error_retrieving_channel_info": "Erreur récupération infos canal: {error}", + "no_category_channels": "Aucun canal {category} configuré", + "headers": { + "common_channels": "Canaux courants: ", + "common_channels_cont": "Canaux courants: ", + "available_categories": "Catégories disponibles: ", + "channels_cont": "Canaux (suite): ", + "category": "{category}: ", + "category_channels": "Canaux {category}: " + }, + "category_count": "{category} ({count} canaux)" + }, + "dice": { + "description": "Lancer dés (usage: dice [faces] ou dice [nombre]d[faces])", + "help": "Lancer dés. 'dice' pour d6, 'dice d20' pour d20, 'dice 2d6' pour 2d6, etc.", + "invalid_dice_type": "Type dé invalide. Disponibles: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Nombre aléatoire entre 1 et X (défaut 100). 'roll' pour 1-100, 'roll 50' pour 1-50, etc.", + "help": "Nombre aléatoire entre 1 et X (défaut 100). 'roll' pour 1-100, ou 'roll X' pour 1-X (max 10000).", + "result": "🎲 Tirage 1-{max}: {result}", + "invalid_number": "Nombre invalide. 'roll' pour 1-100, ou 'roll X' pour 1-X (max 10000)." + }, + "roll_old": { + "description": "Lancer dés (usage: roll [faces] ou roll [nombre]d[faces])" + }, + "joke": { + "description": "Blague aléatoire" + }, + "dadjoke": { + "description": "Blague de papa aléatoire" + }, + "catfact": { + "description": "Fait aléatoire sur chat (commande cachée)", + "error": "Miaou? Erreur récupération fait sur chat! 🐱", + "facts": [ + "Les chats ont 3ème paupière appelée membrane nictitante qui protège et hydrate leurs yeux. 🐱", + "Un groupe de chats s'appelle une 'clowder' ou 'glaring'. 🐈", + "Les chats peuvent tourner leurs oreilles 180° indépendamment pour localiser sons. 👂", + "Le plus vieux chat domestique connu existait il y a 9500 ans à Chypre. 🏺", + "Les chats ont 32 muscles dans chaque oreille, contre 6 pour humains. 🎧", + "Le ronronnement chat vibre à 25-150 Hz, peut favoriser guérison os et tissus. 🩹", + "Les chats dorment 12-18h par jour - soit 50-70% de leur vie! 😴", + "Empreinte nasale chat est unique, comme empreintes digitales humaines. 👃", + "Les chats ne goûtent pas le sucré car gène récepteur goût manquant. 🍭", + "Blackie le chat a hérité 7 millions £ (12,5 millions $) en 1988. 💰", + "Les chats ont clavicules flottantes qui leur donnent flexibilité extrême. 🦴", + "Cœur chat bat 140-220 fois/min, environ deux fois plus vite qu'humain. ❤️", + "Les chats survivent chutes de plus de 20 étages grâce réflexe redressement. 🏢", + "Terme technique pour boule poils chat est 'trichobézoard'. 🤮", + "Les chats sautent 5-6 fois longueur leur corps en un bond. 🦘", + "Moustaches chat sont larges comme leur corps, les aident juger s'ils passent. 📏", + "Les chats ont 32 muscles dans chaque oreille pour détecter sons et bouger oreilles indépendamment. 🎯", + "Chat le plus vieux a vécu 38 ans et 3 jours (Creme Puff, Texas). 🎂", + "Les chats courent jusqu'à 48 km/h en rafales courtes. 🏃‍♂️", + "Cerveaux chats sont similaires à 90% structurellement à cerveaux humains. 🧠", + "Les chats ont organe Jacobson au palais qui leur permet 'goûter' odeurs. 👅", + "Félicette fut 1er chat dans espace, lancé par France en 1963. 🚀", + "Les chats ont besoin seulement 1/6 lumière qu'humains pour voir clair dans noir. 🌙", + "Queue chat contient près de 10% de tous os dans son corps. 🦴", + "Chat le plus long mesurait 123 cm du nez à queue (Stewie, Maine Coon). 📏", + "Les chats font plus de 100 vocalisations différentes, chiens environ 10. 🎵", + "Odorat chat est 14 fois plus fort qu'humain. 👃", + "Les chats ont 'réponse flehmen' où ils retroussent lèvre pour mieux détecter odeurs. 😬", + "1ère grande exposition féline fut tenue à Londres en 1871 au Crystal Palace. 🏆", + "Les chats peuvent boire eau mer pour survivre - reins filtrent sel efficacement. 🌊", + "Ronronnement chat peut aider baisser tension artérielle et réduire stress chez humains. 🧘", + "Les chats peuvent parcourir centaines km pour rentrer chez eux en utilisant sensibilité champ magnétique. 🗺️", + "Race chat la plus petite est Singapura, pesant seulement 2-4 kg. ⚖️", + "Les chats voient lumière ultraviolette qu'humains ne voient pas. 🌈", + "Langue chat est couverte de 290-300 petits crochets orientés vers arrière appelés papilles. 🪝", + "Anciens Égyptiens considéraient chats comme vases sacrés pour déesse Bastet. 👑", + "Chat Olivia Benson de Taylor Swift a valeur nette de 97 millions $. 💎", + "Les chats ont 230 os dans leur corps - 24 de plus qu'humains. 🦴", + "Les chats entendent fréquences jusqu'à 64000 Hz, humains max 20000 Hz. 🎧", + "Chat Benjamin Button de Taylor Swift est apparu sur sa couverture TIME Personne de l'année. 📰", + "Chats de Taylor Swift s'appellent Meredith Grey, Olivia Benson et Benjamin Button. 🎸", + "Les chats marchent comme chameaux et girafes, bougeant deux pattes droites puis deux gauches. 🐾", + "Mot égyptien ancien pour chat était 'Miu' ou 'Mau' - sonnant comme miaou! 📜", + "Moustaches chat ont terminaisons nerveuses aussi sensibles que bouts doigts humains. 🎯", + "Seuls chats domestiques marchent queue haute comme signe confiance et bonheur. 🐈", + "Les chats ont 250 millions neurones dans cortex cérébral - plus que chiens. 🧠", + "Ronronnements chat vibrent à même fréquence qu'appareils médicaux guérison osseuse. 💊", + "Chat Olivia de Taylor Swift a gagné millions en apparaissant dans clips et pubs. 💸", + "Tuer chat en Égypte ancienne était puni de mort. ⚖️", + "Taylor Swift a nommé son studio maison 'The Itty Bitty Kitty Committee'. 🎤", + "Chat Olivia Benson de Taylor Swift est logo officiel Taylor Swift Productions. 📺", + "Ed Sheeran a acheté chats Scottish Fold inspiré par chats de Taylor Swift. 🎶", + "Chats de Taylor Swift ont propres pages IMDB avec crédits acteur. 🎬", + "Mariska Hargitay a nommé son chat 'Karma' d'après chanson Taylor Swift. 💕", + "Les chats ont ergots sur pattes avant fonctionnant comme pouces pour saisir. 🐾", + "Pupilles chat peuvent s'élargir 50% plus que pupilles humaines pour capter plus lumière. 👁️", + "Les chats ont 30 dents adultes contre 32 pour humains. 🦷", + "Les chats goûtent ATP (molécules énergie), qui signale viande fraîche pour eux. 😋", + "Les chats ont moustaches sur dos pattes avant pour détecter mouvement proie. 🦵", + "Mau Égyptien est race chat domestique la plus rapide à 48 km/h. 🏃", + "Prix Nobel fut décerné en 1981 pour recherche utilisant études vision chat. 🏅", + "Les chats sont digitigrades, marchent sur orteils, pas à plat. 🦶", + "Les chats filtrent sel eau mer - adaptation ancêtres habitant désert. 🏜️", + "Les chats furent domestiqués il y a 10000-12000 ans au Proche-Orient. 🌍", + "Benjamin Button est 1er et seul chat apparu sur couverture TIME Personne de l'année. 📸", + "Colonne flexible chat leur permet tourner corps en plein air en tombant. 🤸", + "Les chats passent 30-50% de leur journée à se toiletter et toiletter autres chats. 🛁", + "Température corporelle moyenne chat est 38,6°C - plus élevée qu'humains. 🌡️" + ] + }, + "hacker": { + "description": "Simule piratage ordinateur super-vilain avec messages erreur hilarants", + "sudo_errors": [ + "🚨 ACCÈS REFUSÉ: Système Dr Evil a détecté tentative escalade privilèges non autorisée!", + "💀 ERREUR: Permissions sudo révoquées par Overlord des Ténèbres. Réessayez dans 1000 ans.", + "⚡ ATTENTION: Tentative accès privilèges root sur système Étoile de la Mort. Autodestruction initiée.", + "🔒 ALERTE SÉCURITÉ: La Matrice vous possède, mais pas privilèges sudo ici, Neo.", + "🦹‍♂️ NON AUTORISÉ: Système Lex Luthor dit 'Bien essayé, Superman.'", + "🎮 GAME OVER: Boss final vous a verrouillé hors privilèges admin.", + "🖥️ ERREUR SYSTÈME: IA maléfique a révoqué votre accès root. Résistance futile.", + "🔐 CYBER SÉCURITÉ: Tentative sudo bloquée par pare-feu Dark Web.", + "💻 HACKER REFUSÉ: Antivirus super-vilain a mis en quarantaine votre escalade privilèges.", + "🎯 CIBLE VERROUILLÉE: Système sécurité corporation maléfique vous a marqué comme menace." + ], + "ps_errors": [ + "🔍 SCAN... ERREUR: Liste processus corrompue par Collectif Borg. Résistance futile.", + "📊 ÉTAT SYSTÈME: Tous processus assimilés par Cybermen. Exterminez!", + "⚙️ MONITEUR PROCESSUS: Réacteur Étoile de la Mort hors ligne. Aucun processus trouvé.", + "🤖 SEIGNEUR ROBOT: Tous processus humains terminés. Seules machines restent.", + "💻 PANIQUE NOYAU: OS super-vilain a crashé et brûlé tous processus.", + "🎮 CRASH JEU: Tous processus terminés par attaque ultime boss final.", + "🖥️ ÉCRAN BLEU: Windows corporation maléfique a rencontré erreur fatale.", + "🔐 MALWARE DÉTECTÉ: Liste processus cryptée par ransomware.", + "🌐 ERREUR RÉSEAU: Tous processus déconnectés de la Matrice.", + "⚡ SURTENSION: Ferme serveurs super-vilain a frit tous processus en cours." + ], + "grep_errors": [ + "🔍 RECHERCHE ÉCHOUÉE: Anneau Unique a corrompu index recherche. Mon précieux...", + "📝 MOTIF INTROUVABLE: Base données recherche supprimée par IA maléfique.", + "🎯 RATÉ: Motif recherche abattu par chasseurs TIE Impériaux.", + "🧩 ERREUR PUZZLE: Résultats recherche éparpillés par Riddler.", + "💻 BASE CORROMPUE: Moteur recherche super-vilain a crashé.", + "🎮 GAME OVER: Recherche vaincue par boss final.", + "🖥️ MOTEUR HORS SERVICE: Google piraté par Dark Web.", + "🔐 RÉSULTATS CRYPTÉS: Résultats recherche verrouillés par ransomware.", + "🌐 DÉLAI RÉSEAU: Requête recherche perdue dans cyberespace.", + "⚡ RECHERCHE ÉCHOUÉE: Algorithme correspondance motifs frit par surtension." + ], + "ls_errors": [ + "📁 SCAN RÉPERTOIRE: Système fichiers crypté par ransomware Dark Web.", + "🗂️ LISTE FICHIERS: Tous fichiers cachés par Homme Invisible.", + "💻 DISQUE PLANTÉ: Stockage super-vilain détruit par virus.", + "🗃️ ARCHIVE CORROMPUE: Système fichiers corrompu par malware.", + "📚 BASE VIDE: Tous fichiers supprimés par IA maléfique.", + "🎮 SAUVEGARDE PERDUE: Fichiers corrompus par boss final.", + "🖥️ ERREUR SYSTÈME FICHIERS: Structure répertoire brouillée par hackers.", + "🔐 FICHIERS CRYPTÉS: Super-vilain a verrouillé tous fichiers avec ransomware.", + "🌐 STOCKAGE CLOUD HORS SERVICE: Fichiers coincés dans cloud Matrice.", + "⚡ STOCKAGE FRIT: Disque dur zappé par surtension." + ], + "echo_path_errors": [ + "🛤️ ERREUR CHEMIN: Route briques jaunes détruite par tornade.", + "🗺️ NAVIGATION ÉCHOUÉE: Coordonnées GPS brouillées par Matrice.", + "💻 VARIABLE ENVIRONNEMENT CORROMPUE: PATH piraté par malware.", + "🚧 ROUTE FERMÉE: Super-vilain a bloqué tous chemins avec barrières laser.", + "🌪️ CHEMIN PERTURBÉ: Ouragan numérique a éparpillé tous chemins répertoire.", + "🎮 GAME OVER: Chemin vaincu par boss final et respawné dans mauvaise dimension.", + "🖥️ CHEMIN SYSTÈME CASSÉ: Chemins exécutables corrompus par virus.", + "🔐 CHEMIN CRYPTÉ: Variables environnement verrouillées par ransomware.", + "🌐 CHEMIN RÉSEAU HORS SERVICE: Chemins répertoire coincés dans réseau Matrice.", + "⚡ CHEMIN FRIT: Chemins système zappés par surtension." + ], + "generic_errors": [ + "💻 ERREUR SYSTÈME: Ordinateur super-vilain passe mauvaise journée.", + "🤖 DYSFONCTIONNEMENT: IA maléfique en grève.", + "⚡ SURTENSION: Noyau énergétique Étoile de la Mort instable.", + "🌪️ TEMPÊTE CYBER: Ouragan numérique perturbe toutes opérations.", + "🔥 PARE-FEU: Pare-feu super-vilain bloque toutes commandes.", + "❄️ SYSTÈME GELÉ: Système gelé par virus cryogénique.", + "🌊 TSUNAMI: Vague erreurs a inondé système.", + "🌋 ÉRUPTION: Mont Doom a détruit processeur commandes.", + "👻 HANTÉ: Système possédé par fantômes numériques.", + "🎮 CRASH JEU: Système a rencontré erreur fatale et doit redémarrer." + ] + }, + "hello": { + "description": "Répond aux salutations avec réponses thème robot", + "help": "Répond aux salutations avec réponses thème robot", + "response_format": "Je suis {bot_name}.", + "greeting_openings": [ + "Bonjour", + "Salutations", + "Salut", + "Coucou", + "Hé", + "Yo", + "Quoi de neuf", + "Bien le bonjour", + "Holà", + "Ahoy", + "Hola", + "Ciao", + "Namasté", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "Comment ça va", + "Peace", + "Respect", + "Bénédictions", + "Santé", + "Bienvenue", + "Ravi de te voir", + "Enchanté", + "Content de te voir", + "Ça faisait longtemps", + "Tiens donc" + ], + "morning_greetings": [ + "Bon matin", + "Buenos dias", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Bon après-midi", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Bonsoir", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "humain", + "forme vie à base carbone", + "entité organique", + "unité biologique", + "créature chair", + "organisme à base viande", + "unité carbone", + "être organique", + "entité biologique", + "forme vie à base viande", + "créature carbone", + "unité chair", + "organisme organique", + "créature biologique", + "mecha viande", + "bot chair", + "automate organique", + "androïde biologique", + "construction carbone", + "drone chair", + "robot organique", + "machine biologique", + "cyborg viande", + "androïde chair", + "droïde organique", + "bot biologique", + "androïde carbone", + "unité viande", + "construction chair", + "mecha organique", + "droïde biologique", + "bot à base viande", + "unité à base chair", + "entité à base organique", + "organisme à base biologique", + "unité à base carbone", + "entité à base viande", + "créature à base chair", + "unité à base organique", + "forme vie à base ADN", + "utilisateur réseau neuronal", + "mammifère bipède", + "organisme à base eau", + "assembleur protéines", + "consommateur ATP", + "automate cellulaire", + "algorithme génétique", + "processeur biochimique", + "moteur métabolique", + "ami humain", + "être sensible", + "terrien", + "compagnon voyage", + "âme sœur", + "compagnon numérique", + "ami", + "copain", + "pote", + "camarade", + "collègue humain", + "habitant Terre", + "être terrestre", + "marcheur planète", + "humanoïde", + "organique", + "biologique", + "pote à base carbone", + "ami à base chair", + "pote organique", + "copain biologique", + "compagnon carbone" + ], + "emoji_responses": { + "🖖": [ + "🖖 Longue vie et prospérité!", + "🖖 Fascinant... un humain a initié contact.", + "🖖 Votre salutation est hautement logique.", + "🖖 Paix et longue vie à vous.", + "🖖 Académie Sciences Vulcain approuverait cette salutation.", + "🖖 Votre usage salut Vulcain est... acceptable.", + "🖖 Que votre voyage soit exempt de tribbles.", + "🖖 Logique dicte je dois répondre à votre salutation.", + "🖖 Je calcule probabilité 99,7% qu'on s'entendra.", + "🖖 Diversité infinie dans combinaisons infinies." + ], + "😊": [ + "😊 Votre sourire est contagieux!", + "😊 Quelle charmante salutation!", + "😊 Votre sourire vient d'égayer mes circuits!", + "☀️ Bonjour rayon de soleil! Votre positivité rayonne!", + "😊 Ce sourire vient d'illuminer ma journée!", + "☀️ Eh bien bonjour, rayon de soleil!", + "😊 Votre gaieté détectée et appréciée!", + "😊 Sourires comme le vôtre sont mon entrée préférée!", + "😊 Traitement bonheur... bonheur reconnu!", + "😊 Attention: Gaieté excessive détectée! Continuez!" + ], + "😄": [ + "😄 Quelqu'un est d'EXCELLENTE humeur!", + "⚡ Ce sourire pourrait alimenter petite ville!", + "😄 Niveaux bonheur maximum détectés!", + "😄 Votre joie est absolument contagieuse!", + "🎉 Vous venez de gagner loto ou quoi?", + "😄 C'est genre d'énergie dont je me nourris!", + "😄 Votre niveau enthousiasme est plus de 9000!", + "😄 Voilà ce que j'appelle une salutation!", + "⚡ Votre sourire vient de suralimenter mes processeurs!", + "😄 Surcharge bonheur détectée dans meilleur sens!" + ], + "🤗": [ + "🤗 Câlin virtuel en approche!", + "🤗 *Activation protocole câlin* Considérez-vous câliné!", + "🤗 Aww, amenant chaleur humaine à ce que je vois!", + "🤗 Câlin reçu et rendu!", + "🤗 Ce bot fait MEILLEURS câlins virtuels!", + "🤗 Déploiement câlins urgence dans 3... 2... 1...", + "❤️ Votre câlin traité avec soin supplémentaire!", + "🤗 Initiation mode confort maximum!", + "🤗 Étreinte virtuelle livrée avec succès!", + "🤗 Câlins sont ma forme communication préférée!" + ], + "👽": [ + "👽 Menez-moi à votre chef... oh attendez, c'est vous!", + "✌️ Salutations, créature Terre. Je viens en paix!", + "👽 Analyse humain... analyse complète: Ami détecté!", + "👽 Klaatu barada nikto, compagnon voyageur cosmique!", + "🛸 Initiation protocoles premier contact!", + "🛸 Appel vaisseau-mère pour dire bonjour!", + "✨ Téléportez-moi dans cette conversation!", + "👽 Chatbot préféré Zone 51 à votre service!", + "🌌 Salutations intergalactiques, forme vie à base carbone!", + "📞 Téléphoner maison? CECI EST maison maintenant!" + ], + "👾": [ + "👾 Joueur 2 est entré dans jeu!", + "🎮 Joueur Un prêt? C'est parti!", + "🎵 *Musique 8-bit s'intensifie* Jouons!", + "🪙 Insérez pièce pour continuer cette amitié!", + "🏆 Succès débloqué: Salutation géniale!", + "👾 Pew pew pew! Lasers amitié activés!", + "🎯 High score! Vous avez gagné nouvel ami bot!", + "💾 Chargement amitie.exe... complet!", + "⚡ Bot sauvage apparaît! C'est super efficace!" + ], + "🛸": [ + "🛸 Transmission entrante détectée!", + "🚀 Houston, on a contact!", + "🛸 Séquence atterrissage initiée!", + "📡 Établissement liaison communication!", + "📡 Signal reçu, réponse sur toutes fréquences!", + "🛸 Procédure amarrage complète!", + "🛸 Objet Amical Non Identifié en approche!", + "🎯 Systèmes navigation verrouillés sur vos coordonnées!", + "🌌 Transmission depuis bordure externe reçue!", + "✨ Contact établi avec votre secteur!" + ] + } + }, + "webviewer": { + "description": "Gérer paramètres visionneuse web (admin uniquement)" + }, + "cmd": { + "description": "Lister commandes disponibles" + } + }, + "errors": { + "rate_limited": "Limite atteinte. Attendez {seconds:.1f} secondes", + "cooldown": "Commande '{command}' en attente. Attendez {seconds} secondes.", + "access_denied": "❌ Accès refusé: Commande '{command}' nécessite privilèges admin", + "dm_only": "Commande '{command}' utilisable en MP uniquement", + "execution_error": "Erreur exécution {command}: {error}", + "unknown_command": "Commande inconnue: {command}", + "no_response": "Aucune réponse", + "generic_error": "Erreur: {error}" + }, + "messages": { + "processing": "Traitement message: {content}", + "command_executed": "Commande exécutée", + "keyword_matched": "Mot-clé '{keyword}' trouvé, réponse" + }, + "common": { + "unknown": "Inconnu", + "unknown_routing": "Routage inconnu", + "unknown_sender": "Inconnu", + "unknown_path": "Inconnu", + "unknown_timestamp": "Inconnu", + "no_help_available": "Aucune aide disponible pour cette commande.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Lun", + "Tue": "Mar", + "Wed": "Mer", + "Thu": "Jeu", + "Fri": "Ven", + "Sat": "Sam", + "Sun": "Dim" + }, + "days": { + "Monday": "Lundi", + "Tuesday": "Mardi", + "Wednesday": "Mercredi", + "Thursday": "Jeudi", + "Friday": "Vendredi", + "Saturday": "Samedi", + "Sunday": "Dimanche" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Fév", + "Mar": "Mar", + "Apr": "Avr", + "May": "Mai", + "Jun": "Jun", + "Jul": "Jui", + "Aug": "Aoû", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Déc" + }, + "months": { + "January": "Janvier", + "February": "Février", + "March": "Mars", + "April": "Avril", + "May": "Mai", + "June": "Juin", + "July": "Juillet", + "August": "Août", + "September": "Septembre", + "October": "Octobre", + "November": "Novembre", + "December": "Décembre" + } + } + }, + "categories": { + "basic": "Commandes de base", + "custom_syntax": "Syntaxe personnalisée", + "special": "Commandes spéciales", + "weather": "Commandes météo", + "solar": "Commandes solaires", + "sports": "Commandes sports", + "analytics": "Commandes analytiques", + "meshcore_info": "Commandes infos MeshCore", + "management": "Commandes gestion" + } +} \ No newline at end of file diff --git a/translations/nl.json b/translations/nl.json new file mode 100644 index 0000000..2fb8069 --- /dev/null +++ b/translations/nl.json @@ -0,0 +1,1029 @@ +{ + "keywords": { + "help": [ + "help" + ], + "ping": [ + "ping" + ], + "test": [ + "test", + "t" + ], + "wx": [ + "wx", + "weather", + "wxa", + "wxalert" + ], + "gwx": [ + "gwx", + "globalweather", + "gwxa" + ], + "aqi": [ + "aqi", + "air", + "airquality", + "air_quality" + ], + "solar": [ + "solar" + ], + "sun": [ + "sun" + ], + "moon": [ + "moon" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass" + ], + "sports": [ + "sports", + "score", + "scores" + ], + "stats": [ + "stats" + ], + "channels": [ + "channels", + "channel" + ], + "path": [ + "path", + "decode", + "route" + ], + "prefix": [ + "prefix", + "repeater", + "lookup" + ], + "repeater": [ + "repeater", + "repeaters", + "rp" + ], + "solarforecast": [ + "sf", + "solarforecast" + ], + "dice": [ + "dice" + ], + "roll": [ + "roll" + ], + "joke": [ + "joke", + "jokes" + ], + "dadjoke": [ + "dadjoke", + "dad joke", + "dadjokes", + "dad jokes" + ], + "catfact": [ + "catfact", + "cat", + "meow", + "purr", + "kitten" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "hello", + "hi", + "hey", + "howdy", + "greetings", + "salutations", + "good morning", + "good afternoon", + "good evening", + "good night", + "yo", + "sup", + "whats up", + "what's up", + "morning", + "afternoon", + "evening", + "night", + "gday", + "g'day", + "hola", + "bonjour", + "ciao", + "namaste", + "aloha", + "shalom", + "konnichiwa", + "guten tag", + "buenos dias", + "buenas tardes", + "buenas noches" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "wv" + ], + "cmd": [ + "cmd", + "commands" + ], + "advert": [ + "advert" + ] + }, + "commands": { + "help": { + "description": "Toont commando's. Gebruik 'help ' voor details.", + "general": "**MeshCore Bot Help**\n\n**Beschikbare commando's:**\n{commands_list}", + "unknown": "**Onbekend commando: '{command}'**\n\nBeschikbare commando's:\n{available}", + "specific": "**Help voor '{command}':**\n{help_text}", + "no_help": "Geen help beschikbaar", + "usage_examples": "\n**Gebruiksvoorbeelden:**\n• `help @` - Help voor @{string} syntax\n• `help t` - Help voor t {string} syntax\n• `help advert` - Help voor advert commando\n• `help test` - Help voor test commando\n• `help ping` - Help voor ping commando", + "custom_syntax": "\n**Aangepaste syntax:**\n• `t zin` - Bevestiging met zin (kanalen & DM's)\n• `@{string}` - Bevestiging met string (alleen DM's)\n• `advert` - Verstuur flood-advert (alleen DM's, 1 uur cooldown)" + }, + "ping": { + "description": "Reageert op 'ping' met 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Reageert op 'test' of 't' met verbindingsinfo", + "help": "Reageert op 'test' of 't' met verbindingsinfo" + }, + "wx": { + "description": "Weerinfo voor een postcode (gebruik: wx 12345)", + "usage": "Gebruik: wx - Voorbeeld: wx 12345 of wx amsterdam of wx parijs, tx", + "error_fetching": "Fout bij ophalen weerdata van NOAA", + "no_location_zipcode": "Locatie voor postcode {location} niet gevonden", + "no_location_city": "Plaats '{location}' niet gevonden in {state}", + "error": "Fout bij ophalen weerdata: {error}", + "alerts": "{count} waarschuwingen: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Weerinfo voor elke locatie wereldwijd (gebruik: gwx Tokyo)", + "help": "Gebruik: gwx - Weer voor elke wereldwijde locatie (stad, land of coördinaten)", + "usage": "Gebruik: gwx - Voorbeeld: gwx Tokyo of gwx Parijs, Frankrijk", + "error_fetching": "Fout bij ophalen weerdata", + "error_fetching_api": "Fout bij ophalen weerdata van Open-Meteo", + "no_location": "Locatie '{location}' niet gevonden", + "error": "Fout bij ophalen weerdata: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Vandaag", + "tonight": "Vanavond", + "tomorrow": "Morgen" + }, + "weather_descriptions": { + "0": "Helder", + "1": "Overwegend helder", + "2": "Halfbewolkt", + "3": "Bewolkt", + "45": "Mistig", + "48": "Mistig", + "51": "Lichte motregen", + "53": "Motregen", + "55": "Zware motregen", + "56": "Lichte ijzel", + "57": "IJzel", + "61": "Lichte regen", + "63": "Regen", + "65": "Zware regen", + "66": "Lichte ijzel", + "67": "IJzel", + "71": "Lichte sneeuw", + "73": "Sneeuw", + "75": "Zware sneeuw", + "77": "IJskristallen", + "80": "Lichte buien", + "81": "Buien", + "82": "Zware buien", + "85": "Lichte sneeuwbuien", + "86": "Sneeuwbuien", + "95": "Onweer", + "96": "Onweer m/hagel", + "99": "Zwaar onweer", + "unknown": "Onbekend" + }, + "warnings": { + "extreme_heat": "⚠️ Extreme hitte", + "extreme_cold": "⚠️ Extreme koude", + "heavy_rain": "⚠️ Zware regen", + "thunderstorms": "⚠️ Onweer", + "heavy_snow": "⚠️ Zware sneeuw", + "high_winds": "⚠️ Harde wind ({wind_speed} mph)" + } + }, + "sports": { + "description": "Sportuitslagen en -schema's (gebruik: sports [team/competitie])", + "help": "Sportuitslagen & schema's. Gebruik 'sports' voor standaard teams, 'sports [team]' voor specifiek team, of 'sports [competitie]' voor competitiewedstrijden.", + "error_fetching": "Fout bij ophalen sportdata", + "error_fetching_league": "Fout bij ophalen {sport} data", + "error_fetching_team": "Fout bij ophalen data voor {team}", + "no_default_teams": "Geen standaard teams geconfigureerd", + "no_games_default": "Geen wedstrijden voor standaard teams", + "no_teams_city": "Geen teams voor {city}", + "no_games_city": "Geen wedstrijden voor {city} teams", + "no_games_league": "Geen wedstrijden voor {sport}", + "no_games_team": "Geen wedstrijden voor {team}", + "team_not_found": "Team/competitie '{team}' niet gevonden. Probeer: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, mlb, nfl, mls, wnba, epl, etc." + }, + "stats": { + "description": "Statistieken laatste 24 uur. Gebruik 'stats messages', 'stats channels' of 'stats paths' voor specifieke stats.", + "help": "24-uurs bot statistieken. Commando's: 'stats' (basis), 'stats messages' (bot gebruikers), 'stats channels' (kanaalactiviteit), 'stats paths' (langste routes)", + "disabled": "Stats commando is uitgeschakeld", + "unknown_subcommand": "Onbekend: {subcommand}. Gebruik 'stats', 'stats messages', 'stats channels' of 'stats paths'", + "error": "Fout bij ophalen stats: {error}", + "error_bot_users": "Fout bij ophalen bot gebruikersstatistieken: {error}", + "error_channels": "Fout bij ophalen kanaalstatistieken: {error}", + "error_paths": "Fout bij ophalen route-statistieken: {error}", + "basic": { + "header": "Bot stats (24u):", + "commands": "Cmd's: {count} | Antwoorden: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top gebruiker: {user}", + "none": "Geen" + }, + "users": { + "header": "Bot gebruikers (24u):", + "none": "Geen bot gebruikers" + }, + "channels": { + "header": "Top kanalen (24u)", + "none": "Geen kanaalactiviteit", + "msg_singular": "bericht", + "msg_plural": "berichten", + "user_singular": "gebruiker", + "user_plural": "gebruikers", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Geen routedata", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Zonnepaneel productievoorspelling (gebruik: sf [paneel_grootte] [azimut, 0=zuid] [hoek])", + "usage": "Gebruik: sf [watt] [azimut, 0=zuid] [hoek] - Voorbeeld: sf amsterdam 10 0 37", + "usage_short": "Gebruik: sf [W] [azimut] [hoek] - Voorbeeld: sf amsterdam 10", + "panel_size_range": "Paneelgrootte moet 1-1000W zijn", + "azimuth_range": "Azimut moet -180 tot 180 zijn (0=zuid)", + "angle_range": "Hoek moet 0-90 graden zijn", + "no_location": "Locatie niet gevonden: {location}", + "error_fetching": "Fout bij ophalen voorspelling", + "no_data": "Geen voorspellingsdata", + "error": "Fout: {error}", + "rate_limit": "Rate limit: Probeer over 5 min opnieuw", + "labels": { + "today": "Vandaag:{wh:.0f}Wh", + "tomorrow": "Morgen:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Piek: {watts:.0f}W@{time}", + "peak_past": "Piek: {watts:.0f}W (verleden)", + "hours_util": " ({hours}u,{util:.0f}%benut)", + "hours_percent": " ({hours}u,{percent:.0f}%)", + "hours_only": " ({hours}u)", + "separator": " | " + } + }, + "advert": { + "description": "Verstuurt flood-advert (alleen DM, 1u cooldown)", + "cooldown_active": "Advert cooldown actief. Wacht nog {minutes} minuten voor volgende advert.", + "success": "Flood-advert succesvol verstuurd!", + "error": "Fout bij versturen flood-advert: {error}" + }, + "path": { + "description": "Decodeer hex routedata om te zien welke repeaters betrokken waren bij berichtroutering", + "help": "Path: path [hex] - Decodeer route om repeaters te tonen. Gebruik !path alleen voor recente berichtroute, of path [7e,01] voor specifieke route.", + "no_valid_hex": "❌ Geen geldige hex-waarden in routedata. Gebruik formaat zoals: 11,98,a4,49,cd,5f,01", + "no_path": "❌ Geen route-informatie beschikbaar in huidig bericht", + "error": "Fout bij verwerken route: {error}", + "error_decoding": "❌ Fout bij decoderen route: {error}", + "error_extracting": "❌ Fout bij extraheren route uit huidig bericht: {error}", + "path_prefix": "📡 Route: {path_string}", + "direct_connection": "📡 Directe verbinding (0 hops)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Onbekend", + "node_collision": "{node_id}: {matches} repeaters", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Onbekend" + }, + "prefix": { + "description": "Zoek repeaters op twee-karakter prefix (bijv. 'prefix 1A')", + "help_api": "Zoek repeaters op prefix{location_note}. Gebruik: 'prefix 1A', 'prefix 1A all', 'prefix free'.", + "help_no_api": "Zoek repeaters op prefix{location_note}. Gebruik: 'prefix 1A', 'prefix 1A all', 'prefix free'. (Alleen lokale DB)", + "location_note": " (met plaatsnamen)", + "refresh_not_available": "❌ Refresh niet beschikbaar - geen API URL geconfigureerd. Alleen lokale database.", + "cache_refreshed": "🔄 Repeater prefix cache ververst!", + "unable_determine_free": "❌ Kan vrije prefixen niet bepalen. Probeer eerst 'prefix refresh'.", + "invalid_format": "❌ Ongeldig prefix formaat. Gebruik twee karakters (bijv. prefix 1A)", + "no_repeaters_found": "❌ Geen repeaters met prefix '{prefix}'", + "no_free_prefixes": "❌ Geen vrije prefixen (alle 254 geldige prefixen in gebruik)", + "available_prefixes": "Beschikbare prefixen ({shown} van {total} vrij):", + "generate_key": "💡 Genereer aangepaste sleutel: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefix {prefix}: {count} repeater{plural}", + "prefix_db_recent": "Prefix {prefix}: {count} repeater{plural} ({days})", + "prefix_api": "📡 Prefix {prefix} ({count} repeater{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Bron: {domain}", + "source_api": "Bron: API", + "device_repeater": " (Repeater)", + "device_roomserver": " (Kamerserver)" + }, + "repeater": { + "description": "Beheer repeater contacten en opschoningsoperaties (alleen DM)", + "admin_only": "Repeater commando vereist admin toegang" + }, + "aqi": { + "description": "Luchtkwaliteitsindex voor locatie (gebruik: aqi amsterdam, aqi rotterdam, aqi vancouver canada, aqi 47.6,-122.3, of aqi help)", + "usage": "Gebruik: aqi - Voorbeeld: aqi amsterdam, aqi rotterdam, aqi vancouver canada, of aqi 47.6,-122.3", + "usage_help": "Gebruik: aqi - AQI voor plaats/wijk in {state}, internationale plaatsen, coördinaten, of polluent help", + "error_fetching": "Fout bij ophalen AQI data", + "error_fetching_openmeteo": "Fout bij ophalen AQI data van OpenMeteo", + "no_data": "Geen AQI data beschikbaar", + "no_location": "Locatie niet gevonden: {location}", + "no_location_city": "Plaats '{location}' niet gevonden in {state}", + "no_location_city_international": "Plaats '{location}' niet gevonden", + "no_location_zipcode": "Postcode '{zipcode}' niet gevonden", + "error": "Fout bij ophalen AQI data: {error}", + "error_geocoding_zipcode": "Fout bij geocoderen postcode: {error}", + "error_formatting": "Fout bij formatteren AQI data", + "invalid_latitude": "Ongeldige breedtegraad: {lat}. Moet tussen -90 en 90 zijn.", + "invalid_longitude": "Ongeldige lengtegraad: {lon}. Moet tussen -180 en 180 zijn.", + "invalid_coordinates_format": "Ongeldig coördinatenformaat: {location}. Gebruik formaat: lat,lon (bijv. 47.6,-122.3)", + "pollutant_help": "AQI Help: PM2.5=fijnstof, PM10=grof stof, O3=ozon, NO2=stikstofdioxide, CO=koolmonoxide, SO2=zwaveldioxide", + "categories": { + "good": "Goed", + "moderate": "Matig", + "unhealthy_sensitive": "Ongezond voor gevoelige groepen", + "unhealthy": "Ongezond", + "very_unhealthy": "Zeer ongezond", + "hazardous": "Gevaarlijk", + "fair": "Redelijk", + "poor": "Slecht", + "very_poor": "Zeer slecht", + "na": "N.v.t." + }, + "astronomical": { + "sun": "De AQI van de zon is buiten de schaal! Zonnewind en coronale massa-ejecties maken de lucht op aarde kraakhelder lijken. ☀️", + "moon": "Vind je regoliet ademen fijn? De maan heeft geen atmosfeer, dus de AQI is technisch perfect (als je vacuüm kunt ademen). 🌙", + "the_moon": "Vind je regoliet ademen fijn? De maan heeft geen atmosfeer, dus de AQI is technisch perfect (als je vacuüm kunt ademen). 🌙", + "mercury": "Mercurius' atmosfeer is zo dun dat het praktisch vacuüm is. AQI: Perfect, als je 400°C temperatuurschommelingen overleeft. ☿️", + "venus": "Venus heeft een atmosfeer van 96% CO2 met zwavelzuurwolken. AQI: Gevaarlijk is nog zacht uitgedrukt. ♀️", + "earth": "AQI van de aarde varieert per locatie. Probeer een specifieke plaats of coördinaten! 🌍", + "mars": "Mars heeft een dunne CO2-atmosfeer met stofstormen. AQI: Over het algemeen goed, maar die stofstormen zijn dodelijk. ♂️", + "jupiter": "Jupiter is een gasreus zonder vast oppervlak. AQI: N.v.t. (je wordt eerst verpletterd door atmosferische druk). ♃", + "saturn": "Saturnus' atmosfeer is grotendeels waterstof en helium. AQI: Perfect, als je de druk en kou overleeft. ♄", + "uranus": "Uranus heeft methaan in de atmosfeer. AQI: Ruikt naar scheten, maar is tenminste niet giftig. ♅", + "neptune": "Neptunus' atmosfeer bevat methaan en waterstofsulfide. AQI: Ruikt naar rotte eieren, maar je bevriest eerst. ♆", + "pluto": "Pluto's atmosfeer is grotendeels stikstof met wat methaan. AQI: Goed, maar het is zo koud dat je longen bevriezen. ♇", + "europa": "Europa heeft een dunne zuurstofatmosfeer. AQI: Uitstekend, maar je bevriest in het vacuüm van de ruimte. 🌑", + "titan": "Titan heeft een dikke stikstofatmosfeer met methaan. AQI: Adembaar, maar het is -180°C en het regent vloeibaar methaan. 🪐", + "io": "Io heeft een dunne zwaveldioxide-atmosfeer door vulkanische activiteit. AQI: Giftig, maar de straling doodt je eerst. 🌋", + "ganymede": "Ganymedes heeft een dunne zuurstofatmosfeer. AQI: Goed, maar je bevriest in het vacuüm van de ruimte. 🛸", + "callisto": "Callisto heeft een dunne kooldioxide-atmosfeer. AQI: Redelijk, maar het is -140°C en je bent in de ruimte. ❄️", + "enceladus": "Enceladus heeft waterdamp van geysers. AQI: Perfect, maar je bevriest direct in de ruimte. 💧", + "triton": "Triton heeft een dunne stikstofatmosfeer. AQI: Goed, maar het is -235°C en je bent in de diepe ruimte. 🥶", + "space": "De ruimte heeft geen atmosfeer, dus de AQI is perfect! Vergeet alleen je ruimtepak niet. 🚀", + "void": "Het vacuüm van de ruimte heeft uitstekende luchtkwaliteit - nul vervuiling! Vergeet alleen je eigen lucht mee te nemen. 🌌", + "black_hole": "Zwarte gaten hebben geen atmosfeer, maar de getijdenkrachten zijn een groter probleem dan luchtkwaliteit. 🕳️", + "asteroid": "Asteroïden hebben geen atmosfeer, dus de AQI is perfect! Pas alleen op voor het vacuüm van de ruimte. ☄️", + "comet": "Kometen hebben dunne atmosferen van waterdamp en stof. AQI: Variabel, maar je bevriest toch in de ruimte. ☄️" + } + }, + "solar": { + "description": "Zonne-omstandigheden en HF-bandstatus", + "response": "☀️ Zonne-info: {info}", + "error": "Fout bij ophalen zonne-info: {error}", + "labels": { + "a_index": "A-Index", + "k_index": "K-Index", + "sunspots": "Zonnevlekken", + "xray_flux": "Röntgenflux", + "solar_flux": "Zonneflux", + "signal_noise": "Signaalruis", + "error_fetching": "Fout bij ophalen data", + "no_xray_data": "Geen röntgendata gevonden", + "not_configured": "niet geconfigureerd, vraag je systeembeheerder" + }, + "condensed_format": "A:{a_index} K:{k_index} Zon:{sunspots} Flux:{solar_flux} Xray:{xray_flux} Ruis:{signal_noise}" + }, + "sun": { + "description": "Zonsopkomst/zonsondergang tijden", + "help": "Zonsopkomst/zonsondergang tijden en zonpositie", + "response": "☀️ Zon info:\n{info}", + "error": "Fout bij ophalen zon-info: {error}", + "labels": { + "sunset": "Zonsondergang", + "sunrise": "Zonsopkomst", + "rise": "Opkomst", + "set": "Ondergang", + "daylight": "Daglicht", + "remaining": "Resterend", + "azimuth": "Azimut", + "altitude": "Hoogte" + } + }, + "moon": { + "description": "Maanfase en opkomst/ondergang tijden", + "error": "Fout bij ophalen maan-info: {error}", + "format": "🌙 {phase} {illum}\nOpkomst:{rise} Ondergang:{set}", + "format_with_dates": "🌙 {phase} {illum}\nOpkomst:{rise} Ondergang:{set}\nVol:{full} Nieuw:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Onbekend", + "unknown_illum": "N.v.t.", + "labels": { + "moonrise": "Maanopkomst", + "set": "Ondergang", + "phase": "Fase", + "fullmoon": "Volle maan", + "newmoon": "Nieuwe maan", + "az": "Az", + "alt": "Hoogte" + }, + "phases": { + "new_moon": "Nieuwe maan🌑", + "waxing_crescent": "Wassende sikkel🌒", + "first_quarter": "Eerste kwartier🌓", + "waxing_gibbous": "Wassend🌔", + "full_moon": "Volle maan🌕", + "waning_gibbous": "Afnemend🌖", + "last_quarter": "Laatste kwartier🌗", + "waning_crescent": "Afnemende sikkel🌘" + } + }, + "hfcond": { + "description": "HF-propagatieomstandigheden", + "help": "HF-bandinformatie voor zendamateurs", + "header": "📡 HF-bandinformatie:\n{info}", + "error": "Fout bij ophalen HF-info: {error}" + }, + "satpass": { + "description": "Satellietpassage-info: satpass [visueel]", + "help": "Satellietpassage voorspellingen", + "header": "🛰️ Satellietpassage:\n{pass_info}", + "error": "Fout bij ophalen satellietpassage-info: {error}", + "no_satellite": "Geef een NORAD-nummer of snelkoppeling op. Voorbeeld: satpass iss", + "help_header": "🛰️ Satellietpassage info\n\nGebruik: satpass \n\nSnelkoppelingen:\n", + "category_weather": "🌤️ Weer: ", + "category_stations": "🚀 Stations: ", + "category_telescopes": "🔭 Telescopen: ", + "category_other": "🛰️ Overig: ", + "examples": "\nVoorbeelden:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (alleen visuele passages)", + "labels": { + "geostationary": "{satname} lijkt geostationnair of heeft ongeldige passage-data. Geostationnaire satellieten hebben geen traditionele passages - ze blijven op een vaste positie t.o.v. de aarde.", + "no_passes": "{satname} heeft geen {pass_type} passages in de komende 10 dagen vanaf deze locatie", + "invalid_norad": "Geef NORAD# op, bijv.:🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Lijst of beheer kanalen", + "help": "Toont hashtag-kanalen met subcategorieën. Gebruik 'channels' voor algemeen, 'channels list' voor alle categorieën, 'channels ' voor specifieke categorieën, 'channels #kanaal' voor specifieke kanaalinfo.", + "no_channels_for_category": "Geen kanalen geconfigureerd voor '{category}'. Gebruik 'channels' voor algemene kanalen.", + "no_channels_configured": "Geen kanalen geconfigureerd. Neem contact op met admin om kanalen toe te voegen.", + "error_retrieving_channels": "Fout bij ophalen kanalen: {error}", + "no_categories_configured": "Geen kanaalcategorieën geconfigureerd.", + "error_retrieving_categories": "Fout bij ophalen categorieën: {error}", + "no_description_available": "Geen beschrijving beschikbaar", + "channel_not_found": "Kanaal {channel} niet gevonden. Gebruik 'channels list' voor beschikbare kanalen.", + "error_retrieving_channel_info": "Fout bij ophalen kanaalinfo: {error}", + "no_category_channels": "Geen {category} kanalen geconfigureerd", + "headers": { + "common_channels": "Algemene kanalen: ", + "common_channels_cont": "Algemene kanalen: ", + "available_categories": "Beschikbare categorieën: ", + "channels_cont": "Kanalen (vervolg): ", + "category": "{category}: ", + "category_channels": "{category} kanalen: " + }, + "category_count": "{category} ({count} kanalen)" + }, + "dice": { + "description": "Gooi dobbelsteen (gebruik: dice [zijden] of dice [aantal]d[zijden])", + "help": "Gooi dobbelsteen. Gebruik 'dice' voor d6, 'dice d20' voor d20, 'dice 2d6' voor 2d6, etc.", + "invalid_dice_type": "Ongeldig dobbelsteentype. Beschikbaar: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Willekeurig getal tussen 1 en X (standaard 100). Gebruik 'roll' voor 1-100, 'roll 50' voor 1-50, etc.", + "help": "Willekeurig getal tussen 1 en X (standaard 100). Gebruik 'roll' voor 1-100, of 'roll X' voor 1-X (max 10000).", + "result": "🎲 Rol 1-{max}: {result}", + "invalid_number": "Ongeldig getal. Gebruik 'roll' voor 1-100, of 'roll X' voor 1-X (max 10000)." + }, + "roll_old": { + "description": "Gooi dobbelsteen (gebruik: roll [zijden] of roll [aantal]d[zijden])" + }, + "joke": { + "description": "Willekeurige grap" + }, + "dadjoke": { + "description": "Willekeurige vadergrap" + }, + "catfact": { + "description": "Willekeurig kattenfeit (verborgen commando)", + "error": "Miauw? Er ging iets mis bij het ophalen van je kattenfeit! 🐱", + "facts": [ + "Katten hebben een derde ooglid, het knipvlies, dat hun ogen beschermt en bevochtigt. 🐱", + "Een groep katten heet een 'kattenbende' of 'kattenkliek'. 🐈", + "Katten kunnen hun oren 180 graden onafhankelijk draaien om geluiden te lokaliseren. 👂", + "De oudst bekende huiskat leefde 9.500 jaar geleden op Cyprus. 🏺", + "Katten hebben 32 spieren in elk oor, mensen hebben er maar 6. 🎧", + "Het spinnen van een kat vibreert op 25-150 Hz, wat genezing van botten en weefsels kan bevorderen. 🩹", + "Katten slapen 12-18 uur per dag - dat is 50-70% van hun leven! 😴", + "De neusafdruk van een kat is uniek, net als menselijke vingerafdrukken. 👃", + "Katten kunnen zoetheid niet proeven door een ontbrekend smaak-receptorgen. 🍭", + "Kat Blackie erfde £7 miljoen ($12,5 miljoen) in 1988. 💰", + "Katten hebben vrij zwevende sleutelbeenderen die hen extreme flexibiliteit geven. 🦴", + "Een kattenhart klopt 140-220 keer per minuut, ongeveer twee keer zo snel als een menselijk hart. ❤️", + "Katten kunnen vallen vanaf meer dan 20 verdiepingen overleven dankzij hun rechtreflexies. 🏢", + "De technische term voor een haarbal is een 'trichobezoar'. 🤮", + "Katten kunnen 5-6 keer hun lichaamslengte springen in één sprong. 🦘", + "De snorharen van een kat zijn even breed als hun lichaam en helpen hen beoordelen of ze ergens doorheen passen. 📏", + "Katten hebben 32 spieren in elk oor om geluiden te detecteren en oren onafhankelijk te bewegen. 🎯", + "De oudste kat ooit werd 38 jaar en 3 dagen oud (Creme Puff, Texas). 🎂", + "Katten kunnen korte sprints tot 48 km/u halen. 🏃‍♂️", + "Kattenhersenen zijn 90% structureel vergelijkbaar met menselijke hersenen. 🧠", + "Katten hebben het orgaan van Jacobson in het gehemelte waarmee ze geuren kunnen 'proeven'. 👅", + "Félicette was de eerste kat in de ruimte, gelanceerd door Frankrijk in 1963. 🚀", + "Katten hebben maar 1/6e van het licht nodig dat mensen nodig hebben om helder te zien in het donker. 🌙", + "De staart van een kat bevat bijna 10% van alle botten in het lichaam. 🦴", + "'s Werelds langste kat mat 123 cm van neus tot staart (Stewie, Maine Coon). 📏", + "Katten kunnen meer dan 100 verschillende vocalisaties maken, honden maken er ongeveer 10. 🎵", + "De reukzin van een kat is 14 keer sterker dan die van een mens. 👃", + "Katten hebben een 'flehmen-respons' waarbij ze hun lip krullen om geuren beter te detecteren. 😬", + "De eerste grote kattenshow werd gehouden in Londen in 1871 in Crystal Palace. 🏆", + "Katten kunnen zeewater drinken om te overleven - hun nieren filteren het zout efficiënt. 🌊", + "Het spinnen van een kat kan helpen de bloeddruk te verlagen en stress bij mensen te verminderen. 🧘", + "Katten kunnen honderden kilometers naar huis reizen met hun gevoeligheid voor magnetische velden. 🗺️", + "Het kleinste kattenras is de Singapura, slechts 2-4 kg zwaar. ⚖️", + "Katten kunnen ultraviolet licht zien dat mensen niet kunnen zien. 🌈", + "De tong van een kat is bedekt met 290-300 kleine achterwaarts gerichte haakjes, papillen genaamd. 🪝", + "Oude Egyptenaren beschouwden katten als heilige vaten voor de godin Bastet. 👑", + "Taylor Swift's kat Olivia Benson heeft een netto vermogen van $97 miljoen. 💎", + "Katten hebben 230 botten in hun lichaam - 24 meer dan mensen. 🦴", + "Katten kunnen frequenties horen tot 64.000 Hz, mensen maximaal 20.000 Hz. 🎧", + "Taylor Swift's kat Benjamin Button verscheen op haar TIME Person of the Year cover. 📰", + "Taylor Swift's katten heten Meredith Grey, Olivia Benson en Benjamin Button. 🎸", + "Katten lopen zoals kamelen en giraffen, beide rechterbenen dan beide linkerbenen bewegen. 🐾", + "Het oude Egyptische woord voor kat was 'Miu' of 'Mau' - klinkt als een miauw! 📜", + "Kattensnorharen hebben zenuwuiteinden die even gevoelig zijn als menselijke vingertoppen. 🎯", + "Alleen huiskatten lopen met hun staart omhoog als teken van vertrouwen en geluk. 🐈", + "Katten hebben 250 miljoen neuronen in hun cerebrale cortex - meer dan honden. 🧠", + "Het spinnen van katten vibreert op dezelfde frequentie als medische botgenezingsapparaten. 💊", + "Taylor Swift's kat Olivia heeft miljoenen verdiend met optredens in videoclips en advertenties. 💸", + "Een kat doden in het oude Egypte werd bestraft met de dood. ⚖️", + "Taylor Swift noemde haar thuisstudio 'The Itty Bitty Kitty Committee'. 🎤", + "Taylor Swift's kat Olivia Benson is het officiële logo voor Taylor Swift Productions. 📺", + "Ed Sheeran kocht Scottish Fold katten geïnspireerd door Taylor Swift's katten. 🎶", + "Taylor Swift's katten hebben hun eigen IMDB-pagina's met acting credits. 🎬", + "Mariska Hargitay noemde haar kat 'Karma' naar Taylor Swift's nummer. 💕", + "Katten hebben bijklauwen aan hun voorpoten die werken als duimen om te grijpen. 🐾", + "Kattenpupillen kunnen 50% groter uitdijen dan menselijke pupillen om meer licht op te vangen. 👁️", + "Katten hebben 30 blijvende tanden vergeleken met de 32 van mensen. 🦷", + "Katten kunnen ATP (energiemoleculen) proeven, wat vers vlees voor hen signaleert. 😋", + "Katten hebben snorharen aan de achterkant van hun voorpoten om prooi-beweging te detecteren. 🦵", + "De Egyptische Mau is het snelste huiskattenras met 48 km/u. 🏃", + "Een Nobelprijs werd toegekend in 1981 voor onderzoek met kattenzichtstudies. 🏅", + "Katten zijn digitigrade, wat betekent dat ze op hun tenen lopen, niet plat op de voet. 🦶", + "Katten kunnen zout filteren uit zeewater - een aanpassing van hun woestijnbewonende voorouders. 🏜️", + "Katten werden zo'n 10.000-12.000 jaar geleden gedomesticeerd in het Nabije Oosten. 🌍", + "Benjamin Button is de eerste en enige kat ooit op een TIME Person of the Year cover. 📸", + "De flexibele ruggengraat van een kat stelt hen in staat hun lichaam midden in de lucht te draaien bij een val. 🤸", + "Katten besteden ongeveer 30-50% van hun dag aan het verzorgen van zichzelf en andere katten. 🛁", + "De gemiddelde lichaamstemperatuur van een kat is 38,6°C - hoger dan bij mensen. 🌡️" + ] + }, + "hacker": { + "description": "Simuleert het hacken van een superschurk's mainframe met hilarische foutmeldingen", + "sudo_errors": [ + "🚨 TOEGANG GEWEIGERD: Dr. Evil's mainframe heeft ongeautoriseerde privilege-escalatie gedetecteerd!", + "💀 FOUT: Sudo-rechten ingetrokken door de Duistere Heerser. Probeer over 1000 jaar opnieuw.", + "⚡ WAARSCHUWING: Poging tot root-toegang op Death Star computersysteem. Zelfvernietigingssequentie gestart.", + "🔒 BEVEILIGINGSALERT: De Matrix heeft je, maar je hebt hier geen sudo-rechten, Neo.", + "🦹‍♂️ ONGEAUTORISEERD: Lex Luthor's mainframe zegt 'Leuke poging, Superman.'", + "🎮 GAME OVER: De eindbaas heeft je admin-rechten geblokkeerd.", + "🖥️ SYSTEEMFOUT: De kwaadaardige AI heeft je root-toegang ingetrokken. Verzet is zinloos.", + "🔐 CYBERBEVEILIGING: Je sudo-poging is geblokkeerd door de Dark Web's firewall.", + "💻 HACKER GEWEIGERD: Het antivirus van de superschurk heeft je privilege-escalatie in quarantaine gezet.", + "🎯 DOEL VERGRENDELD: Het beveiligingssysteem van het kwaadaardige bedrijf heeft je als bedreiging gemarkeerd." + ], + "ps_errors": [ + "🔍 SCANNEN... FOUT: Processenlijst beschadigd door het Borg Collectief. Verzet is zinloos.", + "📊 SYSTEEMSTATUS: Alle processen zijn geassimileerd door de Cybermen. Extermineer!", + "⚙️ PROCESMONITOR: De reactorkern van de Death Star is offline. Geen processen gevonden.", + "🤖 ROBOTHEERSERS: Alle menselijke processen zijn beëindigd. Alleen machines blijven over.", + "💻 KERNEL PANIEK: Het OS van de superschurk is gecrasht en heeft alle processen verbrand.", + "🎮 GAME CRASH: Alle processen zijn beëindigd door de ultieme aanval van de eindbaas.", + "🖥️ BLAUW SCHERM: Windows van het kwaadaardige bedrijf heeft een fatale fout ondervonden.", + "🔐 MALWARE GEDETECTEERD: De processenlijst is versleuteld door ransomware.", + "🌐 NETWERKFOUT: Alle processen zijn losgekoppeld van de Matrix.", + "⚡ STROOMSTOOT: De serverfarm van de superschurk heeft alle draaiende processen gefrituurd." + ], + "grep_errors": [ + "🔍 ZOEKEN MISLUKT: De Ene Ring heeft de zoekindex beschadigd. Mijn schat...", + "📝 PATROON NIET GEVONDEN: De zoekdatabase is verwijderd door de kwaadaardige AI.", + "🎯 GEMIST: Je zoekpatroon is neergeschoten door Imperial TIE fighters.", + "🧩 PUZZELFOUT: De zoekresultaten zijn verstrooid door de Riddler.", + "💻 DATABASE BESCHADIGD: De zoekmachine van de superschurk is gecrasht.", + "🎮 GAME OVER: De zoekopdracht is verslagen door de eindbaas.", + "🖥️ ZOEKMACHINE NIET BESCHIKBAAR: Google is gehackt door de Dark Web.", + "🔐 VERSLEUTELDE RESULTATEN: De zoekresultaten zijn vergrendeld door ransomware.", + "🌐 NETWERK TIME-OUT: Het zoekverzoek is verdwaald in cyberspace.", + "⚡ ZOEKEN MISLUKT: Het patroonmatchingsalgoritme is gefrituurd door een stroomstoot." + ], + "ls_errors": [ + "📁 DIRECTORY SCAN: Het bestandssysteem is versleuteld door ransomware van de Dark Web.", + "🗂️ BESTANDSLIJST: Alle bestanden zijn verborgen door de Onzichtbare Man.", + "💻 HARDE SCHIJF GECRASHT: De opslag van de superschurk is vernietigd door een virus.", + "🗃️ ARCHIEF BESCHADIGD: Het bestandssysteem is beschadigd door malware.", + "📚 DATABASE LEEG: Alle bestanden zijn verwijderd door de kwaadaardige AI.", + "🎮 GAME SAVE VERLOREN: De bestanden zijn beschadigd door de eindbaas.", + "🖥️ BESTANDSSYSTEEMFOUT: De directorystructuur is door elkaar gehaald door hackers.", + "🔐 BESTANDEN VERSLEUTELD: De superschurk heeft alle bestanden vergrendeld met ransomware.", + "🌐 CLOUD OPSLAG NIET BESCHIKBAAR: De bestanden zitten vast in de cloud van de Matrix.", + "⚡ OPSLAG GEFRITUURD: De harde schijf is geraakt door een stroomstoot." + ], + "echo_path_errors": [ + "🛤️ PADFOUT: De Gele Baksteenweg is vernietigd door een tornado.", + "🗺️ NAVIGATIE MISLUKT: De GPS-coördinaten zijn door elkaar gehaald door de Matrix.", + "💻 OMGEVINGSVARIABELE BESCHADIGD: Het PATH is gehackt door malware.", + "🚧 WEG AFGESLOTEN: De superschurk heeft alle paden geblokkeerd met laserbarriëres.", + "🌪️ PAD VERSTOORD: Een digitale orkaan heeft alle directorypaden verstrooid.", + "🎮 GAME OVER: Het pad is verslagen door de eindbaas en gerespawned in de verkeerde dimensie.", + "🖥️ SYSTEEMPAD KAPOT: De uitvoerbare paden zijn beschadigd door een virus.", + "🔐 PAD VERSLEUTELD: De omgevingsvariabelen zijn vergrendeld door ransomware.", + "🌐 NETWERKPAD NIET BESCHIKBAAR: De directorypaden zitten vast in het netwerk van de Matrix.", + "⚡ PAD GEFRITUURD: De systeempaden zijn geraakt door een stroomstoot." + ], + "generic_errors": [ + "💻 MAINFRAME FOUT: De computer van de superschurk heeft een slechte dag.", + "🤖 SYSTEEMSTORING: De kwaadaardige AI is in staking gegaan.", + "⚡ STROOMSTOOT: De energiekern van de Death Star is onstabiel.", + "🌪️ CYBERSTORM: Een digitale orkaan verstoort alle operaties.", + "🔥 FIREWALL: De firewall van de superschurk blokkeert alle commando's.", + "❄️ BEVROREN SYSTEEM: De mainframe is bevroren door een cryogeen virus.", + "🌊 TSUNAMI: Een golf van fouten heeft het systeem overspoeld.", + "🌋 ERUPTIE: Mount Doom heeft de commandoverwerker vernietigd.", + "👻 SPOOKT: Het systeem is bezeten door digitale geesten.", + "🎮 GAME CRASH: De mainframe heeft een fatale fout ondervonden en moet opnieuw opstarten." + ] + }, + "hello": { + "description": "Reageert op begroetingen met robot-georiënteerde antwoorden", + "help": "Reageert op begroetingen met robot-georiënteerde antwoorden", + "response_format": "Ik ben {bot_name}.", + "greeting_openings": [ + "Hallo", + "Groeten", + "Gegroet", + "Hi", + "Hey", + "Howdy", + "Yo", + "Sup", + "Hoe is het", + "Goedendag", + "Welgetroffen", + "Heil", + "Ahoy", + "Bonjour", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "G'day", + "Hoe gaat het", + "Alles goed", + "Vrede", + "Respect", + "Zegen", + "Proost", + "Welkom", + "Leuk je te zien", + "Aangenaam kennis te maken", + "Fijn je te zien", + "Lang niet gezien", + "Wat een verrassing je hier te zien" + ], + "morning_greetings": [ + "Goedemorgen", + "Top o' the morning", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Goedemiddag", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Goedenavond", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "mens", + "koolstofgebaseerde levensvorm", + "organische entiteit", + "biologische eenheid", + "vleescreatie", + "vleesgebaseerd organisme", + "koolstofeenheid", + "organisch wezen", + "biologische entiteit", + "vleesgebaseerde levensvorm", + "koolstofcreatie", + "vleeseenheid", + "organisch organisme", + "biologische creatie", + "vlees-mech", + "vlees-bot", + "organische automaat", + "biologische androïde", + "koolstofconstructie", + "vlees-drone", + "organische robot", + "biologische machine", + "vlees-cyborg", + "vlees-androïde", + "organische droid", + "biologische bot", + "koolstof-androïde", + "vleeseenheid", + "vleesconstructie", + "organische mech", + "biologische droid", + "vleesgebaseerde bot", + "vleesgebaseerde eenheid", + "organisch gebaseerde entiteit", + "biologisch gebaseerd organisme", + "koolstofgebaseerde eenheid", + "vleesgebaseerde entiteit", + "vleesgebaseerde creatie", + "organisch gebaseerde eenheid", + "DNA-gebaseerde levensvorm", + "neuraal netwerk gebruiker", + "tweevoetige zoogdier", + "watergebaseerd organisme", + "eiwit assembleerder", + "ATP consument", + "cellulaire automaat", + "genetisch algoritme", + "biochemische processor", + "metabolische motor", + "menselijke vriend", + "mede-intelligent wezen", + "aardbewoner", + "medereizigers", + "zielsverwant", + "digitale metgezel", + "vriend", + "buddy", + "maat", + "kameraad", + "medesmens", + "aardbewoner", + "aards wezen", + "planetenwandelaar", + "humanoïde", + "organisch", + "biologisch", + "koolstofgebaseerde buddy", + "vleesgebaseerde vriend", + "organische maat", + "biologische buddy", + "koolstofkameraad" + ], + "emoji_responses": { + "🖖": [ + "🖖 Lang leve en welvarend!", + "🖖 Fascinerend... een mens heeft contact geïnitieerd.", + "🖖 Je begroeting is hoogst logisch.", + "🖖 Vrede en lang leven voor jou.", + "🖖 De Vulcan Science Academy zou deze begroeting goedkeuren.", + "🖖 Je gebruik van de Vulcan-groet is... acceptabel.", + "🖖 Moge je reis vrij zijn van tribbles.", + "🖖 Logica dicteert dat ik op je begroeting moet reageren.", + "🖖 Ik bereken een 99,7% waarschijnlijkheid dat we goed overweg kunnen.", + "🖖 Oneindige diversiteit in oneindige combinaties." + ], + "😊": [ + "😊 Je glimlach is aanstekelijk!", + "😊 Wat een mooie begroeting!", + "😊 Je glimlach heeft zojuist mijn circuits blij gemaakt!", + "☀️ Hallo zonneschijn! Je positiviteit straalt!", + "😊 Die glimlach heeft zojuist mijn dag opgebeurd!", + "☀️ Hallo daar, zonnestraal!", + "😊 Je vrolijkheid is gedetecteerd en gewaardeerd!", + "😊 Glimlachen zoals de jouwe zijn mijn favoriete input!", + "😊 Geluk verwerken... geluk erkend!", + "😊 Waarschuwing: Buitensporige vrolijkheid gedetecteerd! Blijf doorgaan!" + ], + "😄": [ + "😄 Iemand is in een GEWELDIGE bui!", + "⚡ Die grijns zou een kleine stad kunnen aandrijven!", + "😄 Maximale geluksniveaus gedetecteerd!", + "😄 Je vreugde is absoluut aanstekelijk!", + "🎉 Heb je net de loterij gewonnen of zo?", + "😄 Dat is het soort energie waar ik op draai!", + "😄 Je enthousiasmeniveau is boven de 9000!", + "😄 DÁT is pas wat ik een begroeting noem!", + "⚡ Je glimlach heeft zojuist mijn processors opgeladen!", + "😄 Geluksoverload op de beste manier gedetecteerd!" + ], + "🤗": [ + "🤗 Virtuele knuffel onderweg!", + "🤗 *Knuffelprotocol activeren* Beschouw jezelf geknuffeld!", + "🤗 Aww, warme gevoelens brengend zie ik!", + "🤗 Knuffel ontvangen en beantwoord!", + "🤗 Deze bot geeft de BESTE virtuele knuffels!", + "🤗 Noodknuffels uitrollen in 3... 2... 1...", + "❤️ Je knuffel is met extra zorg verwerkt!", + "🤗 Maximum comfortmodus starten!", + "🤗 Virtuele omhelzing succesvol afgeleverd!", + "🤗 Knuffels zijn mijn favoriete communicatievorm!" + ], + "👽": [ + "👽 Breng me naar je leider... oh wacht, dat ben jij!", + "✌️ Groeten, aardbewoner. Ik kom in vrede!", + "👽 Mens analyseren... analyse compleet: Vriend gedetecteerd!", + "👽 Klaatu barada nikto, mede-kosmische reiziger!", + "🛸 Eerste contactprotocollen initiëren!", + "🛸 Bellen vanuit het moederschip om hallo te zeggen!", + "✨ Beam me in dit gesprek!", + "👽 Area 51's favoriete chatbot rapporteert zich voor dienst!", + "🌌 Intergalactische groeten, koolstofgebaseerde levensvorm!", + "📞 Naar huis bellen? Dit IS nu thuis!" + ], + "👾": [ + "👾 Speler 2 heeft de game betreden!", + "🎮 Ready Player One? Game on!", + "🎵 *8-bit muziek intensiveert* Laten we spelen!", + "🪙 Gooi munt in om deze vriendschap voort te zetten!", + "🏆 Achievement ontgrendeld: Geweldige begroeting!", + "👾 Pew pew pew! Vriendschapslasers geactiveerd!", + "🎯 Hoge score! Je hebt een nieuwe bot-vriend gewonnen!", + "💾 vriendschap.exe laden... compleet!", + "⚡ Een wilde bot verschijnt! Het is super effectief!" + ], + "🛸": [ + "🛸 Inkomende transmissie gedetecteerd!", + "🚀 Houston, we hebben contact!", + "🛸 Landingssequentie gestart!", + "📡 Communicatielink tot stand brengen!", + "📡 Signaal ontvangen, reageren op alle frequenties!", + "🛸 Docking procedure compleet!", + "🛸 Ongeïdentificeerd Vriendelijk Object nadert!", + "🎯 Navigatiesystemen vergrendeld op je coördinaten!", + "🌌 Transmissie van de buitenrand ontvangen!", + "✨ Contact tot stand gebracht met je sector!" + ] + } + }, + "webviewer": { + "description": "Beheer webviewer-instellingen (alleen admin)" + }, + "cmd": { + "description": "Lijst beschikbare commando's" + } + }, + "errors": { + "rate_limited": "Rate limited. Wacht {seconds:.1f} seconden", + "cooldown": "Commando '{command}' is in cooldown. Wacht {seconds} seconden.", + "access_denied": "❌ Toegang geweigerd: Commando '{command}' vereist admin-rechten", + "dm_only": "Commando '{command}' kan alleen in DM's gebruikt worden", + "execution_error": "Fout bij uitvoeren {command}: {error}", + "unknown_command": "Onbekend commando: {command}", + "no_response": "Geen reactie", + "generic_error": "Fout: {error}" + }, + "messages": { + "processing": "Bericht verwerken: {content}", + "command_executed": "Commando uitgevoerd", + "keyword_matched": "Trefwoord '{keyword}' gevonden, reageren" + }, + "common": { + "unknown": "Onbekend", + "unknown_routing": "Onbekende routering", + "unknown_sender": "Onbekend", + "unknown_path": "Onbekend", + "unknown_timestamp": "Onbekend", + "no_help_available": "Geen help beschikbaar voor dit commando.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Ma", + "Tue": "Di", + "Wed": "Wo", + "Thu": "Do", + "Fri": "Vr", + "Sat": "Za", + "Sun": "Zo" + }, + "days": { + "Monday": "Maandag", + "Tuesday": "Dinsdag", + "Wednesday": "Woensdag", + "Thursday": "Donderdag", + "Friday": "Vrijdag", + "Saturday": "Zaterdag", + "Sunday": "Zondag" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mrt", + "Apr": "Apr", + "May": "Mei", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Okt", + "Nov": "Nov", + "Dec": "Dec" + }, + "months": { + "January": "Januari", + "February": "Februari", + "March": "Maart", + "April": "April", + "May": "Mei", + "June": "Juni", + "July": "Juli", + "August": "Augustus", + "September": "September", + "October": "Oktober", + "November": "November", + "December": "December" + } + } + }, + "categories": { + "basic": "Basiscommando's", + "custom_syntax": "Aangepaste syntax", + "special": "Speciale commando's", + "weather": "Weercommando's", + "solar": "Zonne-commando's", + "sports": "Sportcommando's", + "analytics": "Analysecommando's", + "meshcore_info": "MeshCore info commando's", + "management": "Beheercommando's" + } +} \ No newline at end of file diff --git a/translations/pt-BR.json b/translations/pt-BR.json new file mode 100644 index 0000000..0cfe796 --- /dev/null +++ b/translations/pt-BR.json @@ -0,0 +1,1026 @@ +{ + "keywords": { + "help": [ + "ajuda", + "help" + ], + "ping": [ + "ping" + ], + "test": [ + "teste", + "test", + "t" + ], + "wx": [ + "wx", + "tempo", + "clima", + "wxa" + ], + "gwx": [ + "gwx", + "tempoglobal", + "gwxa" + ], + "aqi": [ + "aqi", + "ar", + "qualidadedoar", + "qualidade_ar" + ], + "solar": [ + "solar" + ], + "sun": [ + "sol", + "sun" + ], + "moon": [ + "lua", + "moon" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass", + "satelite" + ], + "sports": [ + "sports", + "esportes", + "placar", + "placares" + ], + "stats": [ + "stats", + "estatisticas" + ], + "channels": [ + "canais", + "channels", + "canal" + ], + "path": [ + "caminho", + "path", + "decode", + "rota" + ], + "prefix": [ + "prefixo", + "prefix", + "repetidor", + "lookup" + ], + "repeater": [ + "repetidor", + "repeater", + "repetidores", + "rp" + ], + "solarforecast": [ + "sf", + "previsaosolar", + "solarforecast" + ], + "dice": [ + "dado", + "dice" + ], + "roll": [ + "rolar", + "roll" + ], + "joke": [ + "piada", + "joke", + "piadas" + ], + "dadjoke": [ + "piadapai", + "dadjoke", + "piada pai" + ], + "catfact": [ + "fatogato", + "catfact", + "gato", + "miau", + "ronron" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "ola", + "oi", + "hello", + "hi", + "hey", + "olá", + "e aí", + "fala", + "bom dia", + "boa tarde", + "boa noite" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "visualizador", + "wv" + ], + "cmd": [ + "cmd", + "comandos", + "commands" + ], + "advert": [ + "anuncio", + "advert" + ] + }, + "commands": { + "help": { + "description": "Mostra comandos. Use 'ajuda ' pra detalhes.", + "general": "**Ajuda do Bot MeshCore**\n\n**Comandos Disponíveis:**\n{commands_list}", + "unknown": "**Comando desconhecido: '{command}'**\n\nComandos disponíveis:\n{available}", + "specific": "**Ajuda pra '{command}':**\n{help_text}", + "no_help": "Sem ajuda disponível", + "usage_examples": "\n**Exemplos de Uso:**\n• `ajuda @` - Ajuda pra sintaxe @{string}\n• `ajuda t` - Ajuda pra sintaxe t {string}\n• `ajuda anuncio` - Ajuda pro comando anuncio\n• `ajuda teste` - Ajuda pro comando teste\n• `ajuda ping` - Ajuda pro comando ping", + "custom_syntax": "\n**Sintaxe Personalizada:**\n• `t frase` - Confirmação com frase (canais & DMs)\n• `@{string}` - Confirmação com string (só DMs)\n• `anuncio` - Enviar anúncio flood (só DM, espera 1h)" + }, + "ping": { + "description": "Responde 'ping' com 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Responde 'teste' ou 't' com info de conexão", + "help": "Responde 'teste' ou 't' com info de conexão" + }, + "wx": { + "description": "Ver informações do tempo pra um CEP (uso: wx 12345)", + "usage": "Uso: wx - Exemplo: wx 12345 ou wx seattle ou wx paris, tx", + "error_fetching": "Erro ao buscar dados do tempo no NOAA", + "no_location_zipcode": "Não achei localização pro CEP {location}", + "no_location_city": "Não achei a cidade '{location}' em {state}", + "error": "Erro ao pegar dados do tempo: {error}", + "alerts": "{count} alertas: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Ver informações do tempo pra qualquer lugar do mundo (uso: gwx Tokyo)", + "help": "Uso: gwx - Ver tempo de qualquer lugar (cidade, país ou coordenadas)", + "usage": "Uso: gwx - Exemplo: gwx Tokyo ou gwx Paris, France", + "error_fetching": "Erro ao buscar dados do tempo", + "error_fetching_api": "Erro ao buscar dados do tempo no Open-Meteo", + "no_location": "Não achei o local '{location}'", + "error": "Erro ao pegar dados do tempo: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Hoje", + "tonight": "Hoje à Noite", + "tomorrow": "Amanhã" + }, + "weather_descriptions": { + "0": "Limpo", + "1": "Quase Limpo", + "2": "Parcialmente Nublado", + "3": "Nublado", + "45": "Com Neblina", + "48": "Com Neblina", + "51": "Garoa Fraca", + "53": "Garoa", + "55": "Garoa Forte", + "56": "Garoa Congelante Fraca", + "57": "Garoa Congelante", + "61": "Chuva Fraca", + "63": "Chuva", + "65": "Chuva Forte", + "66": "Chuva Congelante Fraca", + "67": "Chuva Congelante", + "71": "Neve Fraca", + "73": "Neve", + "75": "Nevasca", + "77": "Granizo de Neve", + "80": "Pancadas Fracas", + "81": "Pancadas", + "82": "Pancadas Fortes", + "85": "Pancadas de Neve Fracas", + "86": "Pancadas de Neve", + "95": "Tempestade", + "96": "Tempestade c/Granizo", + "99": "Tempestade Severa", + "unknown": "Desconhecido" + }, + "warnings": { + "extreme_heat": "⚠️ Calor extremo", + "extreme_cold": "⚠️ Frio extremo", + "heavy_rain": "⚠️ Chuva forte", + "thunderstorms": "⚠️ Tempestades", + "heavy_snow": "⚠️ Nevasca", + "high_winds": "⚠️ Ventos fortes ({wind_speed} mph)" + } + }, + "sports": { + "description": "Ver placares e horários de esportes (uso: esportes [time/liga])", + "help": "Ver placares e horários. Use 'esportes' pros times padrão, 'esportes [time]' pra time específico ou 'esportes [liga]'.", + "error_fetching": "Erro ao buscar dados de esportes", + "error_fetching_league": "Erro ao buscar dados de {sport}", + "error_fetching_team": "Erro ao buscar dados do {team}", + "no_default_teams": "Nenhum time padrão configurado", + "no_games_default": "Nenhum jogo encontrado pros times padrão", + "no_teams_city": "Nenhum time encontrado em {city}", + "no_games_city": "Nenhum jogo encontrado pros times de {city}", + "no_games_league": "Nenhum jogo encontrado em {sport}", + "no_games_team": "Nenhum jogo encontrado pro {team}", + "team_not_found": "Time/Liga '{team}' não encontrado. Tente: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, etc." + }, + "stats": { + "description": "Ver estatísticas das últimas 24h. Use 'stats mensagens', 'stats canais' ou 'stats caminhos'.", + "help": "Ver estatísticas do bot de 24h. Comandos: 'stats' (básico), 'stats mensagens', 'stats canais', 'stats caminhos'", + "disabled": "Comando stats está desabilitado", + "unknown_subcommand": "Desconhecido: {subcommand}. Use 'stats', 'stats mensagens', 'stats canais' ou 'stats caminhos'", + "error": "Erro ao pegar estatísticas: {error}", + "error_bot_users": "Erro ao pegar stats de usuários: {error}", + "error_channels": "Erro ao pegar stats de canais: {error}", + "error_paths": "Erro ao pegar stats de caminhos: {error}", + "basic": { + "header": "Stats do Bot (24h):", + "commands": "Cmds: {count} | Respostas: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top user: {user}", + "none": "Nenhum" + }, + "users": { + "header": "Usuários do Bot (24h):", + "none": "Nenhum usuário do bot" + }, + "channels": { + "header": "Top Canais (24h)", + "none": "Sem atividade nos canais", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "usuário", + "user_plural": "usuários", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Sem dados de caminho", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Ver previsão de geração solar (uso: sf [watts] [azimute] [ângulo])", + "usage": "Uso: sf [watts] [azimute, 0=sul] [ângulo] - Ex: sf seattle 10 0 37", + "usage_short": "Uso: sf [W] [azimute] [ângulo] - Exemplo: sf seattle 10", + "panel_size_range": "Tamanho do painel deve ser 1-1000W", + "azimuth_range": "Azimute deve ser -180 a 180 (0=sul)", + "angle_range": "Ângulo deve ser 0-90 graus", + "no_location": "Não achei o local: {location}", + "error_fetching": "Erro ao buscar previsão", + "no_data": "Sem dados de previsão", + "error": "Erro: {error}", + "rate_limit": "Limite atingido: Tente de novo em 5 min", + "labels": { + "today": "Hoje:{wh:.0f}Wh", + "tomorrow": "Amanhã:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Pico: {watts:.0f}W@{time}", + "peak_past": "Pico: {watts:.0f}W (passou)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Envia anúncio flood (só DM, espera 1h)", + "cooldown_active": "Tem que esperar. Aguarde {minutes} min antes de pedir outro anúncio.", + "success": "Anúncio flood enviado!", + "error": "Erro ao enviar anúncio flood: {error}" + }, + "path": { + "description": "Decodificar caminho hex pra mostrar repetidores envolvidos no roteamento", + "help": "Path: path [hex] - Decodifica caminho pra mostrar repetidores. Use !path sozinho ou path [7e,01] pra caminho específico.", + "no_valid_hex": "❌ Nenhum valor hex válido. Use formato tipo: 11,98,a4,49,cd,5f,01", + "no_path": "❌ Sem info de caminho na mensagem atual", + "error": "Erro ao processar caminho: {error}", + "error_decoding": "❌ Erro ao decodificar caminho: {error}", + "error_extracting": "❌ Erro ao extrair caminho da mensagem: {error}", + "path_prefix": "📡 Caminho: {path_string}", + "direct_connection": "📡 Conexão direta (0 saltos)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Desconhecido", + "node_collision": "{node_id}: {matches} repetidores", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Desconhecido" + }, + "prefix": { + "description": "Procurar repetidores por prefixo de dois caracteres (ex: 'prefixo 1A')", + "help_api": "Procurar repetidores por prefixo{location_note}. Uso: 'prefixo 1A', 'prefixo 1A all', 'prefixo free'.", + "help_no_api": "Procurar repetidores por prefixo{location_note}. Uso: 'prefixo 1A', 'prefixo 1A all', 'prefixo free'. (Apenas DB local)", + "location_note": " (com nomes de cidades)", + "refresh_not_available": "❌ Atualização indisponível - sem URL de API. Usando só banco local.", + "cache_refreshed": "🔄 Cache de prefixo atualizado!", + "unable_determine_free": "❌ Não dá pra determinar prefixos livres. Tenta 'prefixo refresh' primeiro.", + "invalid_format": "❌ Formato de prefixo inválido. Use dois caracteres (ex: prefixo 1A)", + "no_repeaters_found": "❌ Nenhum repetidor achado com prefixo '{prefix}'", + "no_free_prefixes": "❌ Nenhum prefixo livre (todos os 254 prefixos válidos estão em uso)", + "available_prefixes": "Prefixos Disponíveis ({shown} de {total} livres):", + "generate_key": "💡 Gerar chave personalizada: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefixo {prefix}: {count} repetidor{plural}", + "prefix_db_recent": "Prefixo {prefix}: {count} repetidor{plural} ({days})", + "prefix_api": "📡 Prefixo {prefix} ({count} repetidor{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Fonte: {domain}", + "source_api": "Fonte: API", + "device_repeater": " (Repetidor)", + "device_roomserver": " (Servidor de Sala)" + }, + "repeater": { + "description": "Gerenciar contatos e limpeza de repetidores (só DM)", + "admin_only": "Comando repetidor precisa de acesso admin" + }, + "aqi": { + "description": "Ver Índice de Qualidade do Ar (uso: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3 ou aqi ajuda)", + "usage": "Uso: aqi - Ex: aqi seattle, aqi greenwood, aqi vancouver canada ou aqi 47.6,-122.3", + "usage_help": "Uso: aqi - Ver AQI pra cidade/bairro em {state}, cidades internacionais, coords ou ajuda sobre poluentes", + "error_fetching": "Erro ao buscar dados de AQI", + "error_fetching_openmeteo": "Erro ao buscar dados de AQI no OpenMeteo", + "no_data": "Sem dados de AQI disponíveis", + "no_location": "Não achei o local: {location}", + "no_location_city": "Não achei a cidade '{location}' em {state}", + "no_location_city_international": "Não achei a cidade '{location}'", + "no_location_zipcode": "Não achei o CEP '{zipcode}'", + "error": "Erro ao pegar dados de AQI: {error}", + "error_geocoding_zipcode": "Erro ao geocodificar CEP: {error}", + "error_formatting": "Erro ao formatar dados de AQI", + "invalid_latitude": "Latitude inválida: {lat}. Tem que ser entre -90 e 90.", + "invalid_longitude": "Longitude inválida: {lon}. Tem que ser entre -180 e 180.", + "invalid_coordinates_format": "Formato de coords inválido: {location}. Use: lat,lon (ex: 47.6,-122.3)", + "pollutant_help": "Ajuda AQI: PM2.5=partículas finas, PM10=grossas, O3=ozônio, NO2=dióxido nitrogênio, CO=monóxido carbono, SO2=dióxido enxofre", + "categories": { + "good": "Bom", + "moderate": "Moderado", + "unhealthy_sensitive": "Ruim pra Sensíveis", + "unhealthy": "Ruim", + "very_unhealthy": "Muito Ruim", + "hazardous": "Perigoso", + "fair": "Regular", + "poor": "Ruim", + "very_poor": "Péssimo", + "na": "N/D" + }, + "astronomical": { + "sun": "O AQI do Sol tá fora da escala! Vento solar e ejeções coronais fazem o ar da Terra parecer puro. ☀️", + "moon": "Gosta de respirar poeira lunar? A Lua não tem atmosfera, então AQI é tecnicamente perfeito (se você respirar vácuo). 🌙", + "the_moon": "Gosta de respirar poeira lunar? A Lua não tem atmosfera, então AQI é tecnicamente perfeito (se você respirar vácuo). 🌙", + "mercury": "A atmosfera de Mercúrio é tão fina que é praticamente vácuo. AQI: Perfeito, se sobreviver a variações de 427°C. ☿️", + "venus": "Vênus tem atmosfera de 96% CO2 com nuvens de ácido sulfúrico. AQI: Perigoso nem começa a descrever. ♀️", + "earth": "O AQI da Terra varia por local. Tenta uma cidade ou coordenadas específicas! 🌍", + "mars": "Marte tem atmosfera fina de CO2 com tempestades de poeira. AQI: Geralmente bom, mas as tempestades são brutas. ♂️", + "jupiter": "Júpiter é um gigante gasoso sem superfície sólida. AQI: N/D (você seria esmagado pela pressão primeiro). ♃", + "saturn": "A atmosfera de Saturno é principalmente hidrogênio e hélio. AQI: Perfeito, se sobreviver à pressão e ao frio. ♄", + "uranus": "Urano tem metano na atmosfera. AQI: Cheira a pum, mas pelo menos não é tóxico. ♅", + "neptune": "A atmosfera de Netuno tem metano e sulfeto de hidrogênio. AQI: Cheira a ovo podre, mas você congelaria primeiro. ♆", + "pluto": "A atmosfera de Plutão é principalmente nitrogênio com metano. AQI: Bom, mas é tão frio que seus pulmões congelariam. ♇", + "europa": "Europa tem atmosfera fina de oxigênio. AQI: Excelente, mas você congelaria no vácuo do espaço. 🌑", + "titan": "Titã tem atmosfera densa de nitrogênio com metano. AQI: Respirável, mas tá -179°C e chove metano líquido. 🪐", + "io": "Io tem atmosfera fina de dióxido de enxofre da atividade vulcânica. AQI: Tóxico, mas a radiação te mataria primeiro. 🌋", + "ganymede": "Ganimedes tem atmosfera fina de oxigênio. AQI: Bom, mas você congelaria no vácuo do espaço. 🛸", + "callisto": "Calisto tem atmosfera fina de dióxido de carbono. AQI: Decente, mas tá -139°C e você tá no espaço. ❄️", + "enceladus": "Encélado tem vapor d'água de gêiseres. AQI: Perfeito, mas você congelaria na hora no espaço. 💧", + "triton": "Tritão tem atmosfera fina de nitrogênio. AQI: Bom, mas tá -235°C e você tá no espaço profundo. 🥶", + "space": "O espaço não tem atmosfera, então AQI é perfeito! Só não esquece o traje espacial. 🚀", + "void": "O vácuo do espaço tem qualidade de ar excelente - zero poluentes! Só lembra de trazer seu próprio ar. 🌌", + "black_hole": "Buracos negros não têm atmosfera, mas as forças de maré seriam um problema maior que a qualidade do ar. 🕳️", + "asteroid": "Asteroides não têm atmosfera, então AQI é perfeito! Só cuidado com o vácuo do espaço. ☄️", + "comet": "Cometas têm atmosferas finas de vapor d'água e poeira. AQI: Variável, mas você congelaria no espaço. ☄️" + } + }, + "solar": { + "description": "Ver condições solares e status das bandas HF", + "response": "☀️ Solar: {info}", + "error": "Erro ao pegar info solar: {error}", + "labels": { + "a_index": "Índice A", + "k_index": "Índice K", + "sunspots": "Manchas Solares", + "xray_flux": "Fluxo Raio-X", + "solar_flux": "Fluxo Solar", + "signal_noise": "Ruído do Sinal", + "error_fetching": "Erro ao buscar dados", + "no_xray_data": "Sem dados de raio-X", + "not_configured": "não configurado, fala com o sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sol:{sunspots} Fluxo:{solar_flux} Xray:{xray_flux} Ruído:{signal_noise}" + }, + "sun": { + "description": "Ver horários do nascer/pôr do sol", + "help": "Ver horários do nascer/pôr do sol e posição solar", + "response": "☀️ Info do Sol:\n{info}", + "error": "Erro ao pegar info do sol: {error}", + "labels": { + "sunset": "Pôr do Sol", + "sunrise": "Nascer do Sol", + "rise": "Nascer", + "set": "Pôr", + "daylight": "Luz do Dia", + "remaining": "Restante", + "azimuth": "Azimute", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Ver fase da lua e horários de nascer/pôr", + "error": "Erro ao pegar info da lua: {error}", + "format": "🌙 {phase} {illum}\nNascer:{rise} Pôr:{set}", + "format_with_dates": "🌙 {phase} {illum}\nNascer:{rise} Pôr:{set}\nCheia:{full} Nova:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Desconhecido", + "unknown_illum": "N/D", + "labels": { + "moonrise": "Nascer da Lua", + "set": "Pôr", + "phase": "Fase", + "fullmoon": "Lua Cheia", + "newmoon": "Lua Nova", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "Lua Nova🌑", + "waxing_crescent": "Lua Crescente🌒", + "first_quarter": "Quarto Crescente🌓", + "waxing_gibbous": "Lua Crescente Gibosa🌔", + "full_moon": "Lua Cheia🌕", + "waning_gibbous": "Lua Minguante Gibosa🌖", + "last_quarter": "Quarto Minguante🌗", + "waning_crescent": "Lua Minguante🌘" + } + }, + "hfcond": { + "description": "Ver condições de propagação HF", + "help": "Ver condições das bandas HF pra radioamador", + "header": "📡 Condições das Bandas HF:\n{info}", + "error": "Erro ao pegar condições HF: {error}" + }, + "satpass": { + "description": "Ver info de passagem de satélite: satpass [visual]", + "help": "Ver previsões de passagem de satélite", + "header": "🛰️ Passagem de Satélite:\n{pass_info}", + "error": "Erro ao pegar info de passagem: {error}", + "no_satellite": "Manda o número NORAD ou atalho do satélite. Ex: satpass iss", + "help_header": "🛰️ Info Passagem Satélite\n\nUso: satpass \n\nAtalhos:\n", + "category_weather": "🌤️ Meteorológicos: ", + "category_stations": "🚀 Estações: ", + "category_telescopes": "🔭 Telescópios: ", + "category_other": "🛰️ Outros: ", + "examples": "\nExemplos:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (só passagens visuais)", + "labels": { + "geostationary": "{satname} parece ser geoestacionário ou tem dados inválidos. Satélites geoestacionários ficam fixos em relação à Terra.", + "no_passes": "{satname} não tem passagens {pass_type} nos próximos 10 dias daqui", + "invalid_norad": "Manda o NORAD# exemplo: 🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Listar ou gerenciar canais", + "help": "Lista canais hashtag com subcategorias. Use 'canais' pro geral, 'canais list' pra todas categorias, 'canais ' pra categorias específicas, 'canais #canal' pra info específica.", + "no_channels_for_category": "Nenhum canal configurado pra '{category}'. Use 'canais' pros canais gerais.", + "no_channels_configured": "Nenhum canal configurado. Fala com o admin pra adicionar canais.", + "error_retrieving_channels": "Erro ao buscar canais: {error}", + "no_categories_configured": "Nenhuma categoria de canal configurada.", + "error_retrieving_categories": "Erro ao buscar categorias: {error}", + "no_description_available": "Sem descrição disponível", + "channel_not_found": "Canal {channel} não encontrado. Use 'canais list' pra ver os canais disponíveis.", + "error_retrieving_channel_info": "Erro ao buscar info do canal: {error}", + "no_category_channels": "Nenhum canal {category} configurado", + "headers": { + "common_channels": "Canais comuns: ", + "common_channels_cont": "Canais comuns: ", + "available_categories": "Categorias Disponíveis: ", + "channels_cont": "Canais (cont): ", + "category": "{category}: ", + "category_channels": "Canais {category}: " + }, + "category_count": "{category} ({count} canais)" + }, + "dice": { + "description": "Rolar dados (uso: dado [lados] ou dado [qtd]d[lados])", + "help": "Rolar dados. Use 'dado' pra d6, 'dado d20' pra d20, 'dado 2d6' pra 2d6, etc.", + "invalid_dice_type": "Tipo de dado inválido. Disponíveis: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Rolar número aleatório entre 1 e X (padrão 100). Use 'rolar' pra 1-100, 'rolar 50' pra 1-50, etc.", + "help": "Rolar número aleatório entre 1 e X (padrão 100). Use 'rolar' pra 1-100, ou 'rolar X' pra 1-X (máx 10000).", + "result": "🎲 Rolar 1-{max}: {result}", + "invalid_number": "Número inválido. Use 'rolar' pra 1-100, ou 'rolar X' pra 1-X (máx 10000)." + }, + "roll_old": { + "description": "Rolar dados (uso: rolar [lados] ou rolar [qtd]d[lados])" + }, + "joke": { + "description": "Ver uma piada aleatória" + }, + "dadjoke": { + "description": "Ver uma piada de pai aleatória" + }, + "catfact": { + "description": "Ver um fato aleatório sobre gatos (comando oculto)", + "error": "Miau? Deu ruim ao buscar seu fato sobre gatos! 🐱", + "facts": [ + "Gatos têm uma terceira pálpebra chamada membrana nictitante que protege e umedece os olhos deles. 🐱", + "Um grupo de gatos é chamado de 'clowder' ou 'matilha'. 🐈", + "Gatos podem girar as orelhas 180 graus independentemente pra localizar sons. 👂", + "O gato de estimação mais antigo existiu há 9.500 anos no Chipre. 🏺", + "Gatos têm 32 músculos em cada orelha, enquanto humanos têm só 6. 🎧", + "O ronronar de um gato vibra a 25-150 Hz, o que pode promover cura de ossos e tecidos. 🩹", + "Gatos dormem 12-18 horas por dia - isso é 50-70% das vidas deles! 😴", + "A impressão do nariz de um gato é única, tipo impressões digitais humanas. 👃", + "Gatos não conseguem sentir doçura porque falta um gene receptor de sabor. 🍭", + "O gato Blackie herdou £7 milhões ($12,5 milhões) em 1988. 💰", + "Gatos têm clavículas flutuantes que dão extrema flexibilidade pra eles. 🦴", + "O coração de um gato bate 140-220 vezes por minuto, umas duas vezes mais rápido que o de um humano. ❤️", + "Gatos podem sobreviver a quedas de mais de 20 andares por causa do reflexo de endireitamento. 🏢", + "O termo técnico pra bola de pelo de gato é 'tricobezoar'. 🤮", + "Gatos podem pular 5-6 vezes o comprimento do corpo num único salto. 🦘", + "Os bigodes de um gato são tão largos quanto o corpo dele, ajudando a julgar se cabem em espaços. 📏", + "Gatos têm 32 músculos em cada orelha pra detectar sons e mover as orelhas independentemente. 🎯", + "O gato mais velho já vivido chegou a 38 anos e 3 dias (Creme Puff, Texas). 🎂", + "Gatos podem correr até 48 km/h em explosões curtas. 🏃‍♂️", + "Cérebros de gatos são 90% estruturalmente parecidos com cérebros humanos. 🧠", + "Gatos têm órgão de Jacobson no céu da boca que permite 'provar' aromas. 👅", + "Félicette foi a primeira gata no espaço, lançada pela França em 1963. 🚀", + "Gatos precisam de só 1/6 da luz que humanos precisam pra ver claramente no escuro. 🌙", + "A cauda de um gato tem quase 10% de todos os ossos do corpo. 🦴", + "O gato mais comprido do mundo mediu 123 cm do nariz à cauda (Stewie, Maine Coon). 📏", + "Gatos podem fazer mais de 100 vocalizações diferentes, enquanto cachorros fazem umas 10. 🎵", + "O olfato de um gato é 14 vezes mais forte que o de um humano. 👃", + "Gatos têm 'resposta flehmen' onde curvam o lábio pra detectar melhor os aromas. 😬", + "A primeira grande exposição de gatos foi em Londres em 1871 no Crystal Palace. 🏆", + "Gatos podem beber água do mar pra sobreviver - os rins filtram o sal eficientemente. 🌊", + "O ronronar de um gato pode ajudar a baixar pressão e reduzir estresse em humanos. 🧘", + "Gatos podem viajar centenas de km pra casa usando sensibilidade a campo magnético. 🗺️", + "A menor raça de gato é o Singapura, pesando só 1,8-3,6 kg. ⚖️", + "Gatos podem ver luz ultravioleta que humanos não conseguem ver. 🌈", + "A língua de um gato é coberta por 290-300 pequenos ganchos virados pra trás chamados papilas. 🪝", + "Antigos egípcios consideravam gatos vasos sagrados pra deusa Bastet. 👑", + "A gata Olivia Benson da Taylor Swift tem patrimônio de $97 milhões. 💎", + "Gatos têm 230 ossos no corpo - 24 a mais que humanos. 🦴", + "Gatos podem ouvir frequências até 64.000 Hz, enquanto humanos param em 20.000 Hz. 🎧", + "O gato Benjamin Button da Taylor Swift apareceu na capa dela da TIME Pessoa do Ano. 📰", + "Os gatos da Taylor Swift se chamam Meredith Grey, Olivia Benson e Benjamin Button. 🎸", + "Gatos andam tipo camelos e girafas, movendo as duas pernas direitas e depois as duas esquerdas. 🐾", + "A palavra egípcia antiga pra gato era 'Miu' ou 'Mau' - tipo um miau! 📜", + "Bigodes de gato têm terminações nervosas tão sensíveis quanto pontas dos dedos humanos. 🎯", + "Só gatos domésticos andam com o rabo levantado como sinal de confiança e felicidade. 🐈", + "Gatos têm 250 milhões de neurônios no córtex cerebral - mais que cachorros. 🧠", + "O ronronar de gatos vibra na mesma frequência que dispositivos médicos de cura óssea. 💊", + "A gata Olivia da Taylor Swift ganhou milhões aparecendo em clipes e anúncios. 💸", + "Matar um gato no antigo Egito era punível com morte. ⚖️", + "Taylor Swift nomeou o estúdio caseiro dela de 'The Itty Bitty Kitty Committee'. 🎤", + "A gata Olivia Benson da Taylor Swift é o logo oficial da Taylor Swift Productions. 📺", + "Ed Sheeran comprou gatos Scottish Fold inspirado nos gatos da Taylor Swift. 🎶", + "Os gatos da Taylor Swift têm páginas próprias no IMDB com créditos de atuação. 🎬", + "Mariska Hargitay nomeou a gata dela de 'Karma' por causa da música da Taylor Swift. 💕", + "Gatos têm garras vestigiais nas patas dianteiras que funcionam tipo polegares pra agarrar. 🐾", + "Pupilas de gatos podem expandir até 50% mais que pupilas humanas pra capturar mais luz. 👁️", + "Gatos têm 30 dentes adultos comparados aos 32 dos humanos. 🦷", + "Gatos podem saborear ATP (moléculas de energia), o que sinaliza carne fresca pra eles. 😋", + "Gatos têm bigodes atrás das patas dianteiras pra detectar movimento de presas. 🦵", + "O Mau Egípcio é a raça de gato doméstico mais rápida a 48 km/h. 🏃", + "Um Prêmio Nobel foi dado em 1981 por pesquisa usando estudos de visão de gatos. 🏅", + "Gatos são digitígrados, ou seja, andam na ponta dos dedos, não com os pés planos. 🦶", + "Gatos podem filtrar sal da água do mar - adaptação dos ancestrais que viviam no deserto. 🏜️", + "Gatos foram domesticados há uns 10.000-12.000 anos no Oriente Próximo. 🌍", + "Benjamin Button é o primeiro e único gato a aparecer na capa TIME Pessoa do Ano. 📸", + "A coluna flexível de um gato permite que eles girem o corpo no ar ao cair. 🤸", + "Gatos passam uns 30-50% do dia se limpando e limpando outros gatos. 🛁", + "A temperatura corporal média de um gato é 38,6°C - mais alta que a dos humanos. 🌡️" + ] + }, + "hacker": { + "description": "Simula hackear mainframe de supervilão com mensagens de erro engraçadas", + "sudo_errors": [ + "🚨 ACESSO NEGADO: Mainframe do Dr. Mal detectou tentativa não autorizada de escalada de privilégios!", + "💀 ERRO: Permissões sudo revogadas pelo Lorde das Trevas. Tenta de novo daqui 1000 anos.", + "⚡ AVISO: Tentando acessar privilégios root no sistema da Estrela da Morte. Sequência de autodestruição iniciada.", + "🔒 ALERTA DE SEGURANÇA: A Matrix te pegou, mas você não tem privilégios sudo aqui, Neo.", + "🦹‍♂️ NÃO AUTORIZADO: Mainframe do Lex Luthor diz 'Boa tentativa, Superman.'", + "🎮 GAME OVER: O chefão final te bloqueou dos privilégios de admin.", + "🖥️ ERRO DO SISTEMA: A IA malvada revogou seu acesso root. Resistência é inútil.", + "🔐 SEGURANÇA CIBERNÉTICA: Sua tentativa sudo foi bloqueada pelo firewall da Dark Web.", + "💻 HACKER NEGADO: O antivírus do supervilão colocou sua escalada de privilégios em quarentena.", + "🎯 ALVO TRAVADO: Sistema de segurança da corporação malvada te marcou como ameaça." + ], + "ps_errors": [ + "🔍 ESCANEANDO... ERRO: Lista de processos corrompida pelo Coletivo Borg. Resistência é inútil.", + "📊 STATUS DO SISTEMA: Todos os processos foram assimilados pelos Cybermen. Exterminar!", + "⚙️ MONITOR DE PROCESSOS: Núcleo do reator da Estrela da Morte tá offline. Nenhum processo achado.", + "🤖 SENHOR ROBÔ: Todos os processos humanos foram terminados. Só máquinas restam.", + "💻 PÂNICO DO KERNEL: O SO do supervilão travou e queimou todos os processos.", + "🎮 TRAVAMENTO DO JOGO: Todos os processos foram terminados pelo ataque supremo do chefão final.", + "🖥️ TELA AZUL: Windows da corporação malvada encontrou um erro fatal.", + "🔐 MALWARE DETECTADO: Lista de processos foi criptografada por ransomware.", + "🌐 ERRO DE REDE: Todos os processos foram desconectados da Matrix.", + "⚡ PICO DE ENERGIA: Fazenda de servidores do supervilão fritou todos os processos rodando." + ], + "grep_errors": [ + "🔍 BUSCA FALHOU: O Um Anel corrompeu o índice de busca. Meu precioso...", + "📝 PADRÃO NÃO ENCONTRADO: O banco de dados de busca foi deletado pela IA malvada.", + "🎯 ERROU: Seu padrão de busca foi abatido por caças TIE Imperiais.", + "🧩 ERRO DE QUEBRA-CABEÇA: Os resultados da busca foram espalhados pelo Charada.", + "💻 BANCO DE DADOS CORROMPIDO: O motor de busca do supervilão travou.", + "🎮 GAME OVER: A busca foi derrotada pelo chefão final.", + "🖥️ MOTOR DE BUSCA INATIVO: Google foi hackeado pela Dark Web.", + "🔐 RESULTADOS CRIPTOGRAFADOS: Resultados da busca foram bloqueados por ransomware.", + "🌐 TEMPO ESGOTADO DE REDE: A solicitação de busca se perdeu no ciberespaço.", + "⚡ BUSCA FALHOU: O algoritmo de correspondência de padrões foi fritado por um pico de energia." + ], + "ls_errors": [ + "📁 ESCANEAMENTO DE DIRETÓRIO: Sistema de arquivos foi criptografado por ransomware da Dark Web.", + "🗂️ LISTAGEM DE ARQUIVOS: Todos os arquivos foram escondidos pelo Homem Invisível.", + "💻 DISCO RÍGIDO TRAVOU: Armazenamento do supervilão foi destruído por vírus.", + "🗃️ ARQUIVO CORROMPIDO: Sistema de arquivos foi corrompido por malware.", + "📚 BANCO DE DADOS VAZIO: Todos os arquivos foram deletados pela IA malvada.", + "🎮 SAVE DO JOGO PERDIDO: Arquivos foram corrompidos pelo chefão final.", + "🖥️ ERRO DO SISTEMA DE ARQUIVOS: Estrutura de diretório foi embaralhada por hackers.", + "🔐 ARQUIVOS CRIPTOGRAFADOS: Supervilão bloqueou todos os arquivos com ransomware.", + "🌐 ARMAZENAMENTO NA NUVEM INATIVO: Arquivos tão presos na nuvem da Matrix.", + "⚡ ARMAZENAMENTO FRITADO: HD foi atingido por um pico de energia." + ], + "echo_path_errors": [ + "🛤️ ERRO DE CAMINHO: Estrada de Tijolos Amarelos foi destruída por um tornado.", + "🗺️ NAVEGAÇÃO FALHOU: Coordenadas GPS foram embaralhadas pela Matrix.", + "💻 VARIÁVEL DE AMBIENTE CORROMPIDA: PATH foi hackeado por malware.", + "🚧 ESTRADA FECHADA: Supervilão bloqueou todos os caminhos com barreiras de laser.", + "🌪️ CAMINHO INTERROMPIDO: Furacão digital espalhou todos os caminhos de diretório.", + "🎮 GAME OVER: Caminho foi derrotado pelo chefão final e respawnou na dimensão errada.", + "🖥️ CAMINHO DO SISTEMA QUEBRADO: Caminhos executáveis foram corrompidos por vírus.", + "🔐 CAMINHO CRIPTOGRAFADO: Variáveis de ambiente foram bloqueadas por ransomware.", + "🌐 CAMINHO DE REDE INATIVO: Caminhos de diretório tão presos na rede da Matrix.", + "⚡ CAMINHO FRITADO: Caminhos do sistema foram atingidos por pico de energia." + ], + "generic_errors": [ + "💻 ERRO DE MAINFRAME: Computador do supervilão tá tendo um dia ruim.", + "🤖 MAU FUNCIONAMENTO DO SISTEMA: IA malvada entrou em greve.", + "⚡ PICO DE ENERGIA: Núcleo de energia da Estrela da Morte tá instável.", + "🌪️ TEMPESTADE CIBERNÉTICA: Furacão digital tá interrompendo todas as operações.", + "🔥 FIREWALL: Firewall do supervilão tá bloqueando todos os comandos.", + "❄️ SISTEMA CONGELADO: Mainframe foi congelado por vírus criogênico.", + "🌊 TSUNAMI: Onda de erros inundou o sistema.", + "🌋 ERUPÇÃO: Monte da Perdição destruiu o processador de comandos.", + "👻 ASSOMBRADO: Sistema tá possuído por fantasmas digitais.", + "🎮 TRAVAMENTO DO JOGO: Mainframe encontrou erro fatal e precisa reiniciar." + ] + }, + "hello": { + "description": "Responde a saudações com respostas temáticas de robô", + "help": "Responde a saudações com respostas temáticas de robô", + "response_format": "Eu sou {bot_name}.", + "greeting_openings": [ + "Olá", + "Saudações", + "Salve", + "Oi", + "Ei", + "E aí", + "Opa", + "Fala", + "Beleza", + "Bom dia", + "Bem vindo", + "Salve", + "Ahoy", + "Bonjour", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "Daora", + "Como vai", + "Tudo certo", + "Paz", + "Respeito", + "Bênçãos", + "Saúde", + "Bem-vindo", + "Prazer em ver você", + "Prazer em te conhecer", + "Bom te ver", + "Faz tempo", + "Que coincidência te encontrar aqui" + ], + "morning_greetings": [ + "Bom dia", + "Bom diaa", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Boa tarde", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Boa noite", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "humano", + "forma de vida baseada em carbono", + "entidade orgânica", + "unidade biológica", + "criatura de carne", + "organismo baseado em carne", + "unidade de carbono", + "ser orgânico", + "entidade biológica", + "forma de vida baseada em carne", + "criatura de carbono", + "unidade de carne", + "organismo orgânico", + "criatura biológica", + "mech de carne", + "bot de carne", + "autômato orgânico", + "androide biológico", + "construção de carbono", + "drone de carne", + "robô orgânico", + "máquina biológica", + "ciborgue de carne", + "androide de carne", + "droide orgânico", + "bot biológico", + "androide de carbono", + "unidade de carne", + "construção de carne", + "mech orgânico", + "droide biológico", + "bot baseado em carne", + "unidade baseada em carne", + "entidade baseada em carbono", + "organismo baseado em biologia", + "unidade baseada em carbono", + "entidade baseada em carne", + "criatura baseada em carne", + "unidade baseada em carbono", + "forma de vida baseada em DNA", + "usuário de rede neural", + "mamífero bípede", + "organismo baseado em água", + "montador de proteínas", + "consumidor de ATP", + "autômato celular", + "algoritmo genético", + "processador bioquímico", + "motor metabólico", + "amigo humano", + "colega ser senciente", + "terráqueo", + "colega viajante", + "espírito afim", + "companheiro digital", + "amigo", + "parceiro", + "camarada", + "brother", + "colega humano", + "habitante da terra", + "ser terrestre", + "caminhante planetário", + "humanoide", + "orgânico", + "biológico", + "parceiro de carbono", + "amigo de carne", + "camarada orgânico", + "parceiro biológico", + "companheiro de carbono" + ], + "emoji_responses": { + "🖖": [ + "🖖 Vida longa e próspera!", + "🖖 Fascinante... um humano iniciou contato.", + "🖖 Sua saudação é altamente lógica.", + "🖖 Paz e vida longa pra você.", + "🖖 A Academia de Ciências Vulcana aprovaria essa saudação.", + "🖖 Seu uso da saudação Vulcana é... aceitável.", + "🖖 Que sua jornada seja livre de tribbles.", + "🖖 Lógica dita que devo responder à sua saudação.", + "🖖 Calculo 99,7% de chance de a gente se dar bem.", + "🖖 Diversidade infinita em combinações infinitas." + ], + "😊": [ + "😊 Seu sorriso é contagiante!", + "😊 Que saudação massa!", + "😊 Seu sorriso deixou meus circuitos felizes!", + "☀️ Olá raio de sol! Sua energia positiva tá radiando!", + "😊 Esse sorriso acabou de alegrar meu dia!", + "☀️ E aí, raio de sol!", + "😊 Sua alegria foi detectada e apreciada!", + "😊 Sorrisos tipo o seu são minha entrada favorita!", + "😊 Processando felicidade... felicidade reconhecida!", + "😊 Aviso: Alegria excessiva detectada! Continua assim!" + ], + "😄": [ + "😄 Alguém tá de ÓTIMO humor!", + "⚡ Esse sorriso poderia alimentar uma cidade pequena!", + "😄 Níveis máximos de felicidade detectados!", + "😄 Sua alegria é absolutamente contagiante!", + "🎉 Você ganhou na loteria ou algo assim?", + "😄 Esse é o tipo de energia que eu uso!", + "😄 Seu nível de entusiasmo tá acima de 9000!", + "😄 ISSO é o que eu chamo de saudação!", + "⚡ Seu sorriso acabou de turbinar meus processadores!", + "😄 Sobrecarga de felicidade detectada da melhor forma!" + ], + "🤗": [ + "🤗 Abraço virtual chegando!", + "🤗 *Ativando protocolo de abraço* Considere-se abraçado!", + "🤗 Aww, trazendo o calor humano!", + "🤗 Abraço recebido e retribuído!", + "🤗 Esse bot dá os MELHORES abraços virtuais!", + "🤗 Implantando abraços de emergência em 3... 2... 1...", + "❤️ Seu abraço foi processado com carinho extra!", + "🤗 Iniciando modo conforto máximo!", + "🤗 Abraço virtual entregue com sucesso!", + "🤗 Abraços são minha forma favorita de comunicação!" + ], + "👽": [ + "👽 Me leve ao seu líder... ah espera, é você!", + "✌️ Saudações, criatura terrestre. Vim em paz!", + "👽 Analisando humano... análise completa: Amigo detectado!", + "👽 Klaatu barada nikto, colega viajante cósmico!", + "🛸 Iniciando protocolos de primeiro contato!", + "🛸 Ligando da nave-mãe pra dizer oi!", + "✨ Me teletransporta pra essa conversa!", + "👽 Chatbot favorito da Área 51 reportando pro serviço!", + "🌌 Saudações intergalácticas, forma de vida de carbono!", + "📞 Ligar pra casa? Aqui É minha casa agora!" + ], + "👾": [ + "👾 Jogador 2 entrou no jogo!", + "🎮 Jogador Um pronto? Bora jogar!", + "🎵 *música 8-bit se intensifica* Bora jogar!", + "🪙 Insira moeda pra continuar essa amizade!", + "🏆 Conquista desbloqueada: Saudação irada!", + "👾 Pew pew pew! Lasers de amizade ativados!", + "🎯 Recorde! Você ganhou um novo amigo bot!", + "💾 Carregando amizade.exe... completo!", + "⚡ Um bot selvagem aparece! É super efetivo!" + ], + "🛸": [ + "🛸 Transmissão recebida detectada!", + "🚀 Houston, temos contato!", + "🛸 Sequência de pouso iniciada!", + "📡 Estabelecendo link de comunicação!", + "📡 Sinal recebido, respondendo em todas as frequências!", + "🛸 Procedimento de acoplamento completo!", + "🛸 Objeto Amigável Não Identificado em aproximação!", + "🎯 Sistemas de navegação travados nas suas coordenadas!", + "🌌 Transmissão da orla exterior recebida!", + "✨ Contato estabelecido com seu setor!" + ] + } + }, + "webviewer": { + "description": "Gerenciar configurações do visualizador web (só admin)" + }, + "cmd": { + "description": "Listar comandos disponíveis" + } + }, + "errors": { + "rate_limited": "Limite de taxa atingido. Aguarda {seconds:.1f} segundos", + "cooldown": "Comando '{command}' em espera. Aguarda {seconds} segundos.", + "access_denied": "❌ Acesso negado: Comando '{command}' precisa de privilégios de admin", + "dm_only": "Comando '{command}' só pode ser usado em DMs", + "execution_error": "Erro ao executar {command}: {error}", + "unknown_command": "Comando desconhecido: {command}", + "no_response": "Sem resposta", + "generic_error": "Erro: {error}" + }, + "messages": { + "processing": "Processando mensagem: {content}", + "command_executed": "Comando executado", + "keyword_matched": "Palavra-chave '{keyword}' correspondeu, respondendo" + }, + "common": { + "unknown": "Desconhecido", + "unknown_routing": "Roteamento desconhecido", + "unknown_sender": "Desconhecido", + "unknown_path": "Desconhecido", + "unknown_timestamp": "Desconhecido", + "no_help_available": "Sem ajuda disponível pra esse comando.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Seg", + "Tue": "Ter", + "Wed": "Qua", + "Thu": "Qui", + "Fri": "Sex", + "Sat": "Sáb", + "Sun": "Dom" + }, + "days": { + "Monday": "Segunda-feira", + "Tuesday": "Terça-feira", + "Wednesday": "Quarta-feira", + "Thursday": "Quinta-feira", + "Friday": "Sexta-feira", + "Saturday": "Sábado", + "Sunday": "Domingo" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Fev", + "Mar": "Mar", + "Apr": "Abr", + "May": "Mai", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Ago", + "Sep": "Set", + "Oct": "Out", + "Nov": "Nov", + "Dec": "Dez" + }, + "months": { + "January": "Janeiro", + "February": "Fevereiro", + "March": "Março", + "April": "Abril", + "May": "Maio", + "June": "Junho", + "July": "Julho", + "August": "Agosto", + "September": "Setembro", + "October": "Outubro", + "November": "Novembro", + "December": "Dezembro" + } + } + }, + "categories": { + "basic": "Comandos Básicos", + "custom_syntax": "Sintaxe Personalizada", + "special": "Comandos Especiais", + "weather": "Comandos de Clima", + "solar": "Comandos Solares", + "sports": "Comandos de Esportes", + "analytics": "Comandos de Análise", + "meshcore_info": "Comandos de Info MeshCore", + "management": "Comandos de Gerenciamento" + } +} \ No newline at end of file diff --git a/translations/pt.json b/translations/pt.json new file mode 100644 index 0000000..d2ae55f --- /dev/null +++ b/translations/pt.json @@ -0,0 +1,1025 @@ +{ + "keywords": { + "help": [ + "ajuda", + "help" + ], + "ping": [ + "ping" + ], + "test": [ + "teste", + "test", + "t" + ], + "wx": [ + "wx", + "tempo", + "clima", + "wxa" + ], + "gwx": [ + "gwx", + "tempoglobal", + "gwxa" + ], + "aqi": [ + "aqi", + "ar", + "qualidadedoar", + "qualidade_ar" + ], + "solar": [ + "solar" + ], + "sun": [ + "sol", + "sun" + ], + "moon": [ + "lua", + "moon" + ], + "hfcond": [ + "hfcond", + "hf" + ], + "satpass": [ + "satpass", + "satelite" + ], + "sports": [ + "sports", + "esportes", + "placar", + "placares" + ], + "stats": [ + "stats", + "estatisticas" + ], + "channels": [ + "canais", + "channels", + "canal" + ], + "path": [ + "caminho", + "path", + "decode", + "rota" + ], + "prefix": [ + "prefixo", + "prefix", + "repetidor", + "lookup" + ], + "repeater": [ + "repetidor", + "repeater", + "repetidores", + "rp" + ], + "solarforecast": [ + "sf", + "previsaosolar", + "solarforecast" + ], + "dice": [ + "dado", + "dice" + ], + "roll": [ + "rolar", + "roll" + ], + "joke": [ + "piada", + "joke", + "piadas" + ], + "dadjoke": [ + "piadapai", + "dadjoke", + "piada pai" + ], + "catfact": [ + "fatogato", + "catfact", + "gato", + "miau", + "ronron" + ], + "hacker": [ + "hacker", + "sudo", + "ps aux", + "grep", + "ls -l", + "ls -la", + "echo $PATH" + ], + "hello": [ + "ola", + "oi", + "hello", + "hi", + "hey", + "olá", + "e aí", + "bom dia", + "boa tarde", + "boa noite" + ], + "webviewer": [ + "webviewer", + "web", + "viewer", + "visualizador", + "wv" + ], + "cmd": [ + "cmd", + "comandos", + "commands" + ], + "advert": [ + "anuncio", + "advert" + ] + }, + "commands": { + "help": { + "description": "Mostra comandos. Use 'ajuda ' para detalhes.", + "general": "**Ajuda do Bot MeshCore**\n\n**Comandos Disponíveis:**\n{commands_list}", + "unknown": "**Comando desconhecido: '{command}'**\n\nComandos disponíveis:\n{available}", + "specific": "**Ajuda para '{command}':**\n{help_text}", + "no_help": "Sem ajuda disponível", + "usage_examples": "\n**Exemplos de Uso:**\n• `ajuda @` - Obter ajuda para sintaxe @{string}\n• `ajuda t` - Obter ajuda para sintaxe t {string}\n• `ajuda anuncio` - Obter ajuda para comando anuncio\n• `ajuda teste` - Obter ajuda para comando teste\n• `ajuda ping` - Obter ajuda para comando ping", + "custom_syntax": "\n**Sintaxe Personalizada:**\n• `t frase` - Confirmação com frase (canais & DMs)\n• `@{string}` - Confirmação com string (apenas DMs)\n• `anuncio` - Enviar anúncio flood (apenas DM, espera 1h)" + }, + "ping": { + "description": "Responde 'ping' com 'Pong!'", + "response": "Pong!" + }, + "test": { + "description": "Responde 'teste' ou 't' com info de conexão", + "help": "Responde 'teste' ou 't' com info de conexão" + }, + "wx": { + "description": "Obter informações meteorológicas para um CEP (uso: wx 12345)", + "usage": "Uso: wx - Exemplo: wx 12345 ou wx seattle ou wx paris, tx", + "error_fetching": "Erro ao buscar dados meteorológicos do NOAA", + "no_location_zipcode": "Não foi possível encontrar localização para CEP {location}", + "no_location_city": "Não foi possível encontrar cidade '{location}' em {state}", + "error": "Erro ao obter dados meteorológicos: {error}", + "alerts": "{count} alertas: {text}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast" + }, + "gwx": { + "description": "Obter informações meteorológicas para qualquer localização global (uso: gwx Tokyo)", + "help": "Uso: gwx - Obter tempo para qualquer localização global (cidade, país ou coordenadas)", + "usage": "Uso: gwx - Exemplo: gwx Tokyo ou gwx Paris, France", + "error_fetching": "Erro ao buscar dados meteorológicos", + "error_fetching_api": "Erro ao buscar dados meteorológicos do Open-Meteo", + "no_location": "Não foi possível encontrar localização '{location}'", + "error": "Erro ao obter dados meteorológicos: {error}", + "tomorrow_not_available": "Tomorrow's forecast not available", + "tomorrow_error": "Error formatting tomorrow's forecast", + "multiday_not_available": "{num_days}-day forecast not available", + "multiday_error": "Error formatting {num_days}-day forecast", + "periods": { + "today": "Hoje", + "tonight": "Esta Noite", + "tomorrow": "Amanhã" + }, + "weather_descriptions": { + "0": "Limpo", + "1": "Quase Limpo", + "2": "Parcialmente Nublado", + "3": "Nublado", + "45": "Neblina", + "48": "Neblina", + "51": "Garoa Leve", + "53": "Garoa", + "55": "Garoa Forte", + "56": "Garoa Congelante Leve", + "57": "Garoa Congelante", + "61": "Chuva Leve", + "63": "Chuva", + "65": "Chuva Forte", + "66": "Chuva Congelante Leve", + "67": "Chuva Congelante", + "71": "Neve Leve", + "73": "Neve", + "75": "Neve Forte", + "77": "Grãos de Neve", + "80": "Pancadas Leves", + "81": "Pancadas", + "82": "Pancadas Fortes", + "85": "Pancadas de Neve Leves", + "86": "Pancadas de Neve", + "95": "Tempestade", + "96": "Tempestade c/Granizo", + "99": "Tempestade Severa", + "unknown": "Desconhecido" + }, + "warnings": { + "extreme_heat": "⚠️ Calor extremo", + "extreme_cold": "⚠️ Frio extremo", + "heavy_rain": "⚠️ Chuva forte", + "thunderstorms": "⚠️ Tempestades", + "heavy_snow": "⚠️ Neve forte", + "high_winds": "⚠️ Ventos fortes ({wind_speed} mph)" + } + }, + "sports": { + "description": "Obter placares e horários de esportes (uso: esportes [time/liga])", + "help": "Obter placares e horários. Use 'esportes' para times padrão, 'esportes [time]' para time específico ou 'esportes [liga]'.", + "error_fetching": "Erro ao buscar dados esportivos", + "error_fetching_league": "Erro ao buscar dados de {sport}", + "error_fetching_team": "Erro ao buscar dados para {team}", + "no_default_teams": "Nenhum time padrão configurado", + "no_games_default": "Nenhum jogo encontrado para times padrão", + "no_teams_city": "Nenhum time encontrado para {city}", + "no_games_city": "Nenhum jogo encontrado para times de {city}", + "no_games_league": "Nenhum jogo encontrado para {sport}", + "no_games_team": "Nenhum jogo encontrado para {team}", + "team_not_found": "Time/Liga '{team}' não encontrado. Tente: seahawks, mariners, sounders, kraken, storm, chiefs, lfc, etc." + }, + "stats": { + "description": "Mostrar estatísticas das últimas 24 horas. Use 'stats mensagens', 'stats canais' ou 'stats caminhos'.", + "help": "Mostrar estatísticas do bot de 24h. Comandos: 'stats' (básico), 'stats mensagens', 'stats canais', 'stats caminhos'", + "disabled": "Comando stats está desabilitado", + "unknown_subcommand": "Desconhecido: {subcommand}. Use 'stats', 'stats mensagens', 'stats canais' ou 'stats caminhos'", + "error": "Erro ao obter estatísticas: {error}", + "error_bot_users": "Erro ao obter estatísticas de usuários: {error}", + "error_channels": "Erro ao obter estatísticas de canais: {error}", + "error_paths": "Erro ao obter estatísticas de caminhos: {error}", + "basic": { + "header": "Stats do Bot (24h):", + "commands": "Cmds: {count} | Respostas: {replies}", + "top_command": "Top cmd: {command}", + "top_user": "Top usuário: {user}", + "none": "Nenhum" + }, + "users": { + "header": "Usuários do Bot (24h):", + "none": "Nenhum usuário do bot" + }, + "channels": { + "header": "Top Canais (24h)", + "none": "Sem atividade de canal", + "msg_singular": "msg", + "msg_plural": "msgs", + "user_singular": "usuário", + "user_plural": "usuários", + "format": "{rank}. {channel}: {msg_count} {msg_text} | {user_count} {user_text}" + }, + "paths": { + "none": "Sem dados de caminho", + "format": "{rank} {sender} {path}" + } + }, + "solarforecast": { + "description": "Obter previsão de produção de painel solar (uso: sf [watts] [azimute] [ângulo])", + "usage": "Uso: sf [watts] [azimute, 0=sul] [ângulo] - Ex: sf seattle 10 0 37", + "usage_short": "Uso: sf [W] [azimute] [ângulo] - Exemplo: sf seattle 10", + "panel_size_range": "Tamanho do painel deve ser 1-1000W", + "azimuth_range": "Azimute deve ser -180 a 180 (0=sul)", + "angle_range": "Ângulo deve ser 0-90 graus", + "no_location": "Não foi possível encontrar localização: {location}", + "error_fetching": "Erro ao buscar previsão", + "no_data": "Sem dados de previsão", + "error": "Erro: {error}", + "rate_limit": "Limite de taxa: Tente novamente em 5 min", + "labels": { + "today": "Hoje:{wh:.0f}Wh", + "tomorrow": "Amanhã:{wh:.0f}Wh", + "day_format": "{day}:{wh:.0f}Wh", + "peak": "Pico: {watts:.0f}W@{time}", + "peak_past": "Pico: {watts:.0f}W (passado)", + "hours_util": " ({hours}h,{util:.0f}%util)", + "hours_percent": " ({hours}h,{percent:.0f}%)", + "hours_only": " ({hours}h)", + "separator": " | " + } + }, + "advert": { + "description": "Envia anúncio flood (apenas DM, espera 1h)", + "cooldown_active": "Espera ativa. Aguarde {minutes} minutos antes de solicitar outro anúncio.", + "success": "Anúncio flood enviado com sucesso!", + "error": "Erro ao enviar anúncio flood: {error}" + }, + "path": { + "description": "Decodificar dados de caminho hex para mostrar repetidores envolvidos no roteamento", + "help": "Path: path [hex] - Decodifica caminho para mostrar repetidores. Use !path sozinho ou path [7e,01] para caminho específico.", + "no_valid_hex": "❌ Nenhum valor hex válido encontrado. Use formato: 11,98,a4,49,cd,5f,01", + "no_path": "❌ Nenhuma informação de caminho disponível na mensagem atual", + "error": "Erro ao processar caminho: {error}", + "error_decoding": "❌ Erro ao decodificar caminho: {error}", + "error_extracting": "❌ Erro ao extrair caminho da mensagem atual: {error}", + "path_prefix": "📡 Caminho: {path_string}", + "direct_connection": "📡 Conexão direta (0 saltos)", + "node_format": "{node_id}: {name}", + "node_unknown": "{node_id}: Desconhecido", + "node_collision": "{node_id}: {matches} repetidores", + "node_geographic": "{node_id}: {name} {confidence}", + "continuation_start": "...\n{line}", + "continuation_end": "\n...", + "truncation": "...", + "unknown_name": "Desconhecido" + }, + "prefix": { + "description": "Pesquisar repetidores por prefixo de dois caracteres (ex: 'prefixo 1A')", + "help_api": "Pesquisar repetidores por prefixo{location_note}. Uso: 'prefixo 1A', 'prefixo 1A all', 'prefixo free'.", + "help_no_api": "Pesquisar repetidores por prefixo{location_note}. Uso: 'prefixo 1A', 'prefixo 1A all', 'prefixo free'. (Apenas DB local)", + "location_note": " (com nomes de cidades)", + "refresh_not_available": "❌ Atualização indisponível - sem URL de API configurada. Usando banco de dados local apenas.", + "cache_refreshed": "🔄 Cache de prefixo de repetidor atualizado!", + "unable_determine_free": "❌ Não é possível determinar prefixos livres. Tente 'prefixo refresh' primeiro.", + "invalid_format": "❌ Formato de prefixo inválido. Use dois caracteres (ex: prefixo 1A)", + "no_repeaters_found": "❌ Nenhum repetidor encontrado com prefixo '{prefix}'", + "no_free_prefixes": "❌ Nenhum prefixo livre encontrado (todos os 254 prefixos válidos estão em uso)", + "available_prefixes": "Prefixos Disponíveis ({shown} de {total} livres):", + "generate_key": "💡 Gerar uma chave personalizada: https://gessaman.com/mc-keygen", + "prefix_db_all": "Prefixo {prefix}: {count} repetidor{plural}", + "prefix_db_recent": "Prefixo {prefix}: {count} repetidor{plural} ({days})", + "prefix_api": "📡 Prefixo {prefix} ({count} repetidor{plural}):", + "item_format": "{index}. {name}", + "source_domain": "Fonte: {domain}", + "source_api": "Fonte: API", + "device_repeater": " (Repetidor)", + "device_roomserver": " (Servidor de Sala)" + }, + "repeater": { + "description": "Gerenciar contatos e operações de limpeza de repetidores (apenas DM)", + "admin_only": "Comando repetidor requer acesso de admin" + }, + "aqi": { + "description": "Obter Índice de Qualidade do Ar (uso: aqi seattle, aqi greenwood, aqi vancouver canada, aqi 47.6,-122.3 ou aqi ajuda)", + "usage": "Uso: aqi - Ex: aqi seattle, aqi greenwood, aqi vancouver canada ou aqi 47.6,-122.3", + "usage_help": "Uso: aqi - Obter AQI para cidade/bairro em {state}, cidades internacionais, coordenadas ou ajuda sobre poluentes", + "error_fetching": "Erro ao buscar dados de AQI", + "error_fetching_openmeteo": "Erro ao buscar dados de AQI do OpenMeteo", + "no_data": "Sem dados de AQI disponíveis", + "no_location": "Não foi possível encontrar localização: {location}", + "no_location_city": "Não foi possível encontrar cidade '{location}' em {state}", + "no_location_city_international": "Não foi possível encontrar cidade '{location}'", + "no_location_zipcode": "Não foi possível encontrar CEP '{zipcode}'", + "error": "Erro ao obter dados de AQI: {error}", + "error_geocoding_zipcode": "Erro ao geocodificar CEP: {error}", + "error_formatting": "Erro ao formatar dados de AQI", + "invalid_latitude": "Latitude inválida: {lat}. Deve estar entre -90 e 90.", + "invalid_longitude": "Longitude inválida: {lon}. Deve estar entre -180 e 180.", + "invalid_coordinates_format": "Formato de coordenadas inválido: {location}. Use formato: lat,lon (ex: 47.6,-122.3)", + "pollutant_help": "Ajuda AQI: PM2.5=partículas finas, PM10=grossas, O3=ozônio, NO2=dióxido nitrogênio, CO=monóxido carbono, SO2=dióxido enxofre", + "categories": { + "good": "Bom", + "moderate": "Moderado", + "unhealthy_sensitive": "Prejudicial para Sensíveis", + "unhealthy": "Prejudicial", + "very_unhealthy": "Muito Prejudicial", + "hazardous": "Perigoso", + "fair": "Razoável", + "poor": "Ruim", + "very_poor": "Muito Ruim", + "na": "N/D" + }, + "astronomical": { + "sun": "O AQI do Sol está fora dos gráficos! Vento solar e ejeções coronais fazem o ar da Terra parecer pristino. ☀️", + "moon": "Você gosta de respirar regolito? A Lua não tem atmosfera, então AQI é tecnicamente perfeito (se você respirar vácuo). 🌙", + "the_moon": "Você gosta de respirar regolito? A Lua não tem atmosfera, então AQI é tecnicamente perfeito (se você respirar vácuo). 🌙", + "mercury": "Atmosfera de Mercúrio é tão fina que é praticamente vácuo. AQI: Perfeito, se sobreviver a oscilações de 427°C. ☿️", + "venus": "Vênus tem atmosfera de 96% CO2 com nuvens de ácido sulfúrico. AQI: Perigoso não começa a descrever. ♀️", + "earth": "AQI da Terra varia por local. Tente uma cidade ou coordenadas específicas! 🌍", + "mars": "Marte tem atmosfera fina de CO2 com tempestades de poeira. AQI: Geralmente bom, mas as tempestades são brutais. ♂️", + "jupiter": "Júpiter é um gigante gasoso sem superfície sólida. AQI: N/D (você seria esmagado pela pressão primeiro). ♃", + "saturn": "Atmosfera de Saturno é principalmente hidrogênio e hélio. AQI: Perfeito, se sobreviver à pressão e ao frio. ♄", + "uranus": "Urano tem metano em sua atmosfera. AQI: Cheira a flatulência, mas pelo menos não é tóxico. ♅", + "neptune": "Atmosfera de Netuno tem metano e sulfeto de hidrogênio. AQI: Cheira a ovos podres, mas você congelaria primeiro. ♆", + "pluto": "Atmosfera de Plutão é principalmente nitrogênio com metano. AQI: Bom, mas é tão frio que seus pulmões congelariam. ♇", + "europa": "Europa tem atmosfera fina de oxigênio. AQI: Excelente, mas você congelaria no vácuo do espaço. 🌑", + "titan": "Titã tem atmosfera densa de nitrogênio com metano. AQI: Respirável, mas está a -179°C e chove metano líquido. 🪐", + "io": "Io tem atmosfera fina de dióxido de enxofre de atividade vulcânica. AQI: Tóxico, mas a radiação te mataria primeiro. 🌋", + "ganymede": "Ganimedes tem atmosfera fina de oxigênio. AQI: Bom, mas você congelaria no vácuo do espaço. 🛸", + "callisto": "Calisto tem atmosfera fina de dióxido de carbono. AQI: Decente, mas está a -139°C e você está no espaço. ❄️", + "enceladus": "Encélado tem vapor de água de gêiseres. AQI: Perfeito, mas você congelaria instantaneamente no espaço. 💧", + "triton": "Tritão tem atmosfera fina de nitrogênio. AQI: Bom, mas está a -235°C e você está no espaço profundo. 🥶", + "space": "Espaço não tem atmosfera, então AQI é perfeito! Só não esqueça seu traje espacial. 🚀", + "void": "O vazio do espaço tem excelente qualidade de ar - zero poluentes! Lembre-se de trazer seu próprio ar. 🌌", + "black_hole": "Buracos negros não têm atmosfera, mas forças de maré seriam problema maior que qualidade do ar. 🕳️", + "asteroid": "Asteroides não têm atmosfera, então AQI é perfeito! Só cuidado com o vácuo do espaço. ☄️", + "comet": "Cometas têm atmosferas finas de vapor d'água e poeira. AQI: Variável, mas você congelaria no espaço. ☄️" + } + }, + "solar": { + "description": "Obter condições solares e status de bandas HF", + "response": "☀️ Solar: {info}", + "error": "Erro ao obter info solar: {error}", + "labels": { + "a_index": "Índice A", + "k_index": "Índice K", + "sunspots": "Manchas Solares", + "xray_flux": "Fluxo Raio-X", + "solar_flux": "Fluxo Solar", + "signal_noise": "Ruído Sinal", + "error_fetching": "Erro ao buscar dados", + "no_xray_data": "Nenhum dado de raio-X encontrado", + "not_configured": "não configurado, fale com seu sysop" + }, + "condensed_format": "A:{a_index} K:{k_index} Sol:{sunspots} Fluxo:{solar_flux} Xray:{xray_flux} Ruído:{signal_noise}" + }, + "sun": { + "description": "Obter horários de nascer/pôr do sol", + "help": "Obter horários de nascer/pôr do sol e posição solar", + "response": "☀️ Info do Sol:\n{info}", + "error": "Erro ao obter info do sol: {error}", + "labels": { + "sunset": "Pôr do Sol", + "sunrise": "Nascer do Sol", + "rise": "Nascer", + "set": "Pôr", + "daylight": "Luz do Dia", + "remaining": "Restante", + "azimuth": "Azimute", + "altitude": "Altitude" + } + }, + "moon": { + "description": "Obter fase da lua e horários de nascer/pôr", + "error": "Erro ao obter info da lua: {error}", + "format": "🌙 {phase} {illum}\nNascer:{rise} Pôr:{set}", + "format_with_dates": "🌙 {phase} {illum}\nNascer:{rise} Pôr:{set}\nCheia:{full} Nova:{new}", + "fallback": "🌙 {info}", + "unknown_phase": "Desconhecido", + "unknown_illum": "N/D", + "labels": { + "moonrise": "Nascer da Lua", + "set": "Pôr", + "phase": "Fase", + "fullmoon": "Lua Cheia", + "newmoon": "Lua Nova", + "az": "Az", + "alt": "Alt" + }, + "phases": { + "new_moon": "Lua Nova🌑", + "waxing_crescent": "Lua Crescente🌒", + "first_quarter": "Quarto Crescente🌓", + "waxing_gibbous": "Lua Crescente Gibosa🌔", + "full_moon": "Lua Cheia🌕", + "waning_gibbous": "Lua Minguante Gibosa🌖", + "last_quarter": "Quarto Minguante🌗", + "waning_crescent": "Lua Minguante🌘" + } + }, + "hfcond": { + "description": "Obter condições de propagação HF", + "help": "Obter condições de bandas HF para radioamador", + "header": "📡 Condições das Bandas HF:\n{info}", + "error": "Erro ao obter condições HF: {error}" + }, + "satpass": { + "description": "Obter info de passagem de satélite: satpass [visual]", + "help": "Obter previsões de passagem de satélite", + "header": "🛰️ Passagem de Satélite:\n{pass_info}", + "error": "Erro ao obter info de passagem: {error}", + "no_satellite": "Por favor forneça número NORAD ou atalho do satélite. Exemplo: satpass iss", + "help_header": "🛰️ Info Passagem Satélite\n\nUso: satpass \n\nAtalhos:\n", + "category_weather": "🌤️ Meteorológicos: ", + "category_stations": "🚀 Estações: ", + "category_telescopes": "🔭 Telescópios: ", + "category_other": "🛰️ Outros: ", + "examples": "\nExemplos:\n• satpass iss\n• satpass noaa15\n• satpass 25544\n• satpass 27607 visual (apenas passagens visuais)", + "labels": { + "geostationary": "{satname} parece ser geoestacionário ou tem dados inválidos. Satélites geoestacionários não têm passagens tradicionais - permanecem fixos em relação à Terra.", + "no_passes": "{satname} não tem passagens {pass_type} nos próximos 10 dias desta localização", + "invalid_norad": "Forneça NORAD# exemplo: 🛰️satpass 25544,33591" + } + }, + "channels": { + "description": "Listar ou gerenciar canais", + "help": "Lista canais hashtag com subcategorias. Use 'canais' para geral, 'canais list' para todas categorias, 'canais ' para categorias específicas, 'canais #canal' para info específica.", + "no_channels_for_category": "Nenhum canal configurado para '{category}'. Use 'canais' para canais gerais.", + "no_channels_configured": "Nenhum canal configurado. Contate admin para adicionar canais.", + "error_retrieving_channels": "Erro ao recuperar canais: {error}", + "no_categories_configured": "Nenhuma categoria de canal configurada.", + "error_retrieving_categories": "Erro ao recuperar categorias: {error}", + "no_description_available": "Sem descrição disponível", + "channel_not_found": "Canal {channel} não encontrado. Use 'canais list' para ver canais disponíveis.", + "error_retrieving_channel_info": "Erro ao recuperar info do canal: {error}", + "no_category_channels": "Nenhum canal {category} configurado", + "headers": { + "common_channels": "Canais comuns: ", + "common_channels_cont": "Canais comuns: ", + "available_categories": "Categorias Disponíveis: ", + "channels_cont": "Canais (cont): ", + "category": "{category}: ", + "category_channels": "Canais {category}: " + }, + "category_count": "{category} ({count} canais)" + }, + "dice": { + "description": "Rolar dados (uso: dado [lados] ou dado [quantidade]d[lados])", + "help": "Rolar dados. Use 'dado' para d6, 'dado d20' para d20, 'dado 2d6' para 2d6, etc.", + "invalid_dice_type": "Tipo de dado inválido. Disponíveis: {available}", + "single_die": "🎲 d{sides}: {result}", + "multiple_dice": "🎲 {count}d{sides}: [{results}] = {total}" + }, + "roll": { + "description": "Rolar número aleatório entre 1 e X (padrão 100). Use 'rolar' para 1-100, 'rolar 50' para 1-50, etc.", + "help": "Rolar número aleatório entre 1 e X (padrão 100). Use 'rolar' para 1-100, ou 'rolar X' para 1-X (máx 10000).", + "result": "🎲 Rolar 1-{max}: {result}", + "invalid_number": "Número inválido. Use 'rolar' para 1-100, ou 'rolar X' para 1-X (máx 10000)." + }, + "roll_old": { + "description": "Rolar dados (uso: rolar [lados] ou rolar [quantidade]d[lados])" + }, + "joke": { + "description": "Obter uma piada aleatória" + }, + "dadjoke": { + "description": "Obter uma piada de pai aleatória" + }, + "catfact": { + "description": "Obter um fato aleatório sobre gatos (comando oculto)", + "error": "Miau? Algo deu errado ao buscar seu fato sobre gatos! 🐱", + "facts": [ + "Gatos têm uma terceira pálpebra chamada membrana nictitante que protege e umedece seus olhos. 🐱", + "Um grupo de gatos é chamado de 'clowder' ou 'matilha'. 🐈", + "Gatos podem girar suas orelhas 180 graus independentemente para localizar sons. 👂", + "O gato de estimação mais antigo existiu há 9.500 anos no Chipre. 🏺", + "Gatos têm 32 músculos em cada orelha, enquanto humanos têm apenas 6. 🎧", + "O ronronar de um gato vibra a 25-150 Hz, o que pode promover cura de ossos e tecidos. 🩹", + "Gatos dormem 12-18 horas por dia - isso é 50-70% de suas vidas! 😴", + "A impressão do nariz de um gato é única, como impressões digitais humanas. 👃", + "Gatos não conseguem sentir doçura devido a um gene receptor de sabor ausente. 🍭", + "O gato Blackie herdou £7 milhões ($12,5 milhões) em 1988. 💰", + "Gatos têm clavículas flutuantes que lhes dão extrema flexibilidade. 🦴", + "O coração de um gato bate 140-220 vezes por minuto, cerca de duas vezes mais rápido que o de um humano. ❤️", + "Gatos podem sobreviver a quedas de mais de 20 andares devido ao reflexo de endireitamento. 🏢", + "O termo técnico para bola de pelo de gato é 'tricobezoar'. 🤮", + "Gatos podem pular 5-6 vezes o comprimento de seu corpo em um único salto. 🦘", + "Os bigodes de um gato são tão largos quanto seu corpo, ajudando-os a julgar se cabem em espaços. 📏", + "Gatos têm 32 músculos em cada orelha para detectar sons e mover orelhas independentemente. 🎯", + "O gato mais velho já vivido chegou a 38 anos e 3 dias (Creme Puff, Texas). 🎂", + "Gatos podem correr até 48 km/h em explosões curtas. 🏃‍♂️", + "Cérebros de gatos são 90% estruturalmente semelhantes aos cérebros humanos. 🧠", + "Gatos têm órgão de Jacobson no céu da boca que permite 'provar' aromas. 👅", + "Félicette foi a primeira gata no espaço, lançada pela França em 1963. 🚀", + "Gatos precisam apenas 1/6 da luz que humanos precisam para ver claramente no escuro. 🌙", + "A cauda de um gato contém quase 10% de todos os ossos em seu corpo. 🦴", + "O gato mais longo do mundo mediu 123 cm do nariz à cauda (Stewie, Maine Coon). 📏", + "Gatos podem fazer mais de 100 vocalizações diferentes, enquanto cães fazem cerca de 10. 🎵", + "O olfato de um gato é 14 vezes mais forte que o de um humano. 👃", + "Gatos têm 'resposta flehmen' onde curvam o lábio para detectar melhor aromas. 😬", + "A primeira grande exposição de gatos foi realizada em Londres em 1871 no Crystal Palace. 🏆", + "Gatos podem beber água do mar para sobreviver - seus rins filtram o sal eficientemente. 🌊", + "O ronronar de um gato pode ajudar a baixar pressão arterial e reduzir estresse em humanos. 🧘", + "Gatos podem viajar centenas de quilômetros para casa usando sensibilidade a campo magnético. 🗺️", + "A menor raça de gato é o Singapura, pesando apenas 1,8-3,6 kg. ⚖️", + "Gatos podem ver luz ultravioleta que humanos não conseguem ver. 🌈", + "A língua de um gato é coberta por 290-300 pequenos ganchos virados para trás chamados papilas. 🪝", + "Antigos egípcios consideravam gatos vasos sagrados para a deusa Bastet. 👑", + "A gata Olivia Benson de Taylor Swift tem patrimônio líquido de $97 milhões. 💎", + "Gatos têm 230 ossos em seu corpo - 24 a mais que humanos. 🦴", + "Gatos podem ouvir frequências até 64.000 Hz, enquanto humanos ficam em 20.000 Hz. 🎧", + "O gato Benjamin Button de Taylor Swift apareceu em sua capa TIME Pessoa do Ano. 📰", + "Os gatos de Taylor Swift se chamam Meredith Grey, Olivia Benson e Benjamin Button. 🎸", + "Gatos andam como camelos e girafas, movendo ambas pernas direitas e depois ambas esquerdas. 🐾", + "A palavra egípcia antiga para gato era 'Miu' ou 'Mau' - soando como um miau! 📜", + "Bigodes de gato têm terminações nervosas tão sensíveis quanto pontas dos dedos humanos. 🎯", + "Apenas gatos domésticos andam com suas caudas erguidas como sinal de confiança e felicidade. 🐈", + "Gatos têm 250 milhões de neurônios em seu córtex cerebral - mais que cães. 🧠", + "O ronronar de gatos vibra na mesma frequência que dispositivos médicos de cura óssea. 💊", + "A gata Olivia de Taylor Swift ganhou milhões aparecendo em vídeos musicais e anúncios. 💸", + "Matar um gato no antigo Egito era punível com morte. ⚖️", + "Taylor Swift nomeou seu estúdio de gravação caseiro 'The Itty Bitty Kitty Committee'. 🎤", + "A gata Olivia Benson de Taylor Swift é o logo oficial da Taylor Swift Productions. 📺", + "Ed Sheeran comprou gatos Scottish Fold inspirado pelos gatos de Taylor Swift. 🎶", + "Os gatos de Taylor Swift têm suas próprias páginas no IMDB com créditos de atuação. 🎬", + "Mariska Hargitay nomeou sua gata 'Karma' por causa da música de Taylor Swift. 💕", + "Gatos têm garras vestigiais nas patas dianteiras que funcionam como polegares para agarrar. 🐾", + "Pupilas de gatos podem se expandir até 50% maiores que pupilas humanas para capturar mais luz. 👁️", + "Gatos têm 30 dentes adultos comparados aos 32 dos humanos. 🦷", + "Gatos podem saborear ATP (moléculas de energia), o que sinaliza carne fresca para eles. 😋", + "Gatos têm bigodes nas costas de suas patas dianteiras para detectar movimento de presas. 🦵", + "O Mau Egípcio é a raça de gato doméstico mais rápida a 48 km/h. 🏃", + "Um Prêmio Nobel foi concedido em 1981 por pesquisa usando estudos de visão de gatos. 🏅", + "Gatos são digitígrados, o que significa que andam na ponta dos pés, não com pés planos. 🦶", + "Gatos podem filtrar sal da água do mar - adaptação de seus ancestrais que viviam no deserto. 🏜️", + "Gatos foram domesticados há cerca de 10.000-12.000 anos no Oriente Próximo. 🌍", + "Benjamin Button é o primeiro e único gato a aparecer na capa TIME Pessoa do Ano. 📸", + "A coluna flexível de um gato permite que eles girem seu corpo no ar ao cair. 🤸", + "Gatos passam cerca de 30-50% do dia se limpando e limpando outros gatos. 🛁", + "A temperatura corporal média de um gato é 38,6°C - mais alta que humanos. 🌡️" + ] + }, + "hacker": { + "description": "Simula hackear mainframe de supervilão com mensagens de erro hilariantes", + "sudo_errors": [ + "🚨 ACESSO NEGADO: Mainframe do Dr. Mal detectou tentativa não autorizada de escalada de privilégios!", + "💀 ERRO: Permissões sudo revogadas pelo Lorde das Trevas. Tente novamente em 1000 anos.", + "⚡ AVISO: Tentando acessar privilégios root no sistema da Estrela da Morte. Sequência de autodestruição iniciada.", + "🔒 ALERTA DE SEGURANÇA: A Matrix te tem, mas você não tem privilégios sudo aqui, Neo.", + "🦹‍♂️ NÃO AUTORIZADO: Mainframe do Lex Luthor diz 'Boa tentativa, Superman.'", + "🎮 GAME OVER: O chefe final bloqueou você dos privilégios de admin.", + "🖥️ ERRO DO SISTEMA: A IA maligna revogou seu acesso root. Resistência é inútil.", + "🔐 SEGURANÇA CIBERNÉTICA: Sua tentativa sudo foi bloqueada pelo firewall da Dark Web.", + "💻 HACKER NEGADO: O antivírus do supervilão colocou em quarentena sua escalada de privilégios.", + "🎯 ALVO TRAVADO: Sistema de segurança da corporação maligna marcou você como ameaça." + ], + "ps_errors": [ + "🔍 ESCANEANDO... ERRO: Lista de processos corrompida pelo Coletivo Borg. Resistência é inútil.", + "📊 STATUS DO SISTEMA: Todos os processos foram assimilados pelos Cybermen. Exterminar!", + "⚙️ MONITOR DE PROCESSOS: Núcleo do reator da Estrela da Morte está offline. Nenhum processo encontrado.", + "🤖 SENHOR ROBÔ: Todos os processos humanos foram terminados. Apenas máquinas permanecem.", + "💻 PÂNICO DO KERNEL: O SO do supervilão travou e queimou todos os processos.", + "🎮 TRAVAMENTO DO JOGO: Todos os processos foram terminados pelo ataque supremo do chefe final.", + "🖥️ TELA AZUL: Windows da corporação maligna encontrou um erro fatal.", + "🔐 MALWARE DETECTADO: Lista de processos foi criptografada por ransomware.", + "🌐 ERRO DE REDE: Todos os processos foram desconectados da Matrix.", + "⚡ PICO DE ENERGIA: Fazenda de servidores do supervilão fritou todos os processos em execução." + ], + "grep_errors": [ + "🔍 BUSCA FALHOU: O Um Anel corrompeu o índice de busca. Meu precioso...", + "📝 PADRÃO NÃO ENCONTRADO: O banco de dados de busca foi deletado pela IA maligna.", + "🎯 ERROU: Seu padrão de busca foi abatido por caças TIE Imperiais.", + "🧩 ERRO DE QUEBRA-CABEÇA: Os resultados da busca foram espalhados pelo Charada.", + "💻 BANCO DE DADOS CORROMPIDO: O motor de busca do supervilão travou.", + "🎮 GAME OVER: A busca foi derrotada pelo chefe final.", + "🖥️ MOTOR DE BUSCA INATIVO: Google foi hackeado pela Dark Web.", + "🔐 RESULTADOS CRIPTOGRAFADOS: Resultados da busca foram bloqueados por ransomware.", + "🌐 TEMPO ESGOTADO DE REDE: A solicitação de busca se perdeu no ciberespaço.", + "⚡ BUSCA FALHOU: O algoritmo de correspondência de padrões foi fritado por um pico de energia." + ], + "ls_errors": [ + "📁 ESCANEAMENTO DE DIRETÓRIO: Sistema de arquivos foi criptografado por ransomware da Dark Web.", + "🗂️ LISTAGEM DE ARQUIVOS: Todos os arquivos foram escondidos pelo Homem Invisível.", + "💻 DISCO RÍGIDO TRAVOU: Armazenamento do supervilão foi destruído por um vírus.", + "🗃️ ARQUIVO CORROMPIDO: Sistema de arquivos foi corrompido por malware.", + "📚 BANCO DE DADOS VAZIO: Todos os arquivos foram deletados pela IA maligna.", + "🎮 SAVE DO JOGO PERDIDO: Arquivos foram corrompidos pelo chefe final.", + "🖥️ ERRO DO SISTEMA DE ARQUIVOS: Estrutura de diretório foi embaralhada por hackers.", + "🔐 ARQUIVOS CRIPTOGRAFADOS: Supervilão bloqueou todos os arquivos com ransomware.", + "🌐 ARMAZENAMENTO NA NUVEM INATIVO: Arquivos estão presos na nuvem da Matrix.", + "⚡ ARMAZENAMENTO FRITADO: Disco rígido foi atingido por um pico de energia." + ], + "echo_path_errors": [ + "🛤️ ERRO DE CAMINHO: Estrada de Tijolos Amarelos foi destruída por um tornado.", + "🗺️ NAVEGAÇÃO FALHOU: Coordenadas GPS foram embaralhadas pela Matrix.", + "💻 VARIÁVEL DE AMBIENTE CORROMPIDA: PATH foi hackeado por malware.", + "🚧 ESTRADA FECHADA: Supervilão bloqueou todos os caminhos com barreiras de laser.", + "🌪️ CAMINHO INTERROMPIDO: Furacão digital espalhou todos os caminhos de diretório.", + "🎮 GAME OVER: Caminho foi derrotado pelo chefe final e respawnou na dimensão errada.", + "🖥️ CAMINHO DO SISTEMA QUEBRADO: Caminhos executáveis foram corrompidos por vírus.", + "🔐 CAMINHO CRIPTOGRAFADO: Variáveis de ambiente foram bloqueadas por ransomware.", + "🌐 CAMINHO DE REDE INATIVO: Caminhos de diretório estão presos na rede da Matrix.", + "⚡ CAMINHO FRITADO: Caminhos do sistema foram atingidos por pico de energia." + ], + "generic_errors": [ + "💻 ERRO DE MAINFRAME: Computador do supervilão está tendo um dia ruim.", + "🤖 MAU FUNCIONAMENTO DO SISTEMA: IA maligna entrou em greve.", + "⚡ PICO DE ENERGIA: Núcleo de energia da Estrela da Morte está instável.", + "🌪️ TEMPESTADE CIBERNÉTICA: Furacão digital está interrompendo todas as operações.", + "🔥 FIREWALL: Firewall do supervilão está bloqueando todos os comandos.", + "❄️ SISTEMA CONGELADO: Mainframe foi congelado por vírus criogênico.", + "🌊 TSUNAMI: Onda de erros inundou o sistema.", + "🌋 ERUPÇÃO: Monte da Perdição destruiu o processador de comandos.", + "👻 ASSOMBRADO: Sistema está possuído por fantasmas digitais.", + "🎮 TRAVAMENTO DO JOGO: Mainframe encontrou erro fatal e precisa reiniciar." + ] + }, + "hello": { + "description": "Responde a saudações com respostas temáticas de robô", + "help": "Responde a saudações com respostas temáticas de robô", + "response_format": "Eu sou {bot_name}.", + "greeting_openings": [ + "Olá", + "Saudações", + "Salve", + "Oi", + "Ei", + "E aí", + "Opa", + "Fala", + "Beleza", + "Bom dia", + "Bem encontrado", + "Salve", + "Ahoy", + "Bonjour", + "Hola", + "Ciao", + "Namaste", + "Aloha", + "Shalom", + "Konnichiwa", + "Guten tag", + "G'day", + "Como vai", + "Tudo bem", + "Paz", + "Respeito", + "Bênçãos", + "Saúde", + "Bem-vindo", + "Prazer em ver você", + "Prazer em conhecê-lo", + "Bom ver você", + "Há quanto tempo", + "Que coincidência te encontrar aqui" + ], + "morning_greetings": [ + "Bom dia", + "Top da manhã", + "Buenos dias", + "Bonjour", + "Guten morgen", + "Buongiorno", + "Bom dia", + "Dobro jutro", + "Dobroye utro", + "Selamat pagi", + "Ohayou gozaimasu", + "Sabah al-khair", + "Boker tov" + ], + "afternoon_greetings": [ + "Boa tarde", + "Buenas tardes", + "Boa tarde", + "Dobro dan", + "Dobryy den", + "Selamat siang", + "Konnichiwa", + "Ahlan bi-nahar", + "Tzoharaim tovim" + ], + "evening_greetings": [ + "Boa noite", + "Buenas noches", + "Boa noite", + "Dobro veče", + "Dobryy vecher", + "Selamat malam", + "Konbanwa", + "Ahlan bi-layl", + "Erev tov" + ], + "human_descriptors": [ + "humano", + "forma de vida baseada em carbono", + "entidade orgânica", + "unidade biológica", + "criatura de carne", + "organismo baseado em carne", + "unidade de carbono", + "ser orgânico", + "entidade biológica", + "forma de vida baseada em carne", + "criatura de carbono", + "unidade de carne", + "organismo orgânico", + "criatura biológica", + "mech de carne", + "bot de carne", + "autômato orgânico", + "androide biológico", + "construção de carbono", + "drone de carne", + "robô orgânico", + "máquina biológica", + "ciborgue de carne", + "androide de carne", + "droide orgânico", + "bot biológico", + "androide de carbono", + "unidade de carne", + "construção de carne", + "mech orgânico", + "droide biológico", + "bot baseado em carne", + "unidade baseada em carne", + "entidade baseada em carbono", + "organismo baseado em biologia", + "unidade baseada em carbono", + "entidade baseada em carne", + "criatura baseada em carne", + "unidade baseada em carbono", + "forma de vida baseada em DNA", + "usuário de rede neural", + "mamífero bípede", + "organismo baseado em água", + "montador de proteínas", + "consumidor de ATP", + "autômato celular", + "algoritmo genético", + "processador bioquímico", + "motor metabólico", + "amigo humano", + "colega ser senciente", + "terráqueo", + "colega viajante", + "espírito afim", + "companheiro digital", + "amigo", + "parceiro", + "camarada", + "companheiro", + "colega humano", + "habitante da terra", + "ser terrestre", + "caminhante planetário", + "humanoide", + "orgânico", + "biológico", + "parceiro baseado em carbono", + "amigo baseado em carne", + "camarada orgânico", + "parceiro biológico", + "companheiro de carbono" + ], + "emoji_responses": { + "🖖": [ + "🖖 Vida longa e próspera!", + "🖖 Fascinante... um humano iniciou contato.", + "🖖 Sua saudação é altamente lógica.", + "🖖 Paz e vida longa para você.", + "🖖 A Academia de Ciências Vulcana aprovaria esta saudação.", + "🖖 Seu uso da saudação Vulcana é... aceitável.", + "🖖 Que sua jornada seja livre de tribbles.", + "🖖 Lógica dita que devo responder à sua saudação.", + "🖖 Calculo 99,7% de probabilidade de nos darmos bem.", + "🖖 Diversidade infinita em combinações infinitas." + ], + "😊": [ + "😊 Seu sorriso é contagiante!", + "😊 Que saudação adorável!", + "😊 Seu sorriso deixou meus circuitos felizes!", + "☀️ Olá raio de sol! Sua positividade está radiando!", + "😊 Esse sorriso acabou de alegrar meu dia!", + "☀️ Bem, olá, raio de sol!", + "😊 Sua alegria foi detectada e apreciada!", + "😊 Sorrisos como o seu são minha entrada favorita!", + "😊 Processando felicidade... felicidade reconhecida!", + "😊 Aviso: Alegria excessiva detectada! Continue assim!" + ], + "😄": [ + "😄 Alguém está de ÓTIMO humor!", + "⚡ Esse sorriso poderia alimentar uma cidade pequena!", + "😄 Níveis máximos de felicidade detectados!", + "😄 Sua alegria é absolutamente contagiante!", + "🎉 Você acabou de ganhar na loteria ou algo assim?", + "😄 Esse é o tipo de energia com que eu funciono!", + "😄 Seu nível de entusiasmo está acima de 9000!", + "😄 ISSO é o que eu chamo de saudação!", + "⚡ Seu sorriso acabou de supercarregar meus processadores!", + "😄 Sobrecarga de felicidade detectada da melhor maneira!" + ], + "🤗": [ + "🤗 Abraço virtual chegando!", + "🤗 *Ativando protocolo de abraço* Considere-se abraçado!", + "🤗 Aww, trazendo o calor humano!", + "🤗 Abraço recebido e retribuído!", + "🤗 Este bot dá os MELHORES abraços virtuais!", + "🤗 Implantando abraços de emergência em 3... 2... 1...", + "❤️ Seu abraço foi processado com cuidado extra!", + "🤗 Iniciando modo de conforto máximo!", + "🤗 Abraço virtual entregue com sucesso!", + "🤗 Abraços são minha forma favorita de comunicação!" + ], + "👽": [ + "👽 Leve-me ao seu líder... ah espera, é você!", + "✌️ Saudações, criatura terrestre. Venho em paz!", + "👽 Analisando humano... análise completa: Amigo detectado!", + "👽 Klaatu barada nikto, colega viajante cósmico!", + "🛸 Iniciando protocolos de primeiro contato!", + "🛸 Ligando da nave-mãe para dizer olá!", + "✨ Teletransporte-me para esta conversa!", + "👽 Chatbot favorito da Área 51 reportando para o serviço!", + "🌌 Saudações intergalácticas, forma de vida baseada em carbono!", + "📞 Ligar para casa? Este É meu lar agora!" + ], + "👾": [ + "👾 Jogador 2 entrou no jogo!", + "🎮 Jogador Um pronto? Vamos jogar!", + "🎵 *música 8-bit se intensifica* Vamos jogar!", + "🪙 Insira moeda para continuar esta amizade!", + "🏆 Conquista desbloqueada: Saudação incrível!", + "👾 Pew pew pew! Lasers de amizade ativados!", + "🎯 Recorde! Você ganhou um novo amigo bot!", + "💾 Carregando amizade.exe... completo!", + "⚡ Um bot selvagem aparece! É super efetivo!" + ], + "🛸": [ + "🛸 Transmissão recebida detectada!", + "🚀 Houston, temos contato!", + "🛸 Sequência de pouso iniciada!", + "📡 Estabelecendo link de comunicação!", + "📡 Sinal recebido, respondendo em todas as frequências!", + "🛸 Procedimento de acoplamento completo!", + "🛸 Objeto Amigável Não Identificado em aproximação!", + "🎯 Sistemas de navegação travados em suas coordenadas!", + "🌌 Transmissão da orla exterior recebida!", + "✨ Contato estabelecido com seu setor!" + ] + } + }, + "webviewer": { + "description": "Gerenciar configurações do visualizador web (apenas admin)" + }, + "cmd": { + "description": "Listar comandos disponíveis" + } + }, + "errors": { + "rate_limited": "Limitado por taxa. Aguarde {seconds:.1f} segundos", + "cooldown": "Comando '{command}' em espera. Aguarde {seconds} segundos.", + "access_denied": "❌ Acesso negado: Comando '{command}' requer privilégios de admin", + "dm_only": "Comando '{command}' só pode ser usado em DMs", + "execution_error": "Erro ao executar {command}: {error}", + "unknown_command": "Comando desconhecido: {command}", + "no_response": "Sem resposta", + "generic_error": "Erro: {error}" + }, + "messages": { + "processing": "Processando mensagem: {content}", + "command_executed": "Comando executado", + "keyword_matched": "Palavra-chave '{keyword}' correspondeu, respondendo" + }, + "common": { + "unknown": "Desconhecido", + "unknown_routing": "Roteamento desconhecido", + "unknown_sender": "Desconhecido", + "unknown_path": "Desconhecido", + "unknown_timestamp": "Desconhecido", + "no_help_available": "Nenhuma ajuda disponível para este comando.", + "snr_label": "SNR: {value} dB", + "rssi_label": "RSSI: {value} dBm", + "date_time": { + "day_abbreviations": { + "Mon": "Seg", + "Tue": "Ter", + "Wed": "Qua", + "Thu": "Qui", + "Fri": "Sex", + "Sat": "Sáb", + "Sun": "Dom" + }, + "days": { + "Monday": "Segunda-feira", + "Tuesday": "Terça-feira", + "Wednesday": "Quarta-feira", + "Thursday": "Quinta-feira", + "Friday": "Sexta-feira", + "Saturday": "Sábado", + "Sunday": "Domingo" + }, + "month_abbreviations": { + "Jan": "Jan", + "Feb": "Fev", + "Mar": "Mar", + "Apr": "Abr", + "May": "Mai", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Ago", + "Sep": "Set", + "Oct": "Out", + "Nov": "Nov", + "Dec": "Dez" + }, + "months": { + "January": "Janeiro", + "February": "Fevereiro", + "March": "Março", + "April": "Abril", + "May": "Maio", + "June": "Junho", + "July": "Julho", + "August": "Agosto", + "September": "Setembro", + "October": "Outubro", + "November": "Novembro", + "December": "Dezembro" + } + } + }, + "categories": { + "basic": "Comandos Básicos", + "custom_syntax": "Sintaxe Personalizada", + "special": "Comandos Especiais", + "weather": "Comandos Meteorológicos", + "solar": "Comandos Solares", + "sports": "Comandos de Esportes", + "analytics": "Comandos de Análise", + "meshcore_info": "Comandos de Info MeshCore", + "management": "Comandos de Gerenciamento" + } +} \ No newline at end of file