Skip to content

Commit 4a41495

Browse files
committed
fix: translations logic
1 parent d85aa50 commit 4a41495

File tree

3 files changed

+91
-45
lines changed

3 files changed

+91
-45
lines changed

userbot/core/locales.py

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import json
22
import logging
3+
import os
4+
from datetime import datetime, timezone
35
from pathlib import Path
46
from typing import Dict, Any, Optional, Tuple
57

@@ -8,8 +10,9 @@
810
logger: logging.Logger = logging.getLogger(__name__)
911

1012
# This is the base URL for official language packs.
11-
# The user needs to create this repository.
1213
OFFICIAL_LOCALES_REPO_URL: str = "https://raw.githubusercontent.com/DeBotCommunity/locales/main/{lang_code}.json"
14+
GITHUB_API_OWNER: str = "DeBotCommunity"
15+
GITHUB_API_REPO: str = "locales"
1316

1417
class TranslationManager:
1518
"""
@@ -28,58 +31,101 @@ def __init__(self, locales_dir: str = "userbot/locales"):
2831
self._cache: Dict[str, Dict[str, Any]] = {}
2932

3033
async def load_language_pack(self, identifier: str) -> Tuple[Optional[str], Optional[str]]:
31-
"""
32-
Downloads and saves a language pack from a URL or an official repository.
34+
"""Downloads a language pack if the remote version is newer, or loads it.
35+
36+
Checks the last commit date of the language file in the official GitHub
37+
repository against the local file's modification time. If the remote
38+
file is newer or the local file does not exist, it's downloaded.
39+
If the check fails, it falls back to using the local file if available.
40+
If a full URL is provided as an identifier, it's downloaded directly.
3341
3442
Args:
35-
identifier (str): A 2-letter language code or a full URL to a raw JSON file.
43+
identifier (str): A 2-letter language code (e.g., 'en') or a full URL
44+
to a raw JSON file.
3645
3746
Returns:
3847
A tuple containing (lang_code, error_message).
3948
`lang_code` is the determined language code if successful, otherwise None.
4049
`error_message` is a string describing the error if failed, otherwise None.
4150
"""
42-
url: str
43-
lang_code: str
44-
51+
# --- Direct URL Handling ---
4552
if identifier.startswith("http"):
46-
url = identifier
4753
try:
48-
# Extract file name from URL, e.g., ".../neko-lang.json" -> "neko-lang"
49-
lang_code = Path(url.split('/')[-1]).stem
50-
except Exception:
51-
return None, "Invalid URL format."
52-
else:
53-
lang_code = identifier.lower()
54-
if not (2 <= len(lang_code) <= 10 and lang_code.isalnum()):
55-
return None, "Invalid language code format."
56-
url = OFFICIAL_LOCALES_REPO_URL.format(lang_code=lang_code)
57-
54+
lang_code: str = Path(identifier.split('/')[-1]).stem
55+
async with aiohttp.ClientSession() as session:
56+
async with session.get(identifier) as response:
57+
if response.status != 200:
58+
return None, f"Could not fetch from URL (Status: {response.status})."
59+
content: str = await response.text()
60+
json.loads(content)
61+
file_path: Path = self.core_locales_path / f"{lang_code}.json"
62+
with open(file_path, 'w', encoding='utf-8') as f:
63+
f.write(content)
64+
self._cache.pop(str(file_path), None)
65+
return lang_code, None
66+
except Exception as e:
67+
logger.error(f"Failed to process URL language pack '{identifier}': {e}", exc_info=True)
68+
return None, "Invalid URL or content."
69+
70+
# --- Language Code Handling with Update Check ---
71+
lang_code = identifier.lower()
72+
if not (2 <= len(lang_code) <= 10 and lang_code.isalnum()):
73+
return None, "Invalid language code format."
74+
75+
local_path: Path = self.core_locales_path / f"{lang_code}.json"
76+
raw_url: str = OFFICIAL_LOCALES_REPO_URL.format(lang_code=lang_code)
77+
5878
try:
79+
api_url: str = f"https://api.github.com/repos/{GITHUB_API_OWNER}/{GITHUB_API_REPO}/commits"
80+
params: Dict[str, str] = {"path": f"{lang_code}.json", "page": "1", "per_page": "1"}
81+
5982
async with aiohttp.ClientSession() as session:
60-
async with session.get(url) as response:
61-
if response.status != 200:
62-
return None, f"Could not fetch language file (Status: {response.status})."
83+
async with session.get(api_url, params=params) as response:
84+
response_json: Any = await response.json()
85+
if response.status != 200 or not isinstance(response_json, list) or not response_json:
86+
if local_path.is_file():
87+
logger.warning(f"Could not check for '{lang_code}' updates. Using local version.")
88+
return lang_code, None
89+
return None, f"Language '{lang_code}' not in remote repo and no local copy."
6390

64-
content: str = await response.text()
65-
# Validate that it's valid JSON
66-
json.loads(content)
67-
68-
file_path: Path = self.core_locales_path / f"{lang_code}.json"
69-
with open(file_path, 'w', encoding='utf-8') as f:
70-
f.write(content)
71-
72-
# Clear cache for this file if it exists
73-
self._cache.pop(str(file_path), None)
74-
75-
return lang_code, None
76-
except aiohttp.ClientError:
77-
return None, "Network error while downloading language pack."
78-
except json.JSONDecodeError:
79-
return None, "Downloaded file is not valid JSON."
91+
commit_date_str: str = response_json[0]['commit']['committer']['date']
92+
remote_mtime: datetime = datetime.fromisoformat(commit_date_str.replace('Z', '+00:00'))
93+
94+
should_download: bool = False
95+
if not local_path.is_file():
96+
should_download = True
97+
logger.info(f"Local file for '{lang_code}' not found. Downloading.")
98+
else:
99+
local_mtime_ts: float = local_path.stat().st_mtime
100+
local_mtime: datetime = datetime.fromtimestamp(local_mtime_ts, tz=timezone.utc)
101+
if remote_mtime > local_mtime:
102+
should_download = True
103+
logger.info(f"Remote file for '{lang_code}' is newer. Downloading update.")
104+
else:
105+
logger.info(f"Local file for '{lang_code}' is up-to-date.")
106+
107+
if should_download:
108+
async with session.get(raw_url) as raw_response:
109+
if raw_response.status != 200:
110+
raise aiohttp.ClientError(f"Failed to download raw file, status: {raw_response.status}")
111+
content = await raw_response.text()
112+
json.loads(content)
113+
with open(local_path, 'w', encoding='utf-8') as f:
114+
f.write(content)
115+
os.utime(local_path, (remote_mtime.timestamp(), remote_mtime.timestamp()))
116+
self._cache.pop(str(local_path), None)
117+
logger.info(f"Successfully downloaded and updated '{lang_code}'.")
118+
119+
except aiohttp.ClientError as e:
120+
logger.warning(f"Network error checking for '{lang_code}' updates: {e}. Using local version as fallback.")
121+
if not local_path.is_file():
122+
return None, "Network error and no local copy available."
80123
except Exception as e:
81-
logger.error(f"Unexpected error loading language pack '{identifier}': {e}", exc_info=True)
82-
return None, "An unexpected error occurred."
124+
logger.error(f"Unexpected error updating language pack '{identifier}': {e}", exc_info=True)
125+
if not local_path.is_file():
126+
return None, "Unexpected error and no local copy available."
127+
128+
return lang_code, None
83129

84130
def _load_locale_file(self, path: Path) -> Optional[Dict[str, Any]]:
85131
"""

userbot/locales/core/en.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"help_ext_delacc": "**Command:** `.delacc <name>`\n**Argument:** `<name>` - The name of the account to delete.\n\nPermanently deletes an account and its session from the database. Requires confirmation.",
5353
"help_toggleacc": "Enable/disable an account",
5454
"help_ext_toggleacc": "**Command:** `.toggleacc <name>`\n**Argument:** `<name>` - The name of the account.\n\nToggles the account's status between 'enabled' and 'disabled'. Changes take effect after a restart.",
55-
"help_setlang": "Set language (code or URL)",
55+
"help_setlang": "Set account language",
5656
"help_ext_setlang": "**Command:** `.setlang <code|URL>`\n**Argument:** `<code|URL>` - A 2-letter language code (e.g., `en`) or a direct URL to a JSON translation file.\n\nDownloads and sets the language pack for the current account.",
5757
"help_addmod": "Add module from Git",
5858
"help_ext_addmod": "**Command:** `.addmod <url>`\n**Argument:** `<url>` - A direct link to the module's Git repository (e.g., `https://github.com/user/repo.git`).\n\nClones the repository, installs dependencies, and activates the module.",
@@ -69,8 +69,8 @@
6969
"help_restart": "Restart the userbot",
7070
"help_ext_restart": "**Command:** `.restart`\n\nRestarts the userbot's Docker container. All clients will be reconnected.",
7171
"help_updatemodules": "Update all modules",
72-
"help_ext_updatemodules": "**Command:** `.updatemodules`\n\n(In development) Triggers a check and update for modules from their sources.",
73-
"help_logs": "Show logs with filters",
72+
"help_ext_updatemodules": "**Command:** `.updatemodules`\n\nTriggers a check and update for all modules added from Git repositories.",
73+
"help_logs": "Show logs",
7474
"help_ext_logs": "**Command:** `.logs [head|tail] [N] [level=L] [source=S]`\n- `head|tail` (opt.): `tail` (default) - last N logs, `head` - first N.\n- `N` (opt.): Number of lines (default 100).\n- `level=L` (opt.): Filter by level (INFO, WARNING, ERROR).\n- `source=S` (opt.): Filter by source (module name).\n**Subcommand:** `.logs purge` - completely clears all logs.",
7575
"help_logs_purge": "Purge all logs",
7676
"help_about": "About the userbot",

userbot/locales/core/ru.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"help_ext_delacc": "**Команда:** `.delacc <имя>`\n**Аргумент:** `<имя>` - имя аккаунта для удаления.\n\nБезвозвратно удаляет аккаунт и его сессию из базы данных. Требует подтверждения.",
5353
"help_toggleacc": "Включить/выключить аккаунт",
5454
"help_ext_toggleacc": "**Команда:** `.toggleacc <имя>`\n**Аргумент:** `<имя>` - имя аккаунта.\n\nПереключает статус аккаунта между 'включен' и 'выключен'. Изменения вступают в силу после перезагрузки.",
55-
"help_setlang": "Установить язык (код или URL)",
55+
"help_setlang": "Установить язык аккаунта",
5656
"help_ext_setlang": "**Команда:** `.setlang <код|URL>`\n**Аргумент:** `<код|URL>` - двухбуквенный код языка (например, `en`) или прямая ссылка на JSON файл с переводом.\n\nСкачивает и устанавливает языковой пакет для текущего аккаунта.",
5757
"help_addmod": "Добавить модуль из Git",
5858
"help_ext_addmod": "**Команда:** `.addmod <url>`\n**Аргумент:** `<url>` - прямая ссылка на Git-репозиторий модуля (например, `https://github.com/user/repo.git`).\n\nКлонирует репозиторий, устанавливает зависимости и активирует модуль.",
@@ -69,8 +69,8 @@
6969
"help_restart": "Перезагрузить юзербот",
7070
"help_ext_restart": "**Команда:** `.restart`\n\nПолностью перезапускает контейнер с юзерботом. Все клиенты будут переподключены.",
7171
"help_updatemodules": "Обновить все модули",
72-
"help_ext_updatemodules": "**Команда:** `.updatemodules`\n\n(В разработке) Инициирует проверку и обновление модулей из их источников.",
73-
"help_logs": "Показать логи с фильтрами",
72+
"help_ext_updatemodules": "**Команда:** `.updatemodules`\n\nИнициирует проверку и обновление всех модулей, добавленных из Git-репозиториев.",
73+
"help_logs": "Показать логи",
7474
"help_ext_logs": "**Команда:** `.logs [head|tail] [N] [level=L] [source=S]`\n- `head|tail` (опц.): `tail` (по умолч.) - последние N логов, `head` - первые N.\n- `N` (опц.): Количество строк (по умолч. 100).\n- `level=L` (опц.): Фильтр по уровню (INFO, WARNING, ERROR).\n- `source=S` (опц.): Фильтр по источнику (имени модуля).\n**Подкоманда:** `.logs purge` - полностью очищает все логи.",
7575
"help_logs_purge": "Очистить все логи",
7676
"help_about": "О юзерботе",

0 commit comments

Comments
 (0)