131 lines
4.1 KiB
Python
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
|
|
}
|
|
}
|