mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
215 lines
7.4 KiB
Python
215 lines
7.4 KiB
Python
#!/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
|
|
|