Maqola · 2026-08-16 · ~8 daqiqa o‘qiladi · o'rta
Bot uch tilda gapiradi: i18n'ni murakkablashtirmasdan qurish
Mundarija
O'zbekiston bozori uchun yozilgan bot amalda kamida ikki tilda gapirishi kerak, Aladdin botida esa uchta til bor: o'zbek, rus va ingliz. Ko'p tillilikni birinchi marta qo'shayotgan dasturchi odatda gettext, .po fayllar va kompilyatsiya dunyosiga kirib ketadi — vaholanki bot uchun bundan ancha sodda yo'l bor va u yillab bemalol xizmat qiladi. Aladdin'da ishlatilgan yondashuv aynan shunday: oddiy Python lug'atlari.
Boshlashdan oldin bitta muhim ajratishni aniqlab olaylik. Botdagi matnlar ikki xil bo'ladi: interfeys matnlari (tugmalar, xizmat xabarlari — ular kodda yashaydi) va kontent (tovar nomlari, tavsiflar — ular backend'dan keladi). Ikkalasiga yechim boshqa-boshqa, quyida ikkalasini ham ko'ramiz.
1-qadam: tarjima lug'atlari#
bot/
├── locals/
│ ├── __init__.py ← t() funksiyasi
│ ├── uz.py
│ ├── ru.py
│ └── en.py
├── middlewares/
│ └── language.py ← har handler'ga tilni yetkazadi
└── handlers/
└── language.py ← til tanlash ekrani
Har til — bitta oddiy lug'at. Kalitlar inglizcha va ma'noli bo'lsin, chunki kod ichida aynan kalit ko'rinadi:
# bot/locals/uz.py
STRINGS = {
"choose_category": "Bo'limni tanlang:",
"back": "← Ortga",
"price": "Narxi: {price} so'm",
"catalog_error": "Katalog vaqtincha ishlamayapti, birozdan keyin urinib ko'ring.",
}
# bot/locals/ru.py
STRINGS = {
"choose_category": "Выберите раздел:",
"back": "← Назад",
"price": "Цена: {price} сум",
"catalog_error": "Каталог временно недоступен, попробуйте позже.",
}
Endi ularni bitta funksiya ortiga yig'amiz. Eng muhim joyi — fallback zanjiri: tarjima topilmasa bot yiqilmaydi, o'zbekchasini ko'rsatadi:
# bot/locals/__init__.py
from . import en, ru, uz
LANGUAGES = {"uz": uz.STRINGS, "ru": ru.STRINGS, "en": en.STRINGS}
DEFAULT = "uz"
def t(key: str, lang: str, **kwargs) -> str:
"""Tarjimani qaytaradi; topilmasa o'zbekchaga, u ham bo'lmasa kalitga tushadi.
Yangi til qo'shilganda hamma satr birdan tarjima qilinmagan bo'lishi
normal holat — shunda foydalanuvchi bo'sh joy emas, hech bo'lmasa
o'zbekcha matnni ko'radi.
"""
table = LANGUAGES.get(lang, LANGUAGES[DEFAULT])
text = table.get(key) or LANGUAGES[DEFAULT].get(key) or key
return text.format(**kwargs) if kwargs else text
Tarjima yetishmasa jim qolmasin
t() kalitni topolmasa kalitning o'zini qaytaradi — bu ataylab qilingan:
ekranda catalog_error degan "xom" yozuv chiqsa, xato bir qarashda
ko'rinadi va tez tuzatiladi. Bo'sh satr qaytarilsa, muammo oylab
sezilmay yurishi mumkin edi.
2-qadam: foydalanuvchi tilini eslab qolish#
Til — sessiya emas, doimiy tanlov. Shuning uchun uni xotirada emas,
foydalanuvchi yozuvida saqlaymiz (Aladdin'da bu SQLite'dagi User jadvali):
# bot/handlers/language.py
from aiogram import F, Router, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from bot.db import users # user yozuvini o'qish/yozish qatlami
router = Router()
LANG_KB = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="O'zbekcha", callback_data="lang:uz"),
InlineKeyboardButton(text="Русский", callback_data="lang:ru"),
InlineKeyboardButton(text="English", callback_data="lang:en"),
]
]
)
@router.message(F.text == "/start")
async def start(message: types.Message):
# birinchi kirishda til so'raladi; keyin bu ekran /language bilan ochiladi
await message.answer("Tilni tanlang / Выберите язык / Choose language:", reply_markup=LANG_KB)
@router.callback_query(F.data.startswith("lang:"))
async def set_language(callback: types.CallbackQuery):
lang = callback.data.split(":")[1]
users.update(callback.from_user.id, language=lang)
await callback.answer()
await callback.message.edit_text(t("choose_category", lang))
Har handler'da bazadan tilni qo'lda o'qib yurmaslik uchun kichik middleware yozamiz — u tilni bir marta olib, handler'ga tayyor uzatadi:
# bot/middlewares/language.py
from aiogram import BaseMiddleware
from bot.db import users
from bot.locals import DEFAULT
class LanguageMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
user = users.get(event.from_user.id)
# data'ga qo'shilgan narsa handler'ga argument bo'lib keladi
data["lang"] = user.language if user else DEFAULT
return await handler(event, data)
Endi istalgan handler tilni shunchaki parametr sifatida oladi:
@router.callback_query(F.data == "menu")
async def screen_menu(callback: types.CallbackQuery, lang: str):
categories = await catalog.categories()
await show_screen(callback, t("choose_category", lang), categories_kb(categories, lang))
Tugmalarga ham e'tibor bering: avvalgi qismdagi
"← Ortga" tugmasi endi t("back", lang) bilan yasaladi — interfeysning
birorta burchagi tarjimasiz qolmasligi kerak.
3-qadam: kontent tili — backend bilan kelishuv#
Interfeys hal bo'ldi, endi tovar nomlari. Ular bot kodida emas, backend bazasida turadi, demak tarjima ham o'sha yerda bo'lishi kerak. Amalda ikki keng tarqalgan sxema bor:
Ustunlar bilan: name_uz, name_ru, name_en — sodda, admin-panelda
uchta maydon ko'rinadi, menejer uchun tushunarli. Aladdin shu yo'ldan borgan.
JSONField bilan: name = {"uz": ..., "ru": ..., "en": ...} — Metro
loyihasida shu tanlangan, sabablarini mobil backend haqidagi
maqolada yozganman.
Qaysi biri bo'lmasin, botga qulayi — backend hamma tilni birdan qaytarsin, tanlashni bot qilsin:
# bot/utils/content.py
def pick(obj: dict, field: str, lang: str) -> str:
"""Backend javobidan kerakli til maydonini oladi: pick(product, "name", "ru").
Fallback zanjiri interfeys bilan bir xil: so'ralgan til → o'zbekcha → bo'sh.
Yarim tarjima qilingan tovar rus tilida bo'sh ko'rinmasligi kerak.
"""
return obj.get(f"{field}_{lang}") or obj.get(f"{field}_uz") or ""
Nega tanlashni backend emas, bot qiladi? Chunki bitta so'rov bilan hamma til kelsa, botdagi 60 soniyalik kesh ham tilga bog'liq bo'lmay qoladi — uch til uchun uchta alohida kesh saqlash shart emas.
4-qadam: test#
# tests/test_i18n.py
from bot.locals import t
from bot.utils.content import pick
def test_known_translation():
assert t("back", "ru") == "← Назад"
def test_missing_lang_falls_back_to_uz():
assert t("back", "de") == "← Ortga"
def test_missing_key_returns_key():
assert t("no_such_key", "uz") == "no_such_key"
def test_content_fallback():
product = {"name_uz": "Karusel", "name_ru": ""}
assert pick(product, "name", "ru") == "Karusel"
Xulosa#
Bot uchun i18n'ning siri soddalikda: har til bitta lug'at, ularning ustida
fallback'li t() funksiyasi, foydalanuvchi tili bazada, middleware esa uni
handler'larga yetkazib turadi. Kontent tarjimasi backend'da yashaydi va
hamma til bitta javobda keladi — tanlash botning ishi. Eng muhim qoida esa
fallback: tarjima yetishmasa foydalanuvchi bo'sh ekran emas, hech bo'lmasa
o'zbekcha matnni ko'rsin.
Seriyaning oxirgi qismida botni serverga chiqaramiz: systemd bilan 24/7 ishlash, yiqilganda o'zi turish va reboot'dan keyin ham avtomatik ko'tarilish.