Skip to content

Commit b2a94fd

Browse files
authored
Merge pull request #300 from ffmemes/codex/localize-source-admin-ru
Localize source admin controls
2 parents 7c898ff + ec292c9 commit b2a94fd

4 files changed

Lines changed: 330 additions & 54 deletions

File tree

src/tgbot/handlers/moderator/meme_source.py

Lines changed: 145 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import re
22
from dataclasses import dataclass
3+
from html import escape as html_escape
4+
from typing import Any
35

46
from telegram import Message, Update
57
from telegram.ext import (
68
ContextTypes,
79
)
810

11+
from src import localizer
912
from src.flows.parsers.tg import parse_telegram_source
1013
from src.flows.parsers.vk import parse_vk_source
1114
from src.storage.constants import MemeSourceStatus, MemeSourceType
@@ -33,6 +36,11 @@
3336
_MEME_SOURCE_LINK_RE = re.compile(MEME_SOURCE_LINK_REGEXP)
3437

3538

39+
def _t(key: str, lang: str | None, **kwargs: object) -> str:
40+
text = localizer.t(key, lang)
41+
return text.format(**kwargs) if kwargs else text
42+
43+
3644
@dataclass(frozen=True)
3745
class MemeSourceLink:
3846
url: str
@@ -78,13 +86,15 @@ def parse_meme_source_status_callback_data(data: str) -> tuple[int, str]:
7886

7987

8088
async def handle_meme_source_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
81-
if await get_moderator_user_info(update.effective_user.id) is None:
82-
await update.message.reply_text("Only moderators can manage meme sources.")
89+
moderator = await get_moderator_user_info(update.effective_user.id)
90+
lang = _get_moderator_lang(update, moderator)
91+
if moderator is None:
92+
await update.message.reply_text(_t("moderator.meme_source.only_moderators_manage", lang))
8393
return
8494

8595
link = parse_meme_source_link(update.message.text)
8696
if link is None:
87-
await update.message.reply_text("Unsupported meme source")
97+
await update.message.reply_text(_t("moderator.meme_source.unsupported_source", lang))
8898
return
8999

90100
meme_source = await get_or_create_meme_source(
@@ -94,17 +104,19 @@ async def handle_meme_source_link(update: Update, context: ContextTypes.DEFAULT_
94104
added_by=update.effective_user.id,
95105
)
96106

97-
await meme_source_admin_pipeline(meme_source, update)
107+
await meme_source_admin_pipeline(meme_source, update, lang)
98108

99109

100110
async def handle_meme_source_language_selection(
101111
update: Update, context: ContextTypes.DEFAULT_TYPE
102112
) -> None:
103113
user_id = update.effective_user.id
104-
if await get_moderator_user_info(user_id) is None:
114+
moderator = await get_moderator_user_info(user_id)
115+
lang = _get_moderator_lang(update, moderator)
116+
if moderator is None:
105117
await update.callback_query.answer(
106-
"🤷‍♀️ Only moderators can change meme source language 🤷‍♂️"
107-
) # noqa: E501
118+
_t("moderator.meme_source.only_moderators_language", lang)
119+
)
108120
return
109121

110122
args = update.callback_query.data.split(":")
@@ -118,30 +130,34 @@ async def handle_meme_source_language_selection(
118130
trigger_parse=False,
119131
)
120132
except MemeSourceNotFoundError:
121-
await update.callback_query.answer("Meme source not found")
133+
await update.callback_query.answer(_t("moderator.meme_source.not_found", lang))
122134
return
123135

124136
await log(
125137
f"ℹ️ MemeSource ${meme_source_id}: set_lang={lang_code} (by {user_id})",
126138
context.bot,
127139
)
128140

129-
await update.callback_query.answer(f"Meme source lang is {lang_code} now")
130-
await meme_source_admin_pipeline(result["source"], update)
141+
await update.callback_query.answer(
142+
_t("moderator.meme_source.language_updated", lang, language=lang_code)
143+
)
144+
await meme_source_admin_pipeline(result["source"], update, lang)
131145

132146

133147
async def handle_meme_source_change_status(
134148
update: Update, context: ContextTypes.DEFAULT_TYPE
135149
) -> None:
136150
user_id = update.effective_user.id
137-
if await get_moderator_user_info(user_id) is None:
138-
await update.callback_query.answer("🤷‍♀️ Only moderators can change meme source status 🤷‍♂️") # noqa: E501
151+
moderator = await get_moderator_user_info(user_id)
152+
lang = _get_moderator_lang(update, moderator)
153+
if moderator is None:
154+
await update.callback_query.answer(_t("moderator.meme_source.only_moderators_status", lang))
139155
return
140156

141157
try:
142158
meme_source_id, status = parse_meme_source_status_callback_data(update.callback_query.data)
143159
except (IndexError, KeyError, ValueError):
144-
await update.callback_query.answer("Invalid meme source status action")
160+
await update.callback_query.answer(_t("moderator.meme_source.invalid_status_action", lang))
145161
return
146162

147163
try:
@@ -154,24 +170,37 @@ async def handle_meme_source_change_status(
154170
trigger_parse=False,
155171
)
156172
except MemeSourceNotFoundError:
157-
await update.callback_query.answer(f"Meme source {meme_source_id} not found")
173+
await update.callback_query.answer(
174+
_t("moderator.meme_source.not_found_by_id", lang, source_id=meme_source_id)
175+
)
158176
return
159177
except ValueError as e:
160178
await update.callback_query.answer(str(e)[:180])
161179
return
162180

163181
if result["unsnoozed_count"]:
164182
await update.effective_chat.send_message(
165-
f"Unsnoozed {result['unsnoozed_count']} memes of {meme_source_id}"
183+
_t(
184+
"moderator.meme_source.unsnoozed_memes",
185+
lang,
186+
count=result["unsnoozed_count"],
187+
source_id=meme_source_id,
188+
)
166189
)
167190

168191
await log(
169192
f"ℹ️ MemeSource ${meme_source_id}: set_status={status} (by {user_id})",
170193
context.bot,
171194
)
172195

173-
await update.callback_query.answer(f"Meme source status is {status} now")
174-
await meme_source_admin_pipeline(result["source"], update)
196+
await update.callback_query.answer(
197+
_t(
198+
"moderator.meme_source.status_updated",
199+
lang,
200+
status=_status_label(status, lang),
201+
)
202+
)
203+
await meme_source_admin_pipeline(result["source"], update, lang)
175204

176205
meme_source = result["source"]
177206
if status == MemeSourceStatus.PARSING_ENABLED: # trigger parsing
@@ -183,57 +212,127 @@ async def handle_meme_source_change_status(
183212

184213
if result["snoozed_count"]:
185214
await update.effective_chat.send_message(
186-
f"Snoozed {result['snoozed_count']} memes of {meme_source_id}"
215+
_t(
216+
"moderator.meme_source.snoozed_memes",
217+
lang,
218+
count=result["snoozed_count"],
219+
source_id=meme_source_id,
220+
)
187221
)
188222

189223

190-
def _get_meme_source_info(meme_source: dict) -> str:
191-
return f"""
192-
id: {meme_source["id"]}
193-
url: {meme_source["url"]}
194-
type: {meme_source["type"]}
195-
language: {meme_source["language_code"]}
196-
added by: {meme_source["added_by"]}
197-
<b>status</b>: {meme_source["status"]}
198-
"""
224+
def _get_moderator_lang(
225+
update: Update,
226+
moderator_info: dict[str, Any] | None = None,
227+
) -> str | None:
228+
if moderator_info and moderator_info.get("interface_lang"):
229+
return str(moderator_info["interface_lang"])
230+
if update.effective_user and getattr(update.effective_user, "language_code", None):
231+
return update.effective_user.language_code
232+
return "ru"
233+
234+
235+
def _html(value: object) -> str:
236+
if value is None:
237+
return "—"
238+
return html_escape(str(value), quote=False)
239+
240+
241+
def _source_type_label(source_type: object) -> str:
242+
value = getattr(source_type, "value", source_type)
243+
if value == MemeSourceType.TELEGRAM.value:
244+
return "Telegram"
245+
if value == MemeSourceType.INSTAGRAM.value:
246+
return "Instagram"
247+
if value == MemeSourceType.VK.value:
248+
return "VK"
249+
return _html(value)
250+
251+
252+
def _status_label(status: object, lang: str | None) -> str:
253+
value = getattr(status, "value", status)
254+
try:
255+
return localizer.t(f"moderator.meme_source.status.{value}", lang)
256+
except KeyError:
257+
return _html(value)
258+
199259

200-
# Column("nlikes", Integer, nullable=False, server_default="0"),
201-
# Column("ndislikes", Integer, nullable=False, server_default="0"),
202-
# Column("nmemes_sent_events", Integer, nullable=False, server_default="0"),
203-
# Column("nmemes_parsed", Integer, nullable=False, server_default="0"),
204-
# Column("nmemes_sent", Integer, nullable=False, server_default="0"),
205-
# Column("latest_meme_age", Integer, nullable=False, server_default="0"),
260+
def _format_int(value: object) -> str:
261+
try:
262+
return f"{int(value):,}".replace(",", " ")
263+
except (TypeError, ValueError):
264+
return "—"
206265

207266

208-
def _get_meme_source_stats_info(meme_source_stats: dict) -> str:
209-
return f"""
210-
likes: {meme_source_stats["nlikes"]}
211-
dislikes: {meme_source_stats["ndislikes"]}
212-
memes sent events: {meme_source_stats["nmemes_sent_events"]}
213-
memes parsed: {meme_source_stats["nmemes_parsed"]}
214-
memes sent: {meme_source_stats["nmemes_sent"]}
215-
latest meme age: {meme_source_stats["latest_meme_age"]}
216-
"""
267+
def _format_latest_age(value: object, lang: str | None) -> str:
268+
try:
269+
days = int(value)
270+
except (TypeError, ValueError):
271+
return "—"
272+
273+
if days <= 0:
274+
return localizer.t("moderator.meme_source.today", lang)
275+
276+
return _t("moderator.meme_source.days_short", lang, days=days)
277+
278+
279+
def _get_meme_source_info(meme_source: dict, lang: str | None) -> str:
280+
source_type = _source_type_label(meme_source["type"])
281+
language = _html(meme_source["language_code"])
282+
added_by = _html(meme_source["added_by"])
283+
status = _status_label(meme_source["status"], lang)
284+
285+
return _t(
286+
"moderator.meme_source.card",
287+
lang,
288+
id=_html(meme_source["id"]),
289+
type=source_type,
290+
language=language,
291+
url=_html(meme_source["url"]),
292+
status=status,
293+
added_by=added_by,
294+
)
295+
296+
297+
def _get_meme_source_stats_info(meme_source_stats: dict, lang: str | None) -> str:
298+
return _t(
299+
"moderator.meme_source.stats",
300+
lang,
301+
likes=_format_int(meme_source_stats["nlikes"]),
302+
dislikes=_format_int(meme_source_stats["ndislikes"]),
303+
sent_events=_format_int(meme_source_stats["nmemes_sent_events"]),
304+
parsed=_format_int(meme_source_stats["nmemes_parsed"]),
305+
sent=_format_int(meme_source_stats["nmemes_sent"]),
306+
latest_age=_format_latest_age(meme_source_stats["latest_meme_age"], lang),
307+
)
217308

218309

219310
async def meme_source_admin_pipeline(
220311
meme_source: dict,
221312
update: Update,
313+
lang: str | None = None,
222314
) -> Message:
223-
ms_info = _get_meme_source_info(meme_source)
315+
if lang is None:
316+
lang = _get_moderator_lang(update)
317+
318+
ms_info = _get_meme_source_info(meme_source, lang)
224319
ms_stats = await get_meme_source_stats_by_id(meme_source["id"])
225320
if ms_stats:
226-
ms_info += _get_meme_source_stats_info(ms_stats)
321+
ms_info += "\n\n" + _get_meme_source_stats_info(ms_stats, lang)
227322

228323
if meme_source["language_code"] is None:
229324
return await send_or_edit(
230325
update,
231-
text=f"""{ms_info}\nPlease select a language for {meme_source["url"]}""",
326+
text=f"{ms_info}\n\n{localizer.t('moderator.meme_source.select_language', lang)}",
232327
reply_markup=meme_source_language_selection_keyboard(meme_source_id=meme_source["id"]),
233328
)
234329

235330
return await send_or_edit(
236331
update,
237332
text=ms_info,
238-
reply_markup=meme_source_change_status_keyboard(meme_source["id"], meme_source["status"]),
333+
reply_markup=meme_source_change_status_keyboard(
334+
meme_source["id"],
335+
meme_source["status"],
336+
lang,
337+
),
239338
)

src/tgbot/senders/keyboards.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
44

5+
from src import localizer
56
from src.storage.constants import (
67
MemeSourceStatus,
78
)
@@ -126,12 +127,13 @@ def meme_source_language_selection_keyboard(meme_source_id: int):
126127
def meme_source_change_status_keyboard(
127128
meme_source_id: int,
128129
current_status: MemeSourceStatus | str | None = None,
130+
lang: str | None = "ru",
129131
):
130132
return InlineKeyboardMarkup(
131133
[
132134
[
133135
InlineKeyboardButton(
134-
f"➡️ {status.value}",
136+
localizer.t(f"moderator.meme_source.action.{status.value}", lang),
135137
callback_data=MEME_SOURCE_SET_STATUS_PATTERN.format(
136138
meme_source_id=meme_source_id,
137139
status=status.value,

0 commit comments

Comments
 (0)