feat: Backend i18n

This commit is contained in:
fit2cloud-chenyw
2025-07-07 14:16:05 +08:00
parent cd26d6594e
commit 17fd6ffbf7
8 changed files with 124 additions and 9 deletions
+20 -2
View File
@@ -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')
+15 -5
View File
@@ -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
+6 -1
View File
@@ -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(
+46
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
{
"i18n_default_workspace": "Default workspace"
}
+3
View File
@@ -0,0 +1,3 @@
{
"i18n_default_workspace": "默认工作空间"
}
+11
View File
@@ -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
+20 -1
View File
@@ -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'
}