Skip to content

Commit 17fd6ff

Browse files
feat: Backend i18n
1 parent cd26d65 commit 17fd6ff

File tree

8 files changed

+124
-9
lines changed

8 files changed

+124
-9
lines changed

backend/alembic/versions/020_workspace_ddl.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88
from alembic import op
99
import sqlalchemy as sa
10-
import sqlmodel.sql.sqltypes
10+
from sqlmodel import BigInteger, String, column, table
1111

1212

1313
# revision identifiers, used by Alembic.
@@ -25,8 +25,26 @@ def upgrade():
2525
sa.Column('create_time', sa.BigInteger(), default=0, nullable=False)
2626
)
2727
op.create_index(op.f('ix_sys_workspace_id'), 'sys_workspace', ['id'], unique=False)
28+
29+
accounts_table = table(
30+
"sys_workspace",
31+
column("id", BigInteger),
32+
column("name", String),
33+
column("create_time", BigInteger),
34+
)
2835

36+
op.bulk_insert(
37+
accounts_table,
38+
[
39+
{
40+
"id": 1,
41+
"name": "i18n_default_workspace",
42+
"create_time": 1672531199000
43+
}
44+
]
45+
)
2946

3047
def downgrade():
31-
op.drop_table('sys_workspace')
3248
op.drop_index(op.f('ix_sys_workspace_id'), table_name='sys_workspace')
49+
op.drop_table('sys_workspace')
50+

backend/apps/system/api/workspace.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from fastapi import APIRouter
22
from sqlmodel import select
33
from apps.system.models.system_model import WorkspaceBase, WorkspaceEditor, WorkspaceModel
4-
from common.core.deps import SessionDep
4+
from common.core.deps import SessionDep, Trans
55
from common.utils.time import get_timestamp
66

77
router = APIRouter(tags=["system/workspace"], prefix="/system/workspace")
88

99
@router.get("", response_model=list[WorkspaceModel])
10-
async def query(session: SessionDep):
11-
return session.exec(select(WorkspaceModel).order_by(WorkspaceModel.create_time)).all()
10+
async def query(session: SessionDep, trans: Trans):
11+
list_result = session.exec(select(WorkspaceModel).order_by(WorkspaceModel.create_time)).all()
12+
for ws in list_result:
13+
if ws.name.startswith('i18n'):
14+
ws.name = trans(ws.name)
15+
return list_result
1216

1317
@router.post("")
1418
async def add(session: SessionDep, creator: WorkspaceBase):
@@ -29,10 +33,12 @@ async def update(session: SessionDep, editor: WorkspaceEditor):
2933
session.commit()
3034

3135
@router.get("/{id}", response_model=WorkspaceModel)
32-
async def get_one(session: SessionDep, id: int):
36+
async def get_one(session: SessionDep, trans: Trans, id: int):
3337
db_model = session.get(WorkspaceModel, id)
3438
if not db_model:
3539
raise ValueError(f"WorkspaceModel with id {id} not found")
40+
if db_model.name.startswith('i18n'):
41+
db_model.name = trans(db_model.name)
3642
return db_model
3743

3844
@router.delete("/{id}")
@@ -41,4 +47,8 @@ async def delete(session: SessionDep, id: int):
4147
if not db_model:
4248
raise ValueError(f"WorkspaceModel with id {id} not found")
4349
session.delete(db_model)
44-
session.commit()
50+
session.commit()
51+
52+
@router.post("/user/{id}")
53+
async def bindUser(session: SessionDep, user_id: int):
54+
pass

backend/common/core/deps.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Annotated
22

33
import jwt
4-
from fastapi import Depends, HTTPException, status
4+
from fastapi import Depends, HTTPException, Request, status
55
# from fastapi.security import OAuth2PasswordBearer
66
from jwt.exceptions import InvalidTokenError
77
from pydantic import ValidationError
@@ -13,6 +13,7 @@
1313
from common.core.config import settings
1414
from common.core.db import get_session
1515
from apps.system.models.user import UserModel
16+
from common.utils.locale import I18n
1617
reusable_oauth2 = XOAuth2PasswordBearer(
1718
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
1819
)
@@ -22,7 +23,11 @@
2223

2324
SessionDep = Annotated[Session, Depends(get_session)]
2425
TokenDep = Annotated[str, Depends(reusable_oauth2)]
26+
i18n = I18n()
27+
async def get_i18n(request: Request):
28+
return i18n(request)
2529

30+
Trans = Annotated[I18n, Depends(get_i18n)]
2631
async def get_current_user(session: SessionDep, token: TokenDep) -> BaseUserDTO:
2732
try:
2833
payload = jwt.decode(

backend/common/utils/locale.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pathlib import Path
2+
import json
3+
from typing import Dict, Optional
4+
from fastapi import Request
5+
6+
class I18n:
7+
def __init__(self, locale_dir: str = "locales"):
8+
self.locale_dir = Path(locale_dir)
9+
self.translations: Dict[str, Dict[str, str]] = {}
10+
self.load_translations()
11+
12+
def load_translations(self):
13+
14+
if not self.locale_dir.exists():
15+
self.locale_dir.mkdir()
16+
return
17+
18+
for lang_file in self.locale_dir.glob("*.json"):
19+
with open(lang_file, 'r', encoding='utf-8') as f:
20+
self.translations[lang_file.stem.lower()] = json.load(f)
21+
22+
def get_language(self, request: Request) -> str:
23+
accept_language = request.headers.get('accept-language', 'en')
24+
primary_lang = accept_language.split(',')[0].lower()
25+
26+
return primary_lang if primary_lang in self.translations else 'zh-cn'
27+
28+
def __call__(self, request: Request) -> 'I18nHelper':
29+
return I18nHelper(self, request)
30+
31+
class I18nHelper:
32+
def __init__(self, i18n: I18n, request: Request):
33+
self.i18n = i18n
34+
self.request = request
35+
self.lang = i18n.get_language(request)
36+
37+
def __call__(self, key: str, **kwargs) -> str:
38+
lang_data = self.i18n.translations.get(self.lang, {})
39+
text = lang_data.get(key, key) # 找不到则返回原key
40+
41+
if kwargs:
42+
try:
43+
return text.format(**kwargs)
44+
except (KeyError, ValueError):
45+
return text
46+
return text

backend/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"i18n_default_workspace": "Default workspace"
3+
}

backend/locales/zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"i18n_default_workspace": "默认工作空间"
3+
}

frontend/src/utils/request.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import axios, {
99
} from 'axios'
1010

1111
import { useCache } from '@/utils/useCache'
12+
import { getLocale } from './utils'
1213
const { wsCache } = useCache()
1314
// Response data structure
1415
export interface ApiResponse<T = unknown> {
@@ -69,6 +70,16 @@ class HttpService {
6970
if (token && config.headers) {
7071
config.headers['X-SQLBOT-TOKEN'] = `Bearer ${token}`
7172
}
73+
const locale = getLocale()
74+
if (locale) {
75+
/* const mapping = {
76+
'zh-CN': 'zh-CN',
77+
en: 'en-US',
78+
tw: 'zh-TW',
79+
} */
80+
/* const val = mapping[locale] || locale */
81+
config.headers['Accept-Language'] = locale
82+
}
7283
if (config.url?.includes('/xpack_static/') && config.baseURL) {
7384
config.baseURL = config.baseURL.replace('/api/v1', '')
7485
// Skip auth for xpack_static requests

frontend/src/utils/utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dayjs from 'dayjs'
2-
2+
import { useCache } from '@/utils/useCache'
3+
const { wsCache } = useCache()
34
const getCheckDate = (timestamp: any) => {
45
if (!timestamp) return false
56
const dt = new Date(timestamp)
@@ -35,3 +36,21 @@ export function getDate(time?: Date | string | number) {
3536
}
3637
return new Date(time)
3738
}
39+
40+
export const getBrowserLocale = () => {
41+
const language = navigator.language
42+
if (!language) {
43+
return 'zh-CN'
44+
}
45+
if (language.startsWith('en')) {
46+
return 'en'
47+
}
48+
if (language.toLowerCase().startsWith('zh')) {
49+
const temp = language.toLowerCase().replace('_', '-')
50+
return temp === 'zh' ? 'zh-CN' : temp === 'zh-cn' ? 'zh-CN' : 'tw'
51+
}
52+
return language
53+
}
54+
export const getLocale = () => {
55+
return wsCache.get('user.language') || getBrowserLocale() || 'zh-CN'
56+
}

0 commit comments

Comments
 (0)