cipher-attacks commited on
Commit
663fdd6
ยท
1 Parent(s): 9318723

natural amharic

Browse files
app/bot/handlers/errors.py CHANGED
@@ -9,7 +9,7 @@ from app.config import get_settings
9
 
10
  log = logging.getLogger(__name__)
11
  _last_notified: dict[str, float] = {}
12
- NOTICE = "โš ๏ธ แ‰ฝแŒแˆญ แ‰ฐแˆแŒฅแˆฏแˆแฃ แŠฅแ‰ฃแŠญแ‹Ž แŠฅแŠ•แ‹ฐแŒˆแŠ“ แ‹ญแˆžแŠญแˆฉแข\nSomething went wrong, please try again."
13
 
14
 
15
  async def on_error(event: ErrorEvent, bot: Bot) -> bool:
 
9
 
10
  log = logging.getLogger(__name__)
11
  _last_notified: dict[str, float] = {}
12
+ NOTICE = "โš ๏ธ แ‹แ‹ญแฃ แ‰ฝแŒแˆญ แ‰ฐแˆแŒ แˆจแข แŠฅแ‰ฃแŠซแ‰ฝแˆ แŠฅแŠ•แ‹ฐแŒˆแŠ“ แˆžแŠญแˆฉแข\nSomething went wrong, please try again."
13
 
14
 
15
  async def on_error(event: ErrorEvent, bot: Bot) -> bool:
app/bot/keyboards/library.py CHANGED
@@ -5,18 +5,12 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
5
  from app.core.catalog import KINDS
6
  from app.core.i18n import t
7
  from app.db.models import Course, Material
 
8
  from app.services.classifier import display_title
9
 
10
 
11
- def clip(text: str, n: int = 48) -> str:
12
- return text if len(text) <= n else text[: n - 1] + "โ€ฆ"
13
-
14
-
15
  def course_label(c: Course) -> str:
16
- """Short enough for one button line: "code ยท name" when it fits, else just the name."""
17
- if c.code and len(c.code) + 3 + len(c.name) <= 36:
18
- return f"{c.code} ยท {c.name}"
19
- return clip(c.name, 36)
20
 
21
 
22
  def course_icon(c: Course) -> str:
 
5
  from app.core.catalog import KINDS
6
  from app.core.i18n import t
7
  from app.db.models import Course, Material
8
+ from app.core.labels import clip, course_label as _course_label
9
  from app.services.classifier import display_title
10
 
11
 
 
 
 
 
12
  def course_label(c: Course) -> str:
13
+ return _course_label(c.code, c.name)
 
 
 
14
 
15
 
16
  def course_icon(c: Course) -> str:
app/core/labels.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Short labels for buttons (pure, so they can be tested without Telegram)."""
2
+
3
+
4
+ def clip(text: str, n: int = 48) -> str:
5
+ return text if len(text) <= n else text[: n - 1] + "โ€ฆ"
6
+
7
+
8
+ def course_label(code, name: str) -> str:
9
+ """One button line: "code ยท name" when it fits, else just the name."""
10
+ if code and len(code) + 3 + len(name) <= 36:
11
+ return f"{code} ยท {name}"
12
+ return clip(name, 36)
locales/am.json CHANGED
@@ -1,66 +1,66 @@
1
  {
2
- "welcome": "๐Ÿ‘‹ แˆฐแˆ‹แˆ <b>{name}</b>!\nแ‹ˆแ‹ฐ <b>DBU Central</b> แŠฅแŠ•แŠณแŠ• แ‰ แ‹ฐแˆ…แŠ“ แˆ˜แŒกแข",
3
- "gate_text": "แ‰ฆแ‰ฑแŠ• แˆˆแˆ˜แŒ แ‰€แˆ แˆ˜แŒ€แˆ˜แˆชแ‹ซ แ‰ปแŠ“แˆ‰แŠ• แŠฅแŠ“ แ‹จแ‹แ‹ญแ‹ญแ‰ต แŽแˆจแˆ™แŠ• แ‹ญแ‰€แˆ‹แ‰€แˆ‰ ๐Ÿ‘‡\nแŠจแ‹šแ‹ซ ยซโœ… แ‰ฐแ‰€แˆ‹แ‰…แ‹ซแˆˆแˆยป แ‹ญแŒซแŠ‘แข",
4
- "gate_join_btn": "๐Ÿ“ข แ‰ปแŠ“แˆ‰แŠ• แ‰ฐแ‰€แˆ‹แ‰€แˆ",
5
  "gate_check_btn": "โœ… แ‰ฐแ‰€แˆ‹แ‰…แ‹ซแˆˆแˆ",
6
- "gate_not_yet": "แŒˆแŠ“ แ‰ปแŠ“แˆ‰แŠ• แŠ แˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‰แˆแข แˆ˜แŒ€แˆ˜แˆชแ‹ซ แ‹ญแ‰€แˆ‹แ‰€แˆ‰แข",
7
- "choose_stream": "แ‹จแ‰ตแŠ›แ‹ แ‹˜แˆญแ แˆ‹แ‹ญ แАแ‹Žแ‰ต?",
8
  "stream_natural": "๐Ÿ”ฌ แ‰ฐแˆแŒฅแˆฎ แˆณแ‹ญแŠ•แˆต",
9
  "stream_social": "๐Ÿ“– แˆ›แˆ…แ‰ แˆซแ‹Š แˆณแ‹ญแŠ•แˆต",
10
- "choose_year": "แ‹จแ‰ตแŠ›แ‹ แ‹“แˆ˜แ‰ต แˆ‹แ‹ญ แАแ‹Žแ‰ต?",
11
  "year_label": "{n}แŠ› แ‹“แˆ˜แ‰ต",
12
- "onboarded": "โœ… แ‰ฐแˆ˜แ‹แŒแ‰ แ‹‹แˆ! แŠจแ‰ณแ‰ฝ แŠซแˆˆแ‹ แˆแŠ“แˆŒ แ‹ญแˆแˆจแŒกแข",
13
- "menu_title": "แ‹‹แŠ“ แˆแŠ“แˆŒ ๐Ÿ‘‡",
14
- "btn_library": "๐Ÿ“š แ‰คแ‰ฐ-แˆ˜แŒปแˆ•แแ‰ต",
15
  "btn_gpa": "๐Ÿงฎ GPA แˆ›แˆตแ‹ซ",
16
- "btn_invite": "๐Ÿ‘ฅ แŒ“แ‹ฐแŠ› แŒ‹แ‰ฅแ‹",
17
  "btn_lang": "๐ŸŒ แ‰‹แŠ•แ‰‹",
18
- "soon": "๐Ÿšง แ‹ญแˆ… แŠญแแˆ แ‰ แ‰…แˆญแ‰ก แ‹ญแˆ˜แŒฃแˆแข",
19
- "invite_text": "๐Ÿ”— แ‹จแŠฅแˆญแˆตแ‹Ž แˆ˜แŒ‹แ‰ แ‹ฃ แˆŠแŠ•แŠญแฆ\n{link}\n\nแ‹จแŒ‹แ‰ แ‹™แ‰ต: <b>{count}</b> ยท แŠ•แ‰ แ‹จแˆ†แŠ‘: <b>{qualified}</b>\nXP: <b>{points}</b>\n\nแŒ“แ‹ฐแŠ›แ‹Ž แ‰ขแ‹ซแŠ•แˆต 3 แ‰€แŠ• แŠ•แ‰ แˆฒแˆ†แŠ• แ‰ตแˆแ‰ แŒ‰แˆญแˆป แ‹ญแŠจแˆแˆ‹แˆแข",
20
- "referral_reward": "๐Ÿ‘ฅ แŠ แ‹ฒแˆต แŒ“แ‹ฐแŠ› แ‰ แˆŠแŠ•แŠญแ‹Ž แ‰ฐแ‰€แˆ‹แ‰…แˆแˆ! +{points} XP\nแŒ“แ‹ฐแŠ›แ‹ แŠ•แ‰ แ‰ฐแˆ›แˆช แˆฒแˆ†แŠ• แ‰ฐแŒจแˆ›แˆช แ‹ญแŠจแˆแˆ‹แˆแข",
21
- "lang_changed": "โœ… แ‰‹แŠ•แ‰‹ แ‹ˆแ‹ฐ แŠ แˆ›แˆญแŠ› แ‰ฐแ‰€แ‹ญแˆฏแˆแข",
22
- "banned": "โ›” แˆ˜แˆˆแ‹ซแ‹Ž แ‰ณแŒแ‹ทแˆแข",
23
- "btn_profile": "๐Ÿ‘ค แ•แˆฎแ‹แ‹ญแˆ",
24
  "btn_top": "๐Ÿ† แ‹ฐแˆจแŒƒ",
25
  "btn_back": "โ—€๏ธ แ‰ฐแˆ˜แˆˆแˆต",
26
  "btn_prev": "โฌ…๏ธ",
27
  "btn_next": "โžก๏ธ",
28
- "kind_slide": "๐Ÿ“‘ แˆตแˆ‹แ‹ญแ‹ต",
29
- "kind_exam": "๐Ÿ“ แ‹ซแˆˆแˆ แˆแ‰ฐแŠ“",
30
- "kind_note": "๐Ÿ—’ แˆ›แˆตแ‰ณแ‹ˆแˆป",
31
- "kind_book": "๐Ÿ“˜ แˆ˜แŒฝแˆแ",
32
- "lib_courses": "๐Ÿ“š <b>แŠฎแˆญแˆถแ‰ฝ</b>\nแŠฎแˆญแˆต แ‹ญแˆแˆจแŒกแฃ แ‹ˆแ‹ญแˆ แ‹จแŠฎแˆญแˆต แˆตแˆ แŒฝแˆแ‹ แ‹ญแˆแˆแŒ‰ ๐Ÿ”",
33
- "lib_no_courses": "แˆˆแŠฅแˆญแˆตแ‹Ž แ‹˜แˆญแ แŠฅแŠ“ แ‹“แˆ˜แ‰ต แŒˆแŠ“ แŠฎแˆญแˆต แŠ แˆแ‰ฐแŒจแˆ˜แˆจแˆแข\nแ‹จแŠฎแˆญแˆต แˆตแˆ แŒฝแˆแ‹ แ‹ญแˆแˆแŒ‰ ๐Ÿ”",
34
- "lib_kinds": "๐Ÿ“˜ <b>{course}</b>\nแˆแŠ• แ‹ญแˆแˆแŒ‹แˆ‰?",
35
- "lib_no_materials": "แˆˆแ‹šแˆ… แŠฎแˆญแˆต แŒˆแŠ“ แ‹แ‹ญแˆ แŠ แˆแ‰ฐแŒจแˆ˜แˆจแˆแข",
36
  "lib_materials": "๐Ÿ“˜ <b>{course}</b> ยท {kind}",
37
- "file_missing": "แ‹ญแˆ… แ‹แ‹ญแˆ แŠ แˆแ‰ฐแŒˆแŠ˜แˆแข",
38
- "search_none": "๐Ÿ” แˆแŠ•แˆ แŠ แˆแ‰ฐแŒˆแŠ˜แˆแข แˆŒแˆ‹ แ‰ƒแˆ แ‹ญแˆžแŠญแˆฉ (แ‹จแŠฎแˆญแˆต แˆตแˆ แ‹ˆแ‹ญแˆ แŠฎแ‹ต)แข",
39
- "search_results": "๐Ÿ” <b>แ‹แŒคแ‰ถแ‰ฝ</b> แˆˆ ยซ{q}ยป",
40
- "profile_text": "๐Ÿ‘ค <b>{name}</b>\n๐ŸŽ“ {stream} ยท {year}\n\n๐Ÿ… แ‹ฐแˆจแŒƒ {level}: <b>{title}</b>\nโญ แŒ แ‰…แˆ‹แˆ‹ XP: <b>{points}</b> ({to_next})\n๐Ÿ“… แ‹จแ‹šแˆ… แˆณแˆแŠ•แ‰ต XP: <b>{week_xp}</b> ยท {week_rank}\n๐Ÿ”ฅ แ‰ฐแŠจแ‰ณแ‰ณแ‹ญ แ‰€แŠ•: <b>{streak}</b> (แˆแˆญแŒฅ {best})\n๐Ÿ‘ฅ แ‹จแŒ‹แ‰ แ‹Ÿแ‰ธแ‹: <b>{refs}</b> (แŠ•แ‰ แ‹จแˆ†แŠ‘ {refs_q})",
41
- "top_text": "๐Ÿ† <b>แ‹จแˆณแˆแŠ•แ‰ฑ แ‹ฐแˆจแŒƒ</b> ยท แ‰€แˆช {days} แ‰€แŠ•\n{rows}\n\n๐Ÿ“ แ‹จแŠฅแˆญแˆตแ‹Ž: <b>{rank}</b> ยท {week_xp} XP\nโš”๏ธ {n_label}: <b>{natural}</b> ยท {s_label}: <b>{social}</b>{prize}\n\nโ„น๏ธ แˆ…แŒŽแ‰ฝ: /rules",
42
- "inline_open_bot": "๐Ÿ“š แ‰ฆแ‰ฑแŠ• แŠญแˆแ‰ต",
43
- "prize_block": "\n\n๐Ÿ’ฐ <b>แˆณแˆแŠ•แ‰ณแ‹Š แˆฝแˆแˆ›แ‰ต</b> (แ‹จแŠ แ‹จแˆญ แŒŠแ‹œ แŠซแˆญแ‹ตแฃ แ‰ฅแˆญ)แฆ\n{lines}\n๐ŸŽŸ แŠฅแŒฃแฆ {raffle} แ‰ฅแˆญ (แˆˆแˆŒแˆŽแ‰ฝ แ‰ฅแ‰ แ‰ฐแ‹ˆแ‹ณแ‹ณแˆชแ‹Žแ‰ฝ)\nแ‰ฅแ‰ แˆˆแˆ˜แˆ†แŠ•แฆ แ‰ แˆณแˆแŠ•แ‰ฑ แ‰ขแ‹ซแŠ•แˆต {days} แ‰€แŠ• แˆ˜แŒแ‰ฃแ‰ต แŠฅแŠ“ {xp} XP",
44
- "to_next": "แˆˆแ‰€แŒฃแ‹ฉ แ‹ฐแˆจแŒƒ {n} XP แ‹ญแ‰€แˆซแˆ",
45
- "max_level": "แŠจแแ‰ฐแŠ› แ‹ฐแˆจแŒƒ ๐ŸŽ‰",
46
- "levelup": "๐ŸŽ‰ แ‹ฐแˆจแŒƒ {level} แ‹ฐแˆญแˆฐแ‹‹แˆ: <b>{title}</b>!",
47
- "streak_bonus": "๐Ÿ”ฅ {streak} แ‰€แŠ• แ‰ฐแŠจแ‰ณแ‰ณแ‹ญ! +{xp} XP แŒ‰แˆญแˆป",
48
- "referral_qualified": "๐ŸŽ‰ แ‹จแŒ‹แ‰ แ‹™แ‰ต แŒ“แ‹ฐแŠ› แŠ•แ‰ แ‰ฐแˆ›แˆช แˆ†แŠ—แˆ! +{xp} XP",
49
- "season_win": "๐Ÿ… แ‰ฃแˆˆแˆแ‹ แˆณแˆแŠ•แ‰ต #{rank} แ‹ˆแŒฅแ‰ฐแ‹‹แˆ ({xp} XP)! แŠฅแŠ•แŠณแŠ• แ‹ฐแˆต แŠ แˆˆแ‹Žแ‰ตแข",
50
- "season_win_prize": "\n๐Ÿ’ฐ แˆฝแˆแˆ›แ‰ตแฆ {birr} แ‰ฅแˆญ แ‹จแŠ แ‹จแˆญ แŒŠแ‹œ แŠซแˆญแ‹ตแข แŠ แ‹ตแˆšแŠ‘ แ‰  Telegram แ‹ซแŒˆแŠแ‹Žแ‰ณแˆแข",
51
- "season_raffle": "๐ŸŽŸ แ‰ แˆณแˆแŠ•แ‰ณแ‹Š แŠฅแŒฃ แŠ แˆธแŠ•แˆแ‹‹แˆ!",
52
- "rules_text": "๐Ÿ“œ <b>แˆ…แŒŽแ‰ฝ</b>\n\n<b>XP แŠฅแŠ•แ‹ดแ‰ต แ‹ญแŒˆแŠ›แˆ</b>\nโ€ข แ‰ แ‹จแ‰€แŠ‘ แˆ˜แŒแ‰ฃแ‰ต: 3-7 XP (แ‰ฐแŠจแ‰ณแ‰ณแ‹ญ แ‰€แŠ• แˆฒแŒจแˆแˆญ แ‹ญแŒจแˆแˆซแˆ)\nโ€ข แ‹แ‹ญแˆ แˆ›แ‹แˆจแ‹ต: +1 (แ‰ แ‰€แŠ• แŠฅแˆตแŠจ 3)\nโ€ข แŒ“แ‹ฐแŠ› แˆ˜แŒ‹แ‰ แ‹: แˆฒแ‰€แˆ‹แ‰€แˆ +5แฃ แŠ•แ‰ แ‰ฐแˆ›แˆช แˆฒแˆ†แŠ• (3+ แ‰€แŠ•) +30 (แ‰ แˆณแˆแŠ•แ‰ต แŠฅแˆตแŠจ 5)\nโ€ข แˆˆแŒ แ‰ƒแˆš แŠ แˆตแ‰ฐแ‹‹แŒฝแŠฆ แŠ แ‹ตแˆšแŠ• แˆแ‹ฉ XP แˆŠแˆฐแŒฅ แ‹ญแ‰ฝแˆ‹แˆ\nโ€ข แŠจแˆซแˆต-แˆฐแˆญ แˆแŠ•แŒฎแ‰ฝ แ‰ แ‰€แŠ• แŠฅแˆตแŠจ {cap} XP\n\n<b>แˆณแˆแŠ•แ‰ณแ‹Š แ‹แ‹ตแ‹ตแˆญ</b>\nแˆฐแŠž แ‹ญแŒ€แˆแˆซแˆแฃ แŠฅแˆแ‹ต แ‹ญแ‹˜แŒ‹แˆ (แ‹จแŠขแ‰ตแ‹ฎแŒตแ‹ซ แˆฐแ‹“แ‰ต)แข\nแˆฝแˆแˆ›แ‰ต แˆฒแŠ–แˆญ แˆˆแˆ˜แ‹ˆแ‹ณแ‹ฐแˆญแฆ แ‰ แˆณแˆแŠ•แ‰ฑ แ‰ขแ‹ซแŠ•แˆต {days} แ‰€แŠ• แˆ˜แŒแ‰ฃแ‰ตแฃ แ‰ขแ‹ซแŠ•แˆต {xp} XPแฃ แ‰ปแŠ“แˆ‰ แ‹แˆตแŒฅ แˆ˜แ‰†แ‹จแ‰ตแฃ แŠฅแŠ“ แ‹จแ‰ณแˆ›แŠแАแ‰ต แАแŒฅแ‰ฅ แ‰ แ‰‚ แˆ˜แˆ†แŠ• แŠ แˆˆแ‰ แ‰ตแข\n\n<b>แแ‰ตแˆƒแ‹ŠแАแ‰ต</b>\nโ€ข แˆแˆ‰แˆ XP แ‰ แˆ›แ‹ญแˆˆแ‹ˆแŒฅ แˆ˜แ‹แŒˆแ‰ฅ แ‹ญแˆ˜แ‹˜แŒˆแ‰ฃแˆแค แ‰ฐแˆ˜แˆณแˆณแ‹ญ แ‹ตแˆญแŒŠแ‰ต แˆแˆˆแ‰ต แŒŠแ‹œ แŠ แ‹ญแŠจแˆแˆแˆแข\nโ€ข แŠ แŒ แˆซแŒฃแˆช แŠฅแŠ•แ‰…แˆตแ‰ƒแˆด (แ‹จแ‹แˆธแ‰ต แŠ แŠซแ‹แŠ•แ‰ตแฃ แ‹ซแˆแ‰ฐแˆˆแˆ˜แ‹ฐ แ‰ฅแ‹™ แŒ‹แ‰ฃแ‹ฅแАแ‰ต) แ‰ แŠ แ‹ตแˆšแŠ• แ‹ญแˆ˜แˆจแˆ˜แˆซแˆแค แАแŒฅแ‰ฅ แˆŠแˆฐแˆจแ‹ แ‹ˆแ‹ญแˆ แˆŠแ‰ณแŒˆแ‹ต แ‹ญแ‰ฝแˆ‹แˆแข\nโ€ข แˆฝแˆแˆ›แ‰ถแ‰ฝ แˆŠแˆˆแ‹ˆแŒก แ‹ญแ‰ฝแˆ‹แˆ‰แค แ‹จแŠ แ‹ตแˆšแŠ• แ‹แˆณแŠ” แ‹จแˆ˜แŒจแˆจแˆป แАแ‹แข\nโ€ข แˆˆแˆ˜แˆณแ‰ฐแ แˆแŠ•แˆ แŠญแแ‹ซ แŠ แ‹ซแˆตแˆแˆแŒแˆแข",
53
- "level_1": "แŠ แ‹ฒแˆต แ‰ฐแˆ›แˆช",
54
  "level_2": "แŒ€แˆ›แˆช",
55
- "level_3": "แŒŽแ‰ แ‹",
56
- "level_4": "แ‰ตแŒ‰แˆ…",
57
- "level_5": "แ‰ฅแˆแˆƒแ‰ฐแŠ›",
58
  "level_6": "แˆแˆแˆญ",
59
  "level_7": "แŠ แˆ›แŠซแˆช",
60
  "level_8": "แŠฎแŠจแ‰ฅ",
61
- "level_9": "แŒ€แŒแŠ“",
62
  "level_10": "แˆŠแ‰…",
63
- "gate_missing": "แŒˆแŠ“ แ‹ซแˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‰แ‰ตแฆ {list}",
64
- "gate_join_one": "๐Ÿ“ข {name} แ‰ฐแ‰€แˆ‹แ‰€แˆ",
65
- "gate_not_yet_list": "แŒˆแŠ“ แŠ แˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‰แˆแฆ {list}"
66
  }
 
1
  {
2
+ "welcome": "๐Ÿ‘‹ แˆฐแˆ‹แˆ <b>{name}</b>!\nแ‹ˆแ‹ฐ <b>DBU Central</b> แŠฅแŠ•แŠณแŠ• แ‹ฐแˆ…แŠ“ แˆ˜แŒฃแ‰ฝแˆ ๐ŸŽ“",
3
+ "gate_text": "แ‰ฆแ‰ฑแŠ• แˆˆแˆ˜แŒ แ‰€แˆ แ‰ปแŠ“แˆ‹แ‰ฝแŠ•แŠ•แŠ“ แ‹จแ‹แ‹ญแ‹ญแ‰ต แŒแˆฉแ“แ‰ฝแŠ•แŠ• แˆ˜แ‰€แˆ‹แ‰€แˆ แ‹ซแˆตแˆแˆแŒ‹แˆ ๐Ÿ‘‡\nแŠจแ‹› แ‰ แŠ‹แˆ‹ ยซโœ… แ‰ฐแ‰€แˆ‹แ‰…แ‹ซแˆˆแˆยป แŠ•แŠฉแข",
4
+ "gate_join_btn": "๐Ÿ“ข แ‰ปแŠ“แˆ‰แŠ• แ‰ฐแ‰€แˆ‹แ‰€แˆ‰",
5
  "gate_check_btn": "โœ… แ‰ฐแ‰€แˆ‹แ‰…แ‹ซแˆˆแˆ",
6
+ "gate_not_yet": "แŒˆแŠ“ แŠ แˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‹แ‰ฝแˆแˆแค แˆ˜แŒ€แˆ˜แˆชแ‹ซ แ‰ฐแ‰€แˆ‹แ‰€แˆ‰แข",
7
+ "choose_stream": "แ‹˜แˆญแ‹แ‰ฝแˆ แ‹จแ‰ตแŠ›แ‹ แАแ‹? ๐ŸŽ“",
8
  "stream_natural": "๐Ÿ”ฌ แ‰ฐแˆแŒฅแˆฎ แˆณแ‹ญแŠ•แˆต",
9
  "stream_social": "๐Ÿ“– แˆ›แˆ…แ‰ แˆซแ‹Š แˆณแ‹ญแŠ•แˆต",
10
+ "choose_year": "แˆตแŠ•แ‰ฐแŠ› แ‹“แˆ˜แ‰ต แ‰ฐแˆ›แˆช แŠ“แ‰ฝแˆ?",
11
  "year_label": "{n}แŠ› แ‹“แˆ˜แ‰ต",
12
+ "onboarded": "โœ… แ‰ฐแˆ˜แ‹แŒแ‰ฃแ‰ฝแŠ‹แˆ! แŠจแ‰ณแ‰ฝ แŠซแˆˆแ‹ แˆœแŠ‘ แ‹จแˆแˆˆแŒ‹แ‰ฝแˆแ‰ตแŠ• แˆแˆจแŒก ๐Ÿ‘‡",
13
+ "menu_title": "แˆแŠ• แˆ‹แŒแ‹›แ‰ฝแˆ? ๐Ÿ‘‡",
14
+ "btn_library": "๐Ÿ“š แˆ›แ‰ดแˆชแ‹ซแˆŽแ‰ฝ",
15
  "btn_gpa": "๐Ÿงฎ GPA แˆ›แˆตแ‹ซ",
16
+ "btn_invite": "๐Ÿ‘ฅ แŒ“แ‹ฐแŠ› แŒ‹แ‰ฅแ‹™",
17
  "btn_lang": "๐ŸŒ แ‰‹แŠ•แ‰‹",
18
+ "soon": "๐Ÿšง แ‹ญแˆ„ แŒˆแŠ“ แŠฅแ‹จแ‰ฐแˆ แˆซ แАแ‹แฃ แ‰ แ‰…แˆญแ‰ก แ‹ญแ‹ฐแˆญแˆณแ‰ฝแŠ‹แˆ!",
19
+ "invite_text": "๐Ÿ”— แŒ“แ‹ฐแŠžแ‰ปแ‰ฝแˆแŠ• แ‹จแˆแ‰ตแŒ‹แ‰ฅแ‹™แ‰ แ‰ต แˆŠแŠ•แŠญแฆ\n{link}\n\n๐Ÿ‘ฅ แ‹จแŒ‹แ‰ แ‹›แ‰ฝแˆแ‰ตแฆ <b>{count}</b> ยท แ‰ แ‹ฐแŠ•แ‰ฅ แŠฅแ‹จแ‰ฐแŒ แ‰€แˆ™ แ‹ซแˆ‰แ‰ตแฆ <b>{qualified}</b>\nโญ XPแฆ <b>{points}</b>\n\nแŒ“แ‹ฐแŠ›แ‰ฝแˆ แ‰ขแ‹ซแŠ•แˆต แˆˆ3 แ‰€แŠ• แ‰ฆแ‰ฑแŠ• แŠจแ‰ฐแŒ แ‰€แˆ˜ แ‰ตแˆแ‰ แŒ‰แˆญแˆป แ‹ญแˆฐแŒฃแ‰ฝแŠ‹แˆ ๐ŸŽ",
20
+ "referral_reward": "๐Ÿ‘ฅ แŠ แŠ•แ‹ต แŒ“แ‹ฐแŠ›แ‰ฝแˆ แ‰ แˆŠแŠ•แŠซแ‰ฝแˆ แŒˆแ‰ฅแ‰ทแˆ! +{points} XP ๐ŸŽ‰\nแŒ“แ‹ฐแŠ›แ‰ฝแˆ แ‰ฆแ‰ฑแŠ• แˆ˜แŒ แ‰€แˆ แˆฒแ‰€แŒฅแˆ แ‰ฐแŒจแˆ›แˆช แ‰ตแ‰€แ‰ แˆ‹แˆ‹แ‰ฝแˆแข",
21
+ "lang_changed": "โœ… แŠฅแˆบแฃ แŠ แˆแŠ• แ‰ แŠ แˆ›แˆญแŠ› แАแ‹!",
22
+ "banned": "โ›” แŠ แŠซแ‹แŠ•แ‰ณแ‰ฝแˆ แ‰ณแŒแ‹ทแˆแข",
23
+ "btn_profile": "๐Ÿ‘ค แ•แˆฎแ‹แ‹ญแˆŒ",
24
  "btn_top": "๐Ÿ† แ‹ฐแˆจแŒƒ",
25
  "btn_back": "โ—€๏ธ แ‰ฐแˆ˜แˆˆแˆต",
26
  "btn_prev": "โฌ…๏ธ",
27
  "btn_next": "โžก๏ธ",
28
+ "kind_slide": "๐Ÿ“‘ แˆตแˆ‹แ‹ญแ‹ถแ‰ฝ",
29
+ "kind_exam": "๐Ÿ“ แ‹จแ‹ตแˆฎ แˆแ‰ฐแŠ“แ‹Žแ‰ฝ",
30
+ "kind_note": "๐Ÿ—’ แŠ–แ‰ต / แˆžแŒแˆ",
31
+ "kind_book": "๐Ÿ“˜ แˆ˜แŒปแˆ•แแ‰ต",
32
+ "lib_courses": "๐Ÿ“š <b>แŠฎแˆญแˆถแ‰ฝ</b>\nแŠฎแˆญแˆต แˆแˆจแŒกแฃ แ‹ˆแ‹ญแˆ แ‹จแŠฎแˆญแˆฑแŠ• แˆตแˆ แŒฝแ‹แ‰ฝแˆ แˆแˆแŒ‰ ๐Ÿ”",
33
+ "lib_no_courses": "แˆˆแ‹˜แˆญแ‹แ‰ฝแˆแŠ“ แˆˆแ‹“แˆ˜แ‰ณแ‰ฝแˆ แŒˆแŠ“ แˆ›แ‰ดแˆชแ‹ซแˆ แŠ แˆแŒˆแ‰ฃแˆแข\nแ‹จแŠฎแˆญแˆฑแŠ• แˆตแˆ แŒฝแ‹แ‰ฝแˆ แˆแˆแŒ‰ ๐Ÿ”",
34
+ "lib_kinds": "๐Ÿ“˜ <b>{course}</b>\nแˆแŠ• แˆแˆ‹แŠญแˆ‹แ‰ฝแˆ?",
35
+ "lib_no_materials": "แˆˆแ‹šแˆ… แŠฎแˆญแˆต แŒˆแŠ“ แˆแŠ•แˆ แˆ›แ‰ดแˆชแ‹ซแˆ แ‹จแˆˆแˆแค แ‰ แ‰…แˆญแ‰ก แŠฅแŠ•แŒจแˆแˆซแˆˆแŠ•!",
36
  "lib_materials": "๐Ÿ“˜ <b>{course}</b> ยท {kind}",
37
+ "file_missing": "แ‹ญแˆ„ แ‹แ‹ญแˆ แŠ แˆแ‰ฐแŒˆแŠ˜แˆ ๐Ÿ˜•",
38
+ "search_none": "๐Ÿ” แˆแŠ•แˆ แŠ แˆ‹แŒˆแŠ˜แˆแˆแข แˆŒแˆ‹ แ‰ƒแˆ แˆžแŠญแˆฉแฃ แˆˆแˆแˆณแˆŒ แ‹จแŠฎแˆญแˆฑแŠ• แˆตแˆ แ‹ˆแ‹ญแˆ แŠฎแ‹ตแข",
39
+ "search_results": "๐Ÿ” <b>แˆˆ ยซ{q}ยป แ‹ซแŒˆแŠ˜แˆแ‰ต</b>",
40
+ "profile_text": "๐Ÿ‘ค <b>{name}</b>\n๐ŸŽ“ {stream} ยท {year}\n\n๐Ÿ… แ‹ฐแˆจแŒƒ {level}แฆ <b>{title}</b>\nโญ แŒ แ‰…แˆ‹แˆ‹ XPแฆ <b>{points}</b> ({to_next})\n๐Ÿ“… แ‹จแ‹šแˆ… แˆณแˆแŠ•แ‰ต XPแฆ <b>{week_xp}</b> ยท {week_rank}\n๐Ÿ”ฅ แ‰ฐแŠจแ‰ณแ‰ณแ‹ญ แ‰€แŠ“แ‰ตแฆ <b>{streak}</b> (แŠจแแ‰ฐแŠ›แ‹ {best})\n๐Ÿ‘ฅ แ‹จแŒ‹แ‰ แ‹›แ‰ฝแŠ‹แ‰ธแ‹แฆ <b>{refs}</b> (แŠ•แ‰ {refs_q})",
41
+ "top_text": "๐Ÿ† <b>แ‹จแˆณแˆแŠ•แ‰ฑ แ‹ฐแˆจแŒƒ</b> ยท {days} แ‰€แŠ• แ‰€แˆญแ‰ทแˆ\n{rows}\n\n๐Ÿ“ แŠฅแŠ“แŠ•แ‰ฐแฆ <b>{rank}</b> ยท {week_xp} XP\nโš”๏ธ {n_label}แฆ <b>{natural}</b> ยท {s_label}แฆ <b>{social}</b>{prize}\n\nโ„น๏ธ แˆ…แŒŽแ‰นแฆ /rules",
42
+ "inline_open_bot": "๐Ÿ“š แ‰ฆแ‰ฑแŠ• แŠญแˆแ‰ฑ",
43
+ "prize_block": "\n\n๐Ÿ’ฐ <b>แ‹จแˆณแˆแŠ•แ‰ฑ แˆฝแˆแˆ›แ‰ต</b> (แ‹จแˆžแ‰ฃแ‹ญแˆ แŠซแˆญแ‹ต)แฆ\n{lines}\n๐ŸŽŸ แŠฅแŒฃแฆ {raffle} แ‰ฅแˆญ (แˆˆแ‰€แˆฉแ‰ต แ‰ฅแ‰ แ‰ฐแ‹ˆแ‹ณแ‹ณแˆชแ‹Žแ‰ฝ)\nแˆˆแˆ˜แ‹ˆแ‹ณแ‹ฐแˆญแฆ แ‰ แˆณแˆแŠ•แ‰ฑ แ‰ขแ‹ซแŠ•แˆต {days} แ‰€แŠ• แˆ˜แŒแ‰ฃแ‰ตแŠ“ {xp} XP แˆ˜แˆฐแ‰ฅแˆฐแ‰ฅ แ‹ซแˆตแˆแˆแŒ‹แˆ",
44
+ "to_next": "แˆˆแ‰€แŒฃแ‹ฉ แ‹ฐแˆจแŒƒ {n} XP แ‰€แˆญแ‰ทแˆ",
45
+ "max_level": "แŠจแแ‰ฐแŠ›แ‹ แ‹ฐแˆจแŒƒ แˆ‹แ‹ญ แ‹ฐแˆญแˆณแ‰ฝแŠ‹แˆ ๐Ÿ†",
46
+ "levelup": "๐ŸŽ‰ แ‹ฐแˆจแŒƒ {level} แˆ‹แ‹ญ แ‹ฐแˆญแˆณแ‰ฝแŠ‹แˆแฆ <b>{title}</b>!",
47
+ "streak_bonus": "๐Ÿ”ฅ {streak} แ‰€แŠ• แˆณแ‰ณแ‰‹แˆญแŒก แˆ˜แŒฅแ‰ณแ‰ฝแŠ‹แˆ! +{xp} XP แŒ‰แˆญแˆป",
48
+ "referral_qualified": "๐ŸŽ‰ แ‹จแŒ‹แ‰ แ‹›แ‰ฝแˆแ‰ต แŒ“แ‹ฐแŠ› แ‰ฆแ‰ฑแŠ• แ‰ แ‹ฐแŠ•แ‰ฅ แŠฅแ‹จแ‰ฐแŒ แ‰€แˆ˜ แАแ‹! +{xp} XP",
49
+ "season_win": "๐Ÿ… แ‰ฃแˆˆแˆแ‹ แˆณแˆแŠ•แ‰ต #{rank} แ‹ˆแŒฅแ‰ณแ‰ฝแŠ‹แˆ ({xp} XP)! แŠฅแŠ•แŠณแŠ• แ‹ฐแˆต แŠ แˆ‹แ‰ฝแˆ ๐ŸŽ‰",
50
+ "season_win_prize": "\n๐Ÿ’ฐ แˆฝแˆแˆ›แ‰ณแ‰ฝแˆแฆ {birr} แ‰ฅแˆญ แ‹จแˆžแ‰ฃแ‹ญแˆ แŠซแˆญแ‹ตแข แŠ แ‹ตแˆšแŠ‘ แ‰  Telegram แ‹ซแŒˆแŠ›แ‰ฝแŠ‹แˆแข",
51
+ "season_raffle": "๐ŸŽŸ แ‰ แˆณแˆแŠ•แ‰ณแ‹Šแ‹ แŠฅแŒฃ แŠ แˆธแŠ•แ‹๏ฟฝ๏ฟฝ๏ฟฝแŠ‹แˆ! ๐ŸŽ‰",
52
+ "rules_text": "๐Ÿ“œ <b>แˆ…แŒŽแ‰ฝ</b>\n\n<b>XP แŠฅแŠ•แ‹ดแ‰ต แ‹ญแŒˆแŠ›แˆ?</b>\nโ€ข แ‰ แ‹จแ‰€แŠ‘ แ‰ฆแ‰ฑแŠ• แˆตแ‰ตแŠจแแ‰ฑแฆ 3-7 XP (แ‰€แŠ• แˆณแ‰ณแ‰‹แˆญแŒก แˆตแ‰ตแˆ˜แŒก แ‹ญแŒจแˆแˆซแˆ)\nโ€ข แ‹แ‹ญแˆ แˆตแ‰ณแ‹ˆแˆญแ‹ฑแฆ +1 (แ‰ แ‰€แŠ• แŠฅแˆตแŠจ 3)\nโ€ข แŒ“แ‹ฐแŠ› แˆตแ‰ตแŒ‹แ‰ฅแ‹™แฆ แˆฒแŒˆแ‰ฃ +5แฃ แ‰ขแ‹ซแŠ•แˆต แˆˆ3 แ‰€แŠ• แŠจแ‰ฐแŒ แ‰€แˆ˜ +30 (แ‰ แˆณแˆแŠ•แ‰ต แŠฅแˆตแŠจ 5 แŒ“แ‹ฐแŠ›)\nโ€ข แˆˆแˆšแŒ แ‰…แˆ แŠ แˆตแ‰ฐแ‹‹แŒฝแŠฆ แŠ แ‹ตแˆšแŠ‘ แˆแ‹ฉ XP แˆŠแˆฐแŒฅ แ‹ญแ‰ฝแˆ‹แˆ\nโ€ข แ‰ แˆซแˆต-แˆฐแˆญ แ‹จแˆšแŒˆแŠ XP แ‰ แ‰€แŠ• แŠจ{cap} แŠ แ‹ญแ‰ แˆแŒฅแˆ\n\n<b>แ‹จแˆณแˆแŠ•แ‰ฑ แ‹แ‹ตแ‹ตแˆญ</b>\nแˆฐแŠž แ‹ญแŒ€แˆแˆซแˆแฃ แŠฅแˆแ‹ต แ‹ญแ‹˜แŒ‹แˆ (แ‰ แŠขแ‰ตแ‹ฎแŒตแ‹ซ แˆฐแ‹“แ‰ต)แข\nแˆฝแˆแˆ›แ‰ต แˆฒแŠ–แˆญ แˆˆแˆ˜แ‹ˆแ‹ณแ‹ฐแˆญแฆ แ‰ แˆณแˆแŠ•แ‰ฑ แ‰ขแ‹ซแŠ•แˆต {days} แ‰€แŠ• แˆ˜แŒแ‰ฃแ‰ตแฃ แ‰ขแ‹ซแŠ•แˆต {xp} XP แˆ›แŒแŠ˜แ‰ตแฃ แ‰ปแŠ“แˆ‰ แ‹แˆตแŒฅ แˆ˜แ‰†แ‹จแ‰ตแฃ แŠฅแŠ“ แ‹จแ‰ณแˆ›แŠแАแ‰ต แАแŒฅแ‰ฃแ‰ฝแˆ แŒคแŠ“แˆ› แˆ˜แˆ†แŠ• แŠ แˆˆแ‰ แ‰ตแข\n\n<b>แแ‰ตแˆƒแ‹ŠแАแ‰ต</b>\nโ€ข แŠฅแ‹ซแŠ•แ‹ณแŠ•แ‹ฑ XP แ‰ แˆ›แ‹ญแˆˆแ‹ˆแŒฅ แˆ˜แ‹แŒˆแ‰ฅ แ‹ญแˆ˜แ‹˜แŒˆแ‰ฃแˆแค แŠ แŠ•แ‹ต แ‹ตแˆญแŒŠแ‰ต แˆแˆˆแ‰ต แŒŠแ‹œ แŠ แ‹ญแŠจแˆแˆแˆแข\nโ€ข แŠ แŒ แˆซแŒฃแˆช แАแŒˆแˆญ (แ‹จแ‹แˆธแ‰ต แŠ แŠซแ‹แŠ•แ‰ตแฃ แ‹ซแˆแ‰ฐแˆˆแˆ˜แ‹ฐ แ‰ฅแ‹™ แŒ‹แ‰ฃแ‹ฅแАแ‰ต) แŠ แ‹ตแˆšแŠ‘ แ‹ญแˆ˜แˆจแˆแˆจแ‹‹แˆแค แАแŒฅแ‰ฅ แˆŠแ‰€แАแˆต แ‹ˆแ‹ญแˆ แŠ แŠซแ‹แŠ•แ‰ต แˆŠแ‰ณแŒˆแ‹ต แ‹ญแ‰ฝแˆ‹แˆแข\nโ€ข แˆฝแˆแˆ›แ‰ถแ‰ฝ แˆŠแ‰€แ‹จแˆฉ แ‹ญแ‰ฝแˆ‹แˆ‰แค แ‹จแŠ แ‹ตแˆšแŠ‘ แ‹แˆณแŠ” แ‹จแˆ˜แŒจแˆจแˆป แАแ‹แข\nโ€ข แˆˆแˆ˜แˆณแ‰ฐแ แˆแŠ•แˆ แŠญแแ‹ซ แŠ แ‹ซแˆตแˆแˆแŒแˆแข",
53
+ "level_1": "แŠ แ‹ฒแˆต แŒˆแ‰ข",
54
  "level_2": "แŒ€แˆ›แˆช",
55
+ "level_3": "แŒŽแ‰ แ‹ แ‰ฐแˆ›แˆช",
56
+ "level_4": "แ‰ตแŒ‰",
57
+ "level_5": "แ‰ฅแˆแŒฅ",
58
  "level_6": "แˆแˆแˆญ",
59
  "level_7": "แŠ แˆ›แŠซแˆช",
60
  "level_8": "แŠฎแŠจแ‰ฅ",
61
+ "level_9": "แˆปแˆแ’แ‹ฎแŠ•",
62
  "level_10": "แˆŠแ‰…",
63
+ "gate_missing": "แŒˆแŠ“ แ‹ซแˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‹แ‰ฝแˆแ‰ตแฆ {list}",
64
+ "gate_join_one": "๐Ÿ“ข {name} แ‰ฐแ‰€แˆ‹แ‰€แˆ‰",
65
+ "gate_not_yet_list": "แŒˆแŠ“ แŠ แˆแ‰ฐแ‰€แˆ‹แ‰€แˆ‹แ‰ฝแˆแˆแฆ {list}"
66
  }
tests/test_logic.py CHANGED
@@ -338,13 +338,29 @@ def test_display_title_cleans_old_titles_but_not_manual_ones():
338
 
339
 
340
  def test_course_label_fits_one_line():
341
- from app.bot.keyboards.library import course_label
342
 
343
- assert course_label(NS(code="Math1003", name="Calculus")) == "Math1003 ยท Calculus"
344
- long_ = course_label(NS(code="FLEn1001", name="Communicative English Language Skills I"))
 
345
  assert len(long_) <= 36 and long_.startswith("Communicative English")
346
 
347
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  if __name__ == "__main__":
349
  for name, fn in list(globals().items()):
350
  if name.startswith("test_"):
 
338
 
339
 
340
  def test_course_label_fits_one_line():
341
+ from app.core.labels import course_label
342
 
343
+ assert course_label("Math1003", "Calculus") == "Math1003 ยท Calculus"
344
+ assert course_label(None, "General Biology") == "General Biology"
345
+ long_ = course_label("FLEn1001", "Communicative English Language Skills I")
346
  assert len(long_) <= 36 and long_.startswith("Communicative English")
347
 
348
 
349
+ def test_locales_stay_in_sync():
350
+ """Every Amharic string must keep the same keys, {placeholders} and HTML tags as the English one,
351
+ otherwise the bot crashes (KeyError) the first time that message is sent."""
352
+ import json
353
+ import re
354
+
355
+ root = Path(__file__).resolve().parents[1] / "locales"
356
+ am = json.loads((root / "am.json").read_text(encoding="utf-8"))
357
+ en = json.loads((root / "en.json").read_text(encoding="utf-8"))
358
+ assert set(am) == set(en)
359
+ for key in en:
360
+ assert sorted(re.findall(r"\{(\w+)\}", am[key])) == sorted(re.findall(r"\{(\w+)\}", en[key])), key
361
+ assert sorted(re.findall(r"</?\w+>", am[key])) == sorted(re.findall(r"</?\w+>", en[key])), key
362
+
363
+
364
  if __name__ == "__main__":
365
  for name, fn in list(globals().items()):
366
  if name.startswith("test_"):