diff --git a/backend/alembic/versions/020_workspace_ddl.py b/backend/alembic/versions/020_workspace_ddl.py index 860955e7..52af1e66 100644 --- a/backend/alembic/versions/020_workspace_ddl.py +++ b/backend/alembic/versions/020_workspace_ddl.py @@ -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') + diff --git a/backend/apps/system/api/workspace.py b/backend/apps/system/api/workspace.py index 541b38b0..44f28689 100644 --- a/backend/apps/system/api/workspace.py +++ b/backend/apps/system/api/workspace.py @@ -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() \ No newline at end of file + session.commit() + +@router.post("/user/{id}") +async def bindUser(session: SessionDep, user_id: int): + pass \ No newline at end of file diff --git a/backend/common/core/deps.py b/backend/common/core/deps.py index edb1129d..84dc3659 100644 --- a/backend/common/core/deps.py +++ b/backend/common/core/deps.py @@ -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( diff --git a/backend/common/utils/locale.py b/backend/common/utils/locale.py new file mode 100644 index 00000000..32d9009d --- /dev/null +++ b/backend/common/utils/locale.py @@ -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 \ No newline at end of file diff --git a/backend/locales/en.json b/backend/locales/en.json new file mode 100644 index 00000000..a7364eda --- /dev/null +++ b/backend/locales/en.json @@ -0,0 +1,3 @@ +{ + "i18n_default_workspace": "Default workspace" +} \ No newline at end of file diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json new file mode 100644 index 00000000..97aa25ea --- /dev/null +++ b/backend/locales/zh-CN.json @@ -0,0 +1,3 @@ +{ + "i18n_default_workspace": "默认工作空间" +} \ No newline at end of file diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 957cdc15..b3d4f213 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -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 { @@ -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 diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 4df25e79..02b93612 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -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' +}