mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: Backend i18n
This commit is contained in:
@@ -7,7 +7,7 @@ Create Date: 2025-07-06 18:03:36.143060
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel.sql.sqltypes
|
||||
from sqlmodel import BigInteger, String, column, table
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
@@ -25,8 +25,26 @@ def upgrade():
|
||||
sa.Column('create_time', sa.BigInteger(), default=0, nullable=False)
|
||||
)
|
||||
op.create_index(op.f('ix_sys_workspace_id'), 'sys_workspace', ['id'], unique=False)
|
||||
|
||||
accounts_table = table(
|
||||
"sys_workspace",
|
||||
column("id", BigInteger),
|
||||
column("name", String),
|
||||
column("create_time", BigInteger),
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
accounts_table,
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "i18n_default_workspace",
|
||||
"create_time": 1672531199000
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('sys_workspace')
|
||||
op.drop_index(op.f('ix_sys_workspace_id'), table_name='sys_workspace')
|
||||
op.drop_table('sys_workspace')
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
from sqlmodel import select
|
||||
from apps.system.models.system_model import WorkspaceBase, WorkspaceEditor, WorkspaceModel
|
||||
from common.core.deps import SessionDep
|
||||
from common.core.deps import SessionDep, Trans
|
||||
from common.utils.time import get_timestamp
|
||||
|
||||
router = APIRouter(tags=["system/workspace"], prefix="/system/workspace")
|
||||
|
||||
@router.get("", response_model=list[WorkspaceModel])
|
||||
async def query(session: SessionDep):
|
||||
return session.exec(select(WorkspaceModel).order_by(WorkspaceModel.create_time)).all()
|
||||
async def query(session: SessionDep, trans: Trans):
|
||||
list_result = session.exec(select(WorkspaceModel).order_by(WorkspaceModel.create_time)).all()
|
||||
for ws in list_result:
|
||||
if ws.name.startswith('i18n'):
|
||||
ws.name = trans(ws.name)
|
||||
return list_result
|
||||
|
||||
@router.post("")
|
||||
async def add(session: SessionDep, creator: WorkspaceBase):
|
||||
@@ -29,10 +33,12 @@ async def update(session: SessionDep, editor: WorkspaceEditor):
|
||||
session.commit()
|
||||
|
||||
@router.get("/{id}", response_model=WorkspaceModel)
|
||||
async def get_one(session: SessionDep, id: int):
|
||||
async def get_one(session: SessionDep, trans: Trans, id: int):
|
||||
db_model = session.get(WorkspaceModel, id)
|
||||
if not db_model:
|
||||
raise ValueError(f"WorkspaceModel with id {id} not found")
|
||||
if db_model.name.startswith('i18n'):
|
||||
db_model.name = trans(db_model.name)
|
||||
return db_model
|
||||
|
||||
@router.delete("/{id}")
|
||||
@@ -41,4 +47,8 @@ async def delete(session: SessionDep, id: int):
|
||||
if not db_model:
|
||||
raise ValueError(f"WorkspaceModel with id {id} not found")
|
||||
session.delete(db_model)
|
||||
session.commit()
|
||||
session.commit()
|
||||
|
||||
@router.post("/user/{id}")
|
||||
async def bindUser(session: SessionDep, user_id: int):
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
# from fastapi.security import OAuth2PasswordBearer
|
||||
from jwt.exceptions import InvalidTokenError
|
||||
from pydantic import ValidationError
|
||||
@@ -13,6 +13,7 @@ from common.core import security
|
||||
from common.core.config import settings
|
||||
from common.core.db import get_session
|
||||
from apps.system.models.user import UserModel
|
||||
from common.utils.locale import I18n
|
||||
reusable_oauth2 = XOAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
|
||||
)
|
||||
@@ -22,7 +23,11 @@ reusable_oauth2 = XOAuth2PasswordBearer(
|
||||
|
||||
SessionDep = Annotated[Session, Depends(get_session)]
|
||||
TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||
i18n = I18n()
|
||||
async def get_i18n(request: Request):
|
||||
return i18n(request)
|
||||
|
||||
Trans = Annotated[I18n, Depends(get_i18n)]
|
||||
async def get_current_user(session: SessionDep, token: TokenDep) -> BaseUserDTO:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Dict, Optional
|
||||
from fastapi import Request
|
||||
|
||||
class I18n:
|
||||
def __init__(self, locale_dir: str = "locales"):
|
||||
self.locale_dir = Path(locale_dir)
|
||||
self.translations: Dict[str, Dict[str, str]] = {}
|
||||
self.load_translations()
|
||||
|
||||
def load_translations(self):
|
||||
|
||||
if not self.locale_dir.exists():
|
||||
self.locale_dir.mkdir()
|
||||
return
|
||||
|
||||
for lang_file in self.locale_dir.glob("*.json"):
|
||||
with open(lang_file, 'r', encoding='utf-8') as f:
|
||||
self.translations[lang_file.stem.lower()] = json.load(f)
|
||||
|
||||
def get_language(self, request: Request) -> str:
|
||||
accept_language = request.headers.get('accept-language', 'en')
|
||||
primary_lang = accept_language.split(',')[0].lower()
|
||||
|
||||
return primary_lang if primary_lang in self.translations else 'zh-cn'
|
||||
|
||||
def __call__(self, request: Request) -> 'I18nHelper':
|
||||
return I18nHelper(self, request)
|
||||
|
||||
class I18nHelper:
|
||||
def __init__(self, i18n: I18n, request: Request):
|
||||
self.i18n = i18n
|
||||
self.request = request
|
||||
self.lang = i18n.get_language(request)
|
||||
|
||||
def __call__(self, key: str, **kwargs) -> str:
|
||||
lang_data = self.i18n.translations.get(self.lang, {})
|
||||
text = lang_data.get(key, key) # 找不到则返回原key
|
||||
|
||||
if kwargs:
|
||||
try:
|
||||
return text.format(**kwargs)
|
||||
except (KeyError, ValueError):
|
||||
return text
|
||||
return text
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"i18n_default_workspace": "Default workspace"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"i18n_default_workspace": "默认工作空间"
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import axios, {
|
||||
} from 'axios'
|
||||
|
||||
import { useCache } from '@/utils/useCache'
|
||||
import { getLocale } from './utils'
|
||||
const { wsCache } = useCache()
|
||||
// Response data structure
|
||||
export interface ApiResponse<T = unknown> {
|
||||
@@ -69,6 +70,16 @@ class HttpService {
|
||||
if (token && config.headers) {
|
||||
config.headers['X-SQLBOT-TOKEN'] = `Bearer ${token}`
|
||||
}
|
||||
const locale = getLocale()
|
||||
if (locale) {
|
||||
/* const mapping = {
|
||||
'zh-CN': 'zh-CN',
|
||||
en: 'en-US',
|
||||
tw: 'zh-TW',
|
||||
} */
|
||||
/* const val = mapping[locale] || locale */
|
||||
config.headers['Accept-Language'] = locale
|
||||
}
|
||||
if (config.url?.includes('/xpack_static/') && config.baseURL) {
|
||||
config.baseURL = config.baseURL.replace('/api/v1', '')
|
||||
// Skip auth for xpack_static requests
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { useCache } from '@/utils/useCache'
|
||||
const { wsCache } = useCache()
|
||||
const getCheckDate = (timestamp: any) => {
|
||||
if (!timestamp) return false
|
||||
const dt = new Date(timestamp)
|
||||
@@ -35,3 +36,21 @@ export function getDate(time?: Date | string | number) {
|
||||
}
|
||||
return new Date(time)
|
||||
}
|
||||
|
||||
export const getBrowserLocale = () => {
|
||||
const language = navigator.language
|
||||
if (!language) {
|
||||
return 'zh-CN'
|
||||
}
|
||||
if (language.startsWith('en')) {
|
||||
return 'en'
|
||||
}
|
||||
if (language.toLowerCase().startsWith('zh')) {
|
||||
const temp = language.toLowerCase().replace('_', '-')
|
||||
return temp === 'zh' ? 'zh-CN' : temp === 'zh-cn' ? 'zh-CN' : 'tw'
|
||||
}
|
||||
return language
|
||||
}
|
||||
export const getLocale = () => {
|
||||
return wsCache.get('user.language') || getBrowserLocale() || 'zh-CN'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user