# Translation API with default dictionaries from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import or_ from typing import Optional, List from ..database import get_db from ..models import Translation from ..schemas import ( TranslationCreate, TranslationUpdate, TranslationResponse, TranslationListResponse, TranslationBulkRequest, TranslationBulkResponse, ) router = APIRouter(prefix="/translations", tags=["translations"]) # Categories for translations TRANSLATION_CATEGORIES = [ "maker", # Car makers (Hyundai, Kia, etc.) "model", # Car models (Sonata, K5, etc.) "fuel", # Fuel types (가솔린, 디젤, etc.) "transmission", # Transmission types (자동, 수동, etc.) "color", # Colors (흰색, 검정색, etc.) "car_name", # Full car names "general", # General terms ] # Default translation dictionary for common Korean terms DEFAULT_TRANSLATIONS = { # Fuel types (연료) "fuel": { "가솔린": {"en": "Gasoline", "mn": "Бензин", "ru": "Бензин"}, "휘발유": {"en": "Gasoline", "mn": "Бензин", "ru": "Бензин"}, "디젤": {"en": "Diesel", "mn": "Дизель", "ru": "Дизель"}, "경유": {"en": "Diesel", "mn": "Дизель", "ru": "Дизель"}, "LPG": {"en": "LPG", "mn": "LPG", "ru": "Газ"}, "가스": {"en": "LPG", "mn": "LPG", "ru": "Газ"}, "전기": {"en": "Electric", "mn": "Цахилгаан", "ru": "Электро"}, "하이브리드": {"en": "Hybrid", "mn": "Хайбрид", "ru": "Гибрид"}, "플러그인하이브리드": {"en": "Plug-in Hybrid", "mn": "Плаг-ин Хайбрид", "ru": "Плагин гибрид"}, "수소": {"en": "Hydrogen", "mn": "Устөрөгч", "ru": "Водород"}, "CNG": {"en": "CNG", "mn": "CNG", "ru": "Метан"}, }, # Transmission types (변속기) "transmission": { "자동": {"en": "Automatic", "mn": "Автомат", "ru": "Автомат"}, "오토": {"en": "Automatic", "mn": "Автомат", "ru": "Автомат"}, "수동": {"en": "Manual", "mn": "Механик", "ru": "Механика"}, "스틱": {"en": "Manual", "mn": "Механик", "ru": "Механика"}, "CVT": {"en": "CVT", "mn": "CVT", "ru": "Вариатор"}, "무단변속기": {"en": "CVT", "mn": "CVT", "ru": "Вариатор"}, "DCT": {"en": "DCT", "mn": "DCT", "ru": "Робот"}, "듀얼클러치": {"en": "Dual Clutch", "mn": "Давхар холбоо", "ru": "Робот"}, "세미오토": {"en": "Semi-Auto", "mn": "Хагас автомат", "ru": "Полуавтомат"}, }, # Colors (색상) "color": { "흰색": {"en": "White", "mn": "Цагаан", "ru": "Белый"}, "화이트": {"en": "White", "mn": "Цагаан", "ru": "Белый"}, "백색": {"en": "White", "mn": "Цагаан", "ru": "Белый"}, "검정": {"en": "Black", "mn": "Хар", "ru": "Чёрный"}, "검정색": {"en": "Black", "mn": "Хар", "ru": "Чёрный"}, "블랙": {"en": "Black", "mn": "Хар", "ru": "Чёрный"}, "은색": {"en": "Silver", "mn": "Мөнгөлөг", "ru": "Серебристый"}, "실버": {"en": "Silver", "mn": "Мөнгөлөг", "ru": "Серебристый"}, "회색": {"en": "Gray", "mn": "Саарал", "ru": "Серый"}, "그레이": {"en": "Gray", "mn": "Саарал", "ru": "Серый"}, "진회색": {"en": "Dark Gray", "mn": "Бараан саарал", "ru": "Тёмно-серый"}, "빨강": {"en": "Red", "mn": "Улаан", "ru": "Красный"}, "빨간색": {"en": "Red", "mn": "Улаан", "ru": "Красный"}, "레드": {"en": "Red", "mn": "Улаан", "ru": "Красный"}, "파랑": {"en": "Blue", "mn": "Хөх", "ru": "Синий"}, "파란색": {"en": "Blue", "mn": "Хөх", "ru": "Синий"}, "블루": {"en": "Blue", "mn": "Хөх", "ru": "Синий"}, "남색": {"en": "Navy", "mn": "Хар хөх", "ru": "Тёмно-синий"}, "네이비": {"en": "Navy", "mn": "Хар хөх", "ru": "Тёмно-синий"}, "하늘색": {"en": "Sky Blue", "mn": "Тэнгэрийн хөх", "ru": "Голубой"}, "녹색": {"en": "Green", "mn": "Ногоон", "ru": "Зелёный"}, "초록색": {"en": "Green", "mn": "Ногоон", "ru": "Зелёный"}, "그린": {"en": "Green", "mn": "Ногоон", "ru": "Зелёный"}, "노랑": {"en": "Yellow", "mn": "Шар", "ru": "Жёлтый"}, "노란색": {"en": "Yellow", "mn": "Шар", "ru": "Жёлтый"}, "옐로우": {"en": "Yellow", "mn": "Шар", "ru": "Жёлтый"}, "주황색": {"en": "Orange", "mn": "Улбар шар", "ru": "Оранжевый"}, "오렌지": {"en": "Orange", "mn": "Улбар шар", "ru": "Оранжевый"}, "갈색": {"en": "Brown", "mn": "Бор", "ru": "Коричневый"}, "브라운": {"en": "Brown", "mn": "Бор", "ru": "Коричневый"}, "베이지": {"en": "Beige", "mn": "Бээж", "ru": "Бежевый"}, "아이보리": {"en": "Ivory", "mn": "Зүс өнгө", "ru": "Слоновая кость"}, "진주색": {"en": "Pearl", "mn": "Сувдан", "ru": "Перламутр"}, "펄화이트": {"en": "Pearl White", "mn": "Сувдан цагаан", "ru": "Перламутровый белый"}, "보라색": {"en": "Purple", "mn": "Ягаан", "ru": "Фиолетовый"}, "퍼플": {"en": "Purple", "mn": "Ягаан", "ru": "Фиолетовый"}, "분홍색": {"en": "Pink", "mn": "Ягаан", "ru": "Розовый"}, "핑크": {"en": "Pink", "mn": "Ягаан", "ru": "Розовый"}, "와인": {"en": "Wine", "mn": "Дарс өнгө", "ru": "Бордовый"}, "버건디": {"en": "Burgundy", "mn": "Бургунд", "ru": "Бургунди"}, "골드": {"en": "Gold", "mn": "Алтан", "ru": "Золотой"}, "금색": {"en": "Gold", "mn": "Алтан", "ru": "Золотой"}, "샴페인": {"en": "Champagne", "mn": "Шампан", "ru": "Шампань"}, "청색": {"en": "Blue", "mn": "Хөх", "ru": "Синий"}, "연두색": {"en": "Light Green", "mn": "Цайвар ногоон", "ru": "Светло-зелёный"}, "민트": {"en": "Mint", "mn": "Минт", "ru": "Мятный"}, "카키": {"en": "Khaki", "mn": "Хаки", "ru": "Хаки"}, "올리브": {"en": "Olive", "mn": "Чидун ногоон", "ru": "Оливковый"}, "투톤": {"en": "Two-Tone", "mn": "Хоёр өнгө", "ru": "Двухцветный"}, "기타": {"en": "Other", "mn": "Бусад", "ru": "Другой"}, }, # Car makers (제조사) "maker": { "현대": {"en": "Hyundai", "mn": "Хёндай", "ru": "Хендай"}, "기아": {"en": "Kia", "mn": "Киа", "ru": "Киа"}, "제네시스": {"en": "Genesis", "mn": "Женезис", "ru": "Дженезис"}, "쉐보레": {"en": "Chevrolet", "mn": "Шевроле", "ru": "Шевроле"}, "르노삼성": {"en": "Renault Samsung", "mn": "Рено Самсунг", "ru": "Рено Самсунг"}, "르노코리아": {"en": "Renault Korea", "mn": "Рено Солонгос", "ru": "Рено Корея"}, "쌍용": {"en": "SsangYong", "mn": "Ссанёнг", "ru": "СсангЙонг"}, "KG모빌리티": {"en": "KG Mobility", "mn": "KG Мобилити", "ru": "КГ Мобилити"}, "BMW": {"en": "BMW", "mn": "BMW", "ru": "БМВ"}, "벤츠": {"en": "Mercedes-Benz", "mn": "Мерседес-Бенз", "ru": "Мерседес"}, "메르세데스벤츠": {"en": "Mercedes-Benz", "mn": "Мерседес-Бенз", "ru": "Мерседес"}, "아우디": {"en": "Audi", "mn": "Ауди", "ru": "Ауди"}, "폭스바겐": {"en": "Volkswagen", "mn": "Фольксваген", "ru": "Фольксваген"}, "토요타": {"en": "Toyota", "mn": "Тойота", "ru": "Тойота"}, "혼다": {"en": "Honda", "mn": "Хонда", "ru": "Хонда"}, "닛산": {"en": "Nissan", "mn": "Ниссан", "ru": "Ниссан"}, "렉서스": {"en": "Lexus", "mn": "Лексус", "ru": "Лексус"}, "인피니티": {"en": "Infiniti", "mn": "Инфинити", "ru": "Инфинити"}, "마쯔다": {"en": "Mazda", "mn": "Мазда", "ru": "Мазда"}, "스바루": {"en": "Subaru", "mn": "Субару", "ru": "Субару"}, "미쓰비시": {"en": "Mitsubishi", "mn": "Мицубиши", "ru": "Митсубиши"}, "포드": {"en": "Ford", "mn": "Форд", "ru": "Форд"}, "링컨": {"en": "Lincoln", "mn": "Линкольн", "ru": "Линкольн"}, "캐딜락": {"en": "Cadillac", "mn": "Кадиллак", "ru": "Кадиллак"}, "지프": {"en": "Jeep", "mn": "Жийп", "ru": "Джип"}, "크라이슬러": {"en": "Chrysler", "mn": "Крайслер", "ru": "Крайслер"}, "테슬라": {"en": "Tesla", "mn": "Тесла", "ru": "Тесла"}, "볼보": {"en": "Volvo", "mn": "Вольво", "ru": "Вольво"}, "포르쉐": {"en": "Porsche", "mn": "Порше", "ru": "Порше"}, "재규어": {"en": "Jaguar", "mn": "Ягуар", "ru": "Ягуар"}, "랜드로버": {"en": "Land Rover", "mn": "Лэнд Ровер", "ru": "Лэнд Ровер"}, "미니": {"en": "Mini", "mn": "Мини", "ru": "Мини"}, "페라리": {"en": "Ferrari", "mn": "Феррари", "ru": "Феррари"}, "람보르기니": {"en": "Lamborghini", "mn": "Ламборгини", "ru": "Ламборгини"}, "벤틀리": {"en": "Bentley", "mn": "Бентли", "ru": "Бентли"}, "롤스로이스": {"en": "Rolls-Royce", "mn": "Роллс-Ройс", "ru": "Роллс-Ройс"}, "마세라티": {"en": "Maserati", "mn": "Мазерати", "ru": "Мазерати"}, "알파로메오": {"en": "Alfa Romeo", "mn": "Альфа Ромео", "ru": "Альфа Ромео"}, "피아트": {"en": "Fiat", "mn": "Фиат", "ru": "Фиат"}, "푸조": {"en": "Peugeot", "mn": "Пежо", "ru": "Пежо"}, "시트로엥": {"en": "Citroen", "mn": "Ситроен", "ru": "Ситроен"}, "르노": {"en": "Renault", "mn": "Рено", "ru": "Рено"}, "스즈키": {"en": "Suzuki", "mn": "Сузуки", "ru": "Сузуки"}, "다이하쓰": {"en": "Daihatsu", "mn": "Дайхатсу", "ru": "Дайхатсу"}, "이스즈": {"en": "Isuzu", "mn": "Исузу", "ru": "Исузу"}, "GMC": {"en": "GMC", "mn": "GMC", "ru": "ДжиЭмСи"}, "대우": {"en": "Daewoo", "mn": "Дэу", "ru": "Дэу"}, "아큐라": {"en": "Acura", "mn": "Акура", "ru": "Акура"}, "뷰익": {"en": "Buick", "mn": "Бюик", "ru": "Бьюик"}, "스마트": {"en": "Smart", "mn": "Смарт", "ru": "Смарт"}, "BYD": {"en": "BYD", "mn": "BYD", "ru": "БИД"}, }, # Car models (차량 모델) - These generally keep their names but with transliteration "model": { # Hyundai models "소나타": {"en": "Sonata", "mn": "Соната", "ru": "Соната"}, "아반떼": {"en": "Avante", "mn": "Аванте", "ru": "Аванте"}, "그랜저": {"en": "Grandeur", "mn": "Грандер", "ru": "Грандер"}, "싼타페": {"en": "Santa Fe", "mn": "Санта Фе", "ru": "Санта Фе"}, "투싼": {"en": "Tucson", "mn": "Туксон", "ru": "Туксон"}, "팰리세이드": {"en": "Palisade", "mn": "Палисейд", "ru": "Палисад"}, "스타리아": {"en": "Staria", "mn": "Стариа", "ru": "Стариа"}, "아이오닉": {"en": "Ioniq", "mn": "Ионик", "ru": "Ионик"}, "코나": {"en": "Kona", "mn": "Кона", "ru": "Кона"}, "베뉴": {"en": "Venue", "mn": "Венью", "ru": "Венью"}, "넥쏘": {"en": "Nexo", "mn": "Нексо", "ru": "Нексо"}, "캐스퍼": {"en": "Casper", "mn": "Каспер", "ru": "Каспер"}, "엘란트라": {"en": "Elantra", "mn": "Элантра", "ru": "Элантра"}, "액센트": {"en": "Accent", "mn": "Акцент", "ru": "Акцент"}, "벨로스터": {"en": "Veloster", "mn": "Велостер", "ru": "Велостер"}, "i30": {"en": "i30", "mn": "i30", "ru": "i30"}, "i40": {"en": "i40", "mn": "i40", "ru": "i40"}, # Kia models "쏘렌토": {"en": "Sorento", "mn": "Соренто", "ru": "Соренто"}, "스포티지": {"en": "Sportage", "mn": "Спортейж", "ru": "Спортейдж"}, "카니발": {"en": "Carnival", "mn": "Карнивал", "ru": "Карнивал"}, "셀토스": {"en": "Seltos", "mn": "Сельтос", "ru": "Селтос"}, "모하비": {"en": "Mohave", "mn": "Мохаве", "ru": "Мохаве"}, "니로": {"en": "Niro", "mn": "Ниро", "ru": "Ниро"}, "스팅어": {"en": "Stinger", "mn": "Стингер", "ru": "Стингер"}, "레이": {"en": "Ray", "mn": "Рэй", "ru": "Рэй"}, "모닝": {"en": "Morning", "mn": "Морнинг", "ru": "Морнинг"}, "쏘울": {"en": "Soul", "mn": "Соул", "ru": "Соул"}, "K3": {"en": "K3", "mn": "K3", "ru": "K3"}, "K5": {"en": "K5", "mn": "K5", "ru": "K5"}, "K7": {"en": "K7", "mn": "K7", "ru": "K7"}, "K8": {"en": "K8", "mn": "K8", "ru": "K8"}, "K9": {"en": "K9", "mn": "K9", "ru": "K9"}, "EV6": {"en": "EV6", "mn": "EV6", "ru": "EV6"}, "EV9": {"en": "EV9", "mn": "EV9", "ru": "EV9"}, # Genesis models "G70": {"en": "G70", "mn": "G70", "ru": "G70"}, "G80": {"en": "G80", "mn": "G80", "ru": "G80"}, "G90": {"en": "G90", "mn": "G90", "ru": "G90"}, "GV60": {"en": "GV60", "mn": "GV60", "ru": "GV60"}, "GV70": {"en": "GV70", "mn": "GV70", "ru": "GV70"}, "GV80": {"en": "GV80", "mn": "GV80", "ru": "GV80"}, # SsangYong / KG Mobility models "렉스턴": {"en": "Rexton", "mn": "Рекстон", "ru": "Рекстон"}, "코란도": {"en": "Korando", "mn": "Корандо", "ru": "Корандо"}, "티볼리": {"en": "Tivoli", "mn": "Тиволи", "ru": "Тиволи"}, "토레스": {"en": "Torres", "mn": "Торрес", "ru": "Торрес"}, "무쏘": {"en": "Musso", "mn": "Муссо", "ru": "Муссо"}, # Chevrolet models "말리부": {"en": "Malibu", "mn": "Малибу", "ru": "Малибу"}, "트래버스": {"en": "Traverse", "mn": "Траверс", "ru": "Траверс"}, "트랙스": {"en": "Trax", "mn": "Тракс", "ru": "Тракс"}, "이쿼녹스": {"en": "Equinox", "mn": "Эквинокс", "ru": "Эквинокс"}, "스파크": {"en": "Spark", "mn": "Спарк", "ru": "Спарк"}, "볼트": {"en": "Bolt", "mn": "Болт", "ru": "Болт"}, # Renault Samsung models "SM6": {"en": "SM6", "mn": "SM6", "ru": "SM6"}, "QM6": {"en": "QM6", "mn": "QM6", "ru": "QM6"}, "XM3": {"en": "XM3", "mn": "XM3", "ru": "XM3"}, }, } @router.get("/categories") def get_categories(): """Get available translation categories""" return TRANSLATION_CATEGORIES @router.get("", response_model=TranslationListResponse) def get_translations( page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), category: Optional[str] = None, search: Optional[str] = None, db: Session = Depends(get_db), ): """Get all translations with pagination and filtering""" query = db.query(Translation) if category: query = query.filter(Translation.category == category) if search: search_term = f"%{search}%" query = query.filter( or_( Translation.source_text.ilike(search_term), Translation.text_en.ilike(search_term), Translation.text_mn.ilike(search_term), Translation.text_ru.ilike(search_term), ) ) total = query.count() translations = query.order_by( Translation.category, Translation.source_text ).offset((page - 1) * page_size).limit(page_size).all() return TranslationListResponse( total=total, page=page, page_size=page_size, translations=translations ) @router.get("/{translation_id}", response_model=TranslationResponse) def get_translation(translation_id: int, db: Session = Depends(get_db)): """Get a specific translation by ID""" translation = db.query(Translation).filter(Translation.id == translation_id).first() if not translation: raise HTTPException(status_code=404, detail="Translation not found") return translation @router.post("", response_model=TranslationResponse) def create_translation(data: TranslationCreate, db: Session = Depends(get_db)): """Create a new translation""" # Check if already exists existing = db.query(Translation).filter( Translation.source_text == data.source_text, Translation.category == data.category ).first() if existing: raise HTTPException(status_code=400, detail="Translation already exists for this text and category") translation = Translation(**data.dict()) db.add(translation) db.commit() db.refresh(translation) return translation @router.put("/{translation_id}", response_model=TranslationResponse) def update_translation( translation_id: int, data: TranslationUpdate, db: Session = Depends(get_db) ): """Update a translation""" translation = db.query(Translation).filter(Translation.id == translation_id).first() if not translation: raise HTTPException(status_code=404, detail="Translation not found") for key, value in data.dict(exclude_unset=True).items(): setattr(translation, key, value) db.commit() db.refresh(translation) return translation @router.delete("/{translation_id}") def delete_translation(translation_id: int, db: Session = Depends(get_db)): """Delete a translation""" translation = db.query(Translation).filter(Translation.id == translation_id).first() if not translation: raise HTTPException(status_code=404, detail="Translation not found") db.delete(translation) db.commit() return {"message": "Translation deleted"} @router.post("/bulk-lookup", response_model=TranslationBulkResponse) def bulk_lookup( request: TranslationBulkRequest, db: Session = Depends(get_db) ): """Lookup translations for multiple texts at once""" if not request.texts: return TranslationBulkResponse(translations={}) query = db.query(Translation).filter(Translation.source_text.in_(request.texts)) if request.category: query = query.filter(Translation.category == request.category) translations_db = query.all() # Build result dictionary result = {} lang_field = f"text_{request.lang}" for trans in translations_db: translated = getattr(trans, lang_field, None) if translated: result[trans.source_text] = translated else: # Fallback to source text if no translation result[trans.source_text] = trans.source_text # For texts not found in DB, return original for text in request.texts: if text not in result: result[text] = text return TranslationBulkResponse(translations=result) @router.get("/lookup/{text}") def lookup_single( text: str, lang: str = Query("en", description="Target language: en, mn, ru"), category: Optional[str] = None, db: Session = Depends(get_db) ): """Lookup translation for a single text""" query = db.query(Translation).filter(Translation.source_text == text) if category: query = query.filter(Translation.category == category) translation = query.first() if not translation: return {"source": text, "translated": text} lang_field = f"text_{lang}" translated = getattr(translation, lang_field, None) return { "source": text, "translated": translated if translated else text, "category": translation.category } def get_default_translation(source_text: str, category: str) -> dict: """Get default translations from the predefined dictionary""" if category in DEFAULT_TRANSLATIONS: return DEFAULT_TRANSLATIONS[category].get(source_text, {}) return {} # Korean year suffix translations YEAR_SUFFIX_TRANSLATIONS = { "년형": {"en": "", "mn": " он", "ru": " г."}, "년식": {"en": "", "mn": " он", "ru": " г."}, "년": {"en": "", "mn": " он", "ru": " г."}, } def translate_car_name(car_name: str, lang: str, db) -> str: """ Translate a car name like "기아 K5 2024년형" to "Kia K5 2024" by translating each component separately. """ import re if not car_name: return car_name result = car_name # First, look up all translations we have in DB all_translations = db.query(Translation).filter( Translation.category.in_(["maker", "model", "fuel", "transmission", "color"]) ).all() # Build lookup dict trans_lookup = {} for t in all_translations: lang_field = f"text_{lang}" translated = getattr(t, lang_field, None) if translated and t.source_text: trans_lookup[t.source_text] = translated # Also add from default dictionary for category in ["maker", "model", "fuel", "transmission", "color"]: if category in DEFAULT_TRANSLATIONS: for korean, translations in DEFAULT_TRANSLATIONS[category].items(): if korean not in trans_lookup and lang in translations: trans_lookup[korean] = translations[lang] # Replace Korean terms with translations (longest first to avoid partial matches) sorted_keys = sorted(trans_lookup.keys(), key=len, reverse=True) for korean in sorted_keys: if korean in result: result = result.replace(korean, trans_lookup[korean]) # Handle year format: "2024년형" -> "2024" (en) or "2024 он" (mn) or "2024 г." (ru) for suffix, translations in YEAR_SUFFIX_TRANSLATIONS.items(): pattern = r'(\d{4})' + re.escape(suffix) replacement = r'\1' + translations.get(lang, "") result = re.sub(pattern, replacement, result) # Clean up extra spaces result = re.sub(r'\s+', ' ', result).strip() return result @router.post("/auto-extract") def auto_extract_terms(db: Session = Depends(get_db)): """ Auto-extract unique terms from cars database that need translation. This creates translation entries for makers, models, fuels, colors, etc. Auto-fills with default translations from built-in dictionary. """ from ..models import Car, CarMaker, CarModel added_count = 0 # Track already added entries in this batch to avoid duplicates added_keys = set() def add_translation(source_text: str, category: str, text_en: str, text_mn: str, text_ru: str): nonlocal added_count key = (source_text, category) if key in added_keys: return # Skip duplicate in same batch existing = db.query(Translation).filter( Translation.source_text == source_text, Translation.category == category ).first() if not existing: trans = Translation( source_text=source_text, category=category, text_en=text_en, text_mn=text_mn, text_ru=text_ru ) db.add(trans) added_keys.add(key) added_count += 1 # Extract makers makers = db.query(CarMaker).all() for maker in makers: if maker.name: defaults = get_default_translation(maker.name, "maker") add_translation( maker.name, "maker", defaults.get("en") or maker.name_en or maker.name, defaults.get("mn") or maker.name, defaults.get("ru") or maker.name ) # Extract models models = db.query(CarModel).all() for model in models: if model.name: defaults = get_default_translation(model.name, "model") add_translation( model.name, "model", defaults.get("en") or model.name_en or model.name, defaults.get("mn") or model.name_en or model.name, defaults.get("ru") or model.name_en or model.name ) # Extract unique fuels fuels = db.query(Car.fuel).distinct().filter(Car.fuel.isnot(None)).all() for (fuel,) in fuels: if fuel: defaults = get_default_translation(fuel, "fuel") add_translation( fuel, "fuel", defaults.get("en") or fuel, defaults.get("mn") or fuel, defaults.get("ru") or fuel ) # Extract unique transmissions transmissions = db.query(Car.transmission).distinct().filter(Car.transmission.isnot(None)).all() for (trans_type,) in transmissions: if trans_type: defaults = get_default_translation(trans_type, "transmission") add_translation( trans_type, "transmission", defaults.get("en") or trans_type, defaults.get("mn") or trans_type, defaults.get("ru") or trans_type ) # Extract unique colors colors = db.query(Car.color).distinct().filter(Car.color.isnot(None)).all() for (color,) in colors: if color: defaults = get_default_translation(color, "color") add_translation( color, "color", defaults.get("en") or color, defaults.get("mn") or color, defaults.get("ru") or color ) # Extract unique car_names and auto-translate car_names = db.query(Car.car_name).distinct().filter(Car.car_name.isnot(None)).all() for (car_name,) in car_names: if car_name: key = (car_name, "car_name") if key not in added_keys: existing = db.query(Translation).filter( Translation.source_text == car_name, Translation.category == "car_name" ).first() if not existing: # Auto-translate car name by translating each component translated_en = translate_car_name(car_name, "en", db) translated_mn = translate_car_name(car_name, "mn", db) translated_ru = translate_car_name(car_name, "ru", db) trans = Translation( source_text=car_name, category="car_name", text_en=translated_en, text_mn=translated_mn, text_ru=translated_ru ) db.add(trans) added_keys.add(key) added_count += 1 db.commit() return {"message": f"Added {added_count} new translation entries with default translations"} @router.post("/fill-defaults") def fill_default_translations(db: Session = Depends(get_db)): """ Fill in missing translations with default values from the built-in dictionary. This updates existing entries that have null translations. """ updated_count = 0 # Get all translations that have at least one null translation translations = db.query(Translation).filter( (Translation.text_en.is_(None)) | (Translation.text_mn.is_(None)) | (Translation.text_ru.is_(None)) ).all() for trans in translations: defaults = get_default_translation(trans.source_text, trans.category) updated = False if not trans.text_en and defaults.get("en"): trans.text_en = defaults["en"] updated = True elif not trans.text_en: # Use source text as fallback for non-Korean languages trans.text_en = trans.source_text updated = True if not trans.text_mn and defaults.get("mn"): trans.text_mn = defaults["mn"] updated = True elif not trans.text_mn: trans.text_mn = trans.source_text updated = True if not trans.text_ru and defaults.get("ru"): trans.text_ru = defaults["ru"] updated = True elif not trans.text_ru: trans.text_ru = trans.source_text updated = True if updated: updated_count += 1 db.commit() return {"message": f"Updated {updated_count} translations with default values"} # AI Auto-Translation Service import httpx import json from typing import Dict, List import asyncio # Translation service configuration TRANSLATION_SERVICE = { "enabled": True, "provider": "google", # Options: "google", "deepl", "libre" "api_key": None, # Set via environment variable "libre_url": "https://libretranslate.com/translate", # Free option } async def translate_text_ai(text: str, source_lang: str, target_lang: str) -> str: """ Translate text using AI translation service. Falls back to original text if translation fails. """ if not text or not TRANSLATION_SERVICE["enabled"]: return text # Language code mapping lang_map = { "ko": "ko", "en": "en", "mn": "mn", "ru": "ru", } source = lang_map.get(source_lang, source_lang) target = lang_map.get(target_lang, target_lang) try: async with httpx.AsyncClient(timeout=10.0) as client: # Using LibreTranslate (free option) response = await client.post( TRANSLATION_SERVICE["libre_url"], json={ "q": text, "source": source, "target": target, "format": "text" }, headers={"Content-Type": "application/json"} ) if response.status_code == 200: result = response.json() return result.get("translatedText", text) else: return text except Exception as e: print(f"Translation error: {e}") return text def translate_text_sync(text: str, source_lang: str, target_lang: str) -> str: """Synchronous wrapper for AI translation""" try: return asyncio.run(translate_text_ai(text, source_lang, target_lang)) except RuntimeError: # Already in an event loop loop = asyncio.get_event_loop() if loop.is_running(): # Can't run async in sync context when loop is running return text return loop.run_until_complete(translate_text_ai(text, source_lang, target_lang)) from pydantic import BaseModel from typing import List as PyList class AutoTranslateRequest(BaseModel): target_langs: PyList[str] = ["en", "mn", "ru"] @router.post("/auto-translate/{translation_id}") def auto_translate_single( translation_id: int, request: AutoTranslateRequest = None, db: Session = Depends(get_db) ): """ Auto-translate a single translation entry using AI. Translates Korean source text to all target languages. """ if request is None: request = AutoTranslateRequest() translation = db.query(Translation).filter(Translation.id == translation_id).first() if not translation: raise HTTPException(status_code=404, detail="Translation not found") source_text = translation.source_text translations_result = {} # First check defaults defaults = get_default_translation(source_text, translation.category) # Try to translate to requested languages if "en" in request.target_langs: if not translation.text_en or translation.text_en == source_text: if defaults.get("en"): translation.text_en = defaults["en"] else: translated = translate_text_sync(source_text, "ko", "en") if translated != source_text: translation.text_en = translated translations_result["en"] = translation.text_en or "" if "mn" in request.target_langs: if not translation.text_mn or translation.text_mn == source_text: if defaults.get("mn"): translation.text_mn = defaults["mn"] else: translated = translate_text_sync(source_text, "ko", "mn") if translated != source_text: translation.text_mn = translated translations_result["mn"] = translation.text_mn or "" if "ru" in request.target_langs: if not translation.text_ru or translation.text_ru == source_text: if defaults.get("ru"): translation.text_ru = defaults["ru"] else: translated = translate_text_sync(source_text, "ko", "ru") if translated != source_text: translation.text_ru = translated translations_result["ru"] = translation.text_ru or "" db.commit() db.refresh(translation) return { "id": translation.id, "source_text": translation.source_text, "translations": translations_result, "message": f"Auto-translated to: {', '.join(request.target_langs)}" } class BatchTranslateRequest(BaseModel): target_langs: PyList[str] = ["en", "mn", "ru"] category: Optional[str] = None overwrite_existing: bool = False @router.post("/auto-translate-batch") def auto_translate_batch( request: BatchTranslateRequest = None, limit: int = Query(100, ge=1, le=500), db: Session = Depends(get_db) ): """ Auto-translate multiple translations that have missing translations. Limits batch size to avoid API rate limits. """ if request is None: request = BatchTranslateRequest() # Build query based on options if request.overwrite_existing: # Get all translations (optionally filtered by category) query = db.query(Translation) else: # Only get translations with missing values query = db.query(Translation).filter( (Translation.text_en.is_(None)) | (Translation.text_mn.is_(None)) | (Translation.text_ru.is_(None)) | (Translation.text_en == Translation.source_text) | (Translation.text_mn == Translation.source_text) | (Translation.text_ru == Translation.source_text) | (Translation.text_en == "") | (Translation.text_mn == "") | (Translation.text_ru == "") ) if request.category: query = query.filter(Translation.category == request.category) translations = query.limit(limit).all() successful = 0 failed = 0 results = [] for trans in translations: source_text = trans.source_text try: updated = False # First check if we have default translations defaults = get_default_translation(source_text, trans.category) # English if "en" in request.target_langs: if request.overwrite_existing or not trans.text_en or trans.text_en == source_text or trans.text_en == "": if defaults.get("en"): trans.text_en = defaults["en"] updated = True else: translated = translate_text_sync(source_text, "ko", "en") if translated and translated != source_text: trans.text_en = translated updated = True # Mongolian if "mn" in request.target_langs: if request.overwrite_existing or not trans.text_mn or trans.text_mn == source_text or trans.text_mn == "": if defaults.get("mn"): trans.text_mn = defaults["mn"] updated = True else: translated = translate_text_sync(source_text, "ko", "mn") if translated and translated != source_text: trans.text_mn = translated updated = True # Russian if "ru" in request.target_langs: if request.overwrite_existing or not trans.text_ru or trans.text_ru == source_text or trans.text_ru == "": if defaults.get("ru"): trans.text_ru = defaults["ru"] updated = True else: translated = translate_text_sync(source_text, "ko", "ru") if translated and translated != source_text: trans.text_ru = translated updated = True if updated: successful += 1 results.append({ "id": trans.id, "source_text": source_text, "success": True }) else: results.append({ "id": trans.id, "source_text": source_text, "success": True, "error": "No changes needed" }) except Exception as e: failed += 1 results.append({ "id": trans.id, "source_text": source_text, "success": False, "error": str(e) }) db.commit() return { "total_processed": len(translations), "successful": successful, "failed": failed, "results": results } class TranslateOnDemandRequest(BaseModel): text: str source_lang: str = "ko" target_lang: str = "en" @router.post("/translate-on-demand") def translate_on_demand( request: TranslateOnDemandRequest, db: Session = Depends(get_db) ): """ Translate a single text on-demand without saving to database. Useful for dynamic content translation. """ text = request.text target_lang = request.target_lang if not text: return {"source_text": "", "translated_text": "", "source_lang": request.source_lang, "target_lang": target_lang} translated_text = text # First, check existing translations in DB trans = db.query(Translation).filter( Translation.source_text == text ).first() if trans: lang_field = f"text_{target_lang}" translated = getattr(trans, lang_field, None) if translated and translated != text: translated_text = translated else: # Check default dictionary for category in DEFAULT_TRANSLATIONS: if text in DEFAULT_TRANSLATIONS[category]: if target_lang in DEFAULT_TRANSLATIONS[category][text]: translated_text = DEFAULT_TRANSLATIONS[category][text][target_lang] break # If still not found, try AI translation if translated_text == text: translated_text = translate_text_sync(text, request.source_lang, target_lang) return { "source_text": text, "translated_text": translated_text, "source_lang": request.source_lang, "target_lang": target_lang } @router.get("/stats") def get_translation_stats(db: Session = Depends(get_db)): """Get translation statistics""" total = db.query(Translation).count() # Count by category from sqlalchemy import func by_category = db.query( Translation.category, func.count(Translation.id) ).group_by(Translation.category).all() # Count missing translations (null or same as source) missing_en = db.query(Translation).filter( (Translation.text_en.is_(None)) | (Translation.text_en == Translation.source_text) | (Translation.text_en == "") ).count() missing_mn = db.query(Translation).filter( (Translation.text_mn.is_(None)) | (Translation.text_mn == Translation.source_text) | (Translation.text_mn == "") ).count() missing_ru = db.query(Translation).filter( (Translation.text_ru.is_(None)) | (Translation.text_ru == Translation.source_text) | (Translation.text_ru == "") ).count() translated_en = total - missing_en translated_mn = total - missing_mn translated_ru = total - missing_ru return { "total_entries": total, "by_category": {cat: count for cat, count in by_category}, "translation_coverage": { "english": { "translated": translated_en, "total": total, "percentage": round(translated_en / total * 100, 1) if total > 0 else 0 }, "mongolian": { "translated": translated_mn, "total": total, "percentage": round(translated_mn / total * 100, 1) if total > 0 else 0 }, "russian": { "translated": translated_ru, "total": total, "percentage": round(translated_ru / total * 100, 1) if total > 0 else 0 } } }