添加多语言请求接口参数

This commit is contained in:
LittleBoy 2025-01-25 11:38:12 +08:00
parent e022bc8036
commit 381e1f16d1
13 changed files with 48 additions and 13 deletions

View File

@ -22,6 +22,11 @@ npm run build
2、单独域名部署设置环境变量ONLY_LIVE=yes使用正常编译即可 2、单独域名部署设置环境变量ONLY_LIVE=yes使用正常编译即可
#### 多语言部署
应用默认使用简体中文进行编译;如果需要指定其他语言需要设置环境变量:`APP_LANGUAGE`;
`APP_LANGUAGE`目前支持的值有:`zh-CN`(中文)、`en-US`(英语)。
**使用docker** **使用docker**
[x] TODO [x] TODO

View File

@ -56,7 +56,7 @@ export default function ButtonBatch(
if(confirmMessage){ if(confirmMessage){
modal.confirm({ modal.confirm({
wrapClassName: 'root-modal-confirm', wrapClassName: 'root-modal-confirm',
title: title || t('confirm.title'), title: <span dangerouslySetInnerHTML={{__html:title || t('confirm.title')}}></span>,
centered: true, centered: true,
icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>, icon: <span className="anticon anticon-exclamation-circle"><IconWarningCircle/></span>,
content: confirmMessage, content: confirmMessage,

View File

@ -9,6 +9,6 @@ export const DocumentTitle: React.FC<DocumentTitleProps> = ({children, title}) =
if (title || children) { if (title || children) {
document.title = title || children || ''; document.title = title || children || '';
} }
}, []); }, [title,children]);
return <></> return <></>
} }

View File

@ -0,0 +1,13 @@
const globalConfig:{
i18n?:I18n
} = {
}
function useGlobalConfig(){
return {
globalConfig
}
}
export default useGlobalConfig;

View File

@ -6,7 +6,6 @@ import LangCN from './translations/zh-CN.json';
console.log('AppConfig',AppMode) console.log('AppConfig',AppMode)
i18next.use(initReactI18next).init({ i18next.use(initReactI18next).init({
debug: true, debug: true,
lng:'en-US',
fallbackLng: 'en-US', fallbackLng: 'en-US',
resources: { resources: {
'en-US': {translation:LangEN}, 'en-US': {translation:LangEN},

View File

@ -128,7 +128,7 @@
}, },
"video": { "video": {
"delete_confirm_title": "Are you sure you want to delete this video?", "delete_confirm_title": "Are you sure you want to delete this video?",
"delete_confirm": "These videos will be deleted.<br.>They can be recovered from the “news” page. ", "delete_confirm": "These videos will be deleted.<br/> They can be recovered from the “news” page. ",
"delete_description": "Are you sure you want to delete these {{count}} videos?", "delete_description": "Are you sure you want to delete these {{count}} videos?",
"delete_empty": "Select the video you want to delete", "delete_empty": "Select the video you want to delete",
"download": "Download", "download": "Download",

View File

@ -49,7 +49,7 @@ export default function SearchForm({onSearch}: Props) {
placeholder={t("history.search_key")} placeholder={t("history.search_key")}
/> />
<TimeSelect <TimeSelect
className="w-[120px] ml-1" className="w-[140px] ml-1"
value={state.time_flag} value={state.time_flag}
onChange={handleTimeFilter} onChange={handleTimeFilter}
/> />

View File

@ -106,17 +106,17 @@ export default function LibraryIndex() {
<div className="live-control flex justify-between mb-2"> <div className="live-control flex justify-between mb-2">
<div className="pl-[70px]"></div> <div className="pl-[70px]"></div>
<div className="flex items-center"> <div className="flex items-center">
<Space className="text-gray-400"> <Space className="text-gray-400" size={20}>
<span>{t('select.total',{count:data?.list.length || 0})}</span> <span>{t('select.total',{count:data?.list?.length || 0})}</span>
<span>{t('select.pushed',{count:state.pushedCount})}</span> <span>{t('select.pushed',{count:state.pushedCount})}</span>
<span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span> <span className={'text-blue-500'}>{t('select.selected_some',{count:checkedIdArray.length})}</span>
</Space> </Space>
<button className="hover:text-blue-300 text-gray-400 ml-2" <button className="hover:text-blue-300 text-gray-400 ml-4"
onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}> onClick={() => handleAllCheckedChange(checkedIdArray.length != data?.list.length)}>
<span className="text-sm mr-2">{t("select.select_all")}</span> <span className="text-sm mr-2">{t("select.select_all")}</span>
{/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/} {/*<CheckCircleFilled className={clsx({'text-blue-500': state.checkedAll})}/>*/}
</button> </button>
<Checkbox checked={checkedIdArray.length == data?.list.length} <Checkbox checked={checkedIdArray.length == data?.list?.length}
onChange={e => handleAllCheckedChange(e.target.checked)}/> onChange={e => handleAllCheckedChange(e.target.checked)}/>
</div> </div>
</div> </div>
@ -161,7 +161,7 @@ export default function LibraryIndex() {
icon={<IconDelete className=""/>} icon={<IconDelete className=""/>}
title={t('video.delete_description',{count:checkedIdArray.length})} title={t('video.delete_description',{count:checkedIdArray.length})}
emptyMessage={t('video.delete_empty')} emptyMessage={t('video.delete_empty')}
confirmMessage={t('video.delete_confirm')} confirmMessage={<span dangerouslySetInnerHTML={{__html:t('video.delete_confirm')}}></span>}
onProcess={deleteHistories} onProcess={deleteHistories}
>{t('delete_batch')}</ButtonBatch>} >{t('delete_batch')}</ButtonBatch>}
{checkedIdArray?.length > 0 && <ButtonBatch {checkedIdArray?.length > 0 && <ButtonBatch

View File

@ -11,6 +11,7 @@ import routes from "@/routes/routes.tsx";
import {DocumentTitle} from "@/components/document.tsx"; import {DocumentTitle} from "@/components/document.tsx";
import useConfig from "@/hooks/useConfig.ts"; import useConfig from "@/hooks/useConfig.ts";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import useGlobalConfig from "@/hooks/useGlobalConfig.ts";
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -27,6 +28,7 @@ const router = createBrowserRouter([
} }
}) })
const {globalConfig} = useGlobalConfig();
// future={{v7_startTransition: true,v7_relativeSplatPath: true}} // future={{v7_startTransition: true,v7_relativeSplatPath: true}}
const AppRouter = () => { const AppRouter = () => {
const {t,i18n:langConfig} = useTranslation(); const {t,i18n:langConfig} = useTranslation();
@ -37,6 +39,7 @@ const AppRouter = () => {
}else{ }else{
dayjs.locale('en') dayjs.locale('en')
} }
globalConfig.i18n = i18n
langConfig.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n)) langConfig.changeLanguage(i18n).then(()=>console.log('change lang to ',i18n))
}, [i18n]) }, [i18n])

View File

@ -1,4 +1,4 @@
import {Outlet, useLocation, useNavigate} from "react-router-dom"; import {Outlet, useLocation, useNavigate, useSearchParams} from "react-router-dom";
import {Button, Divider, Dropdown, MenuProps} from "antd"; import {Button, Divider, Dropdown, MenuProps} from "antd";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
@ -78,6 +78,7 @@ const NavigationUserContainer = () => {
} }
export const BaseLayout: React.FC<LayoutProps> = ({children}) => { export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
const {i18n,onChangeLocalization} = useConfig(); const {i18n,onChangeLocalization} = useConfig();
const [params] = useSearchParams();
return (<div className={'dashboard-layout min-h-screen'}> return (<div className={'dashboard-layout min-h-screen'}>
<div className="min-h-screen w-full"> <div className="min-h-screen w-full">
<div className="app-header"> <div className="app-header">
@ -86,6 +87,7 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
</div> </div>
<DashboardNavigation/> <DashboardNavigation/>
<div className="flex items-center"> <div className="flex items-center">
{params.get('lang') || AppConfig.APP_LANG == 'multiple' && <div>
{ {
i18n == 'zh-CN'?( i18n == 'zh-CN'?(
<Button className="ml-2" onClick={()=>onChangeLocalization('en-US')}>Change To EN</Button> <Button className="ml-2" onClick={()=>onChangeLocalization('en-US')}>Change To EN</Button>
@ -93,6 +95,7 @@ export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
<Button className="ml-2" onClick={()=>onChangeLocalization('zh-CN')}></Button> <Button className="ml-2" onClick={()=>onChangeLocalization('zh-CN')}></Button>
) )
} }
</div>}
<NavigationUserContainer/> <NavigationUserContainer/>
</div> </div>
</div> </div>
@ -116,7 +119,7 @@ const DashboardLayout: React.FC<{ children?: React.ReactNode }> = ({children}) =
} }
},[]) },[])
return <AuthGuard> return <AuthGuard>
<div className="fixed">first path:{defaultCache.firstLoadPath}</div> <div className="fixed">{defaultCache.firstLoadPath}</div>
<BaseLayout> <BaseLayout>
{children ? children : <Outlet/>} {children ? children : <Outlet/>}
</BaseLayout> </BaseLayout>

View File

@ -2,6 +2,7 @@ import axios from 'axios';
import {stringify} from 'qs' import {stringify} from 'qs'
import {BizError} from './types'; import {BizError} from './types';
import {getAuthToken} from "@/hooks/useAuth.ts"; import {getAuthToken} from "@/hooks/useAuth.ts";
import useGlobalConfig from '@/hooks/useGlobalConfig';
const JSON_FORMAT: string = 'application/json'; const JSON_FORMAT: string = 'application/json';
const REQUEST_TIMEOUT = 300000; // 超时时长5min const REQUEST_TIMEOUT = 300000; // 超时时长5min
@ -11,10 +12,19 @@ const Axios = axios.create({
headers: {'Content-Type': JSON_FORMAT} headers: {'Content-Type': JSON_FORMAT}
}) })
const {globalConfig} = useGlobalConfig();
// 请求前拦截 // 请求前拦截
Axios.interceptors.request.use(config => { Axios.interceptors.request.use(config => {
const token = getAuthToken(); const token = getAuthToken();
if (globalConfig.i18n){
let url = config.url;
if(url){
url += (url.indexOf('?') == -1?'?':'&') + `lang=${globalConfig.i18n || ''}`
config.url = url;
}
//config.headers['language'] = globalConfig.i18n;
}
if (token) { if (token) {
config.headers['Token'] = `${token}`; config.headers['Token'] = `${token}`;
} }

1
src/vite-env.d.ts vendored
View File

@ -13,6 +13,7 @@ declare const AppConfig: {
AUTHED_PERSON_DATA_KEY: string; AUTHED_PERSON_DATA_KEY: string;
API_PREFIX: string; API_PREFIX: string;
ONLY_LIVE: string; ONLY_LIVE: string;
APP_LANG: string;
}; };
declare const AppMode: 'test' | 'production' | 'development'; declare const AppMode: 'test' | 'production' | 'development';

View File

@ -22,6 +22,7 @@ export default defineConfig(({mode}) => {
AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || AUTH_TOKEN_KEY, AUTH_TOKEN_KEY: process.env.AUTH_TOKEN_KEY || AUTH_TOKEN_KEY,
AUTHED_PERSON_DATA_KEY: process.env.AUTHED_PERSON_DATA_KEY || 'digital-person-user-info', AUTHED_PERSON_DATA_KEY: process.env.AUTHED_PERSON_DATA_KEY || 'digital-person-user-info',
ONLY_LIVE: process.env.ONLY_LIVE || 'no', ONLY_LIVE: process.env.ONLY_LIVE || 'no',
APP_LANG: process.env.APP_LANGUAGE || 'zh-CN'
}), }),
AppMode: JSON.stringify(mode), AppMode: JSON.stringify(mode),
AppBuildVersion: JSON.stringify(AppPackage.name + '-' + AppPackage.version + '-' + dayjs().format('YYYYMMDDHH_mmss')) AppBuildVersion: JSON.stringify(AppPackage.name + '-' + AppPackage.version + '-' + dayjs().format('YYYYMMDDHH_mmss'))