Asosiy kontentga o‘tish

Maqolalar

Maqola · 2026-08-16 · ~8 daqiqa o‘qiladi · o'rta

Bot uch tilda gapiradi: i18n'ni murakkablashtirmasdan qurish

#python #aiogram #telegram-bot #i18n

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.