guardia-itsm/routers/i18n_engine.py

131 lines
4.1 KiB
Python

"""미래 준비 — 다국어(i18n) 번역 데이터 관리."""
from __future__ import annotations
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, update, delete
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, I18nEntry
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/i18n", tags=["미래준비-다국어"])
SUPPORTED_LOCALES = ["ko", "en", "ja", "zh"]
class TranslationIn(BaseModel):
key: str
locale: str
value: str
namespace: str = "common"
class BulkTranslationIn(BaseModel):
locale: str
namespace: str = "common"
entries: dict[str, str]
@router.get("/locales")
async def list_locales(user: User = Depends(get_current_user)):
"""지원 로케일 목록."""
return {"locales": SUPPORTED_LOCALES}
@router.get("/translations/{locale}")
async def get_translations(
locale: str,
namespace: str = Query("common"),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""특정 로케일 + 네임스페이스 번역 데이터 반환 (JSON 맵)."""
if locale not in SUPPORTED_LOCALES:
raise HTTPException(400, f"지원 로케일: {SUPPORTED_LOCALES}")
rows = await db.execute(
select(I18nEntry).where(I18nEntry.locale == locale, I18nEntry.namespace == namespace)
)
return {e.key: e.value for e in rows.scalars().all()}
@router.put("/translation")
async def upsert_translation(
t: TranslationIn,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
"""번역 항목 추가/수정."""
if t.locale not in SUPPORTED_LOCALES:
raise HTTPException(400, f"지원 로케일: {SUPPORTED_LOCALES}")
existing = await db.execute(
select(I18nEntry).where(
I18nEntry.key == t.key, I18nEntry.locale == t.locale, I18nEntry.namespace == t.namespace
).limit(1)
)
entry = existing.scalar_one_or_none()
if entry:
entry.value = t.value
entry.updated_at = datetime.utcnow()
else:
entry = I18nEntry(
key=t.key, locale=t.locale, value=t.value,
namespace=t.namespace, created_at=datetime.utcnow(),
)
db.add(entry)
await db.commit()
return {"ok": True}
@router.post("/bulk")
async def bulk_import(
req: BulkTranslationIn,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
"""번역 데이터 일괄 임포트 (JSON 맵)."""
if req.locale not in SUPPORTED_LOCALES:
raise HTTPException(400, f"지원 로케일: {SUPPORTED_LOCALES}")
count = 0
for key, value in req.entries.items():
existing = await db.execute(
select(I18nEntry).where(
I18nEntry.key == key, I18nEntry.locale == req.locale, I18nEntry.namespace == req.namespace
).limit(1)
)
entry = existing.scalar_one_or_none()
if entry:
entry.value = value
entry.updated_at = datetime.utcnow()
else:
db.add(I18nEntry(
key=key, locale=req.locale, value=value,
namespace=req.namespace, created_at=datetime.utcnow(),
))
count += 1
await db.commit()
return {"ok": True, "imported": count}
@router.get("/coverage")
async def translation_coverage(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""로케일별 번역 커버리지."""
from sqlalchemy import func as sqlfunc
rows = await db.execute(
select(I18nEntry.locale, sqlfunc.count().label("cnt"))
.group_by(I18nEntry.locale)
)
result = {r.locale: r.cnt for r in rows.all()}
base = result.get("ko", 1)
return {
"coverage": {
locale: {"count": result.get(locale, 0), "pct": round(result.get(locale, 0) / base * 100)}
for locale in SUPPORTED_LOCALES
}
}